milk-lib 0.0.17 → 0.0.18
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.
- package/README.md +4 -3
- package/dist/components/Icon/IconLoading.svelte +62 -0
- package/dist/components/Icon/IconLoading.svelte.d.ts +6 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/index.js +1 -0
- package/dist/components/Modal/DialogContent.svelte +13 -0
- package/dist/components/Modal/DialogContent.svelte.d.ts +4 -0
- package/dist/components/Modal/DialogDescription.svelte +13 -0
- package/dist/components/Modal/DialogDescription.svelte.d.ts +4 -0
- package/dist/components/Modal/DialogFooter.svelte +16 -0
- package/dist/components/Modal/DialogFooter.svelte.d.ts +4 -0
- package/dist/components/Modal/DialogHeader.svelte +13 -0
- package/dist/components/Modal/DialogHeader.svelte.d.ts +4 -0
- package/dist/components/Modal/DialogTitle.svelte +15 -0
- package/dist/components/Modal/DialogTitle.svelte.d.ts +4 -0
- package/dist/components/Modal/Modal.svelte +122 -0
- package/dist/components/Modal/Modal.svelte.d.ts +4 -0
- package/dist/components/Modal/Modal.types.d.ts +30 -0
- package/dist/components/Modal/Modal.types.js +1 -0
- package/dist/components/Modal/ModalDialog.svelte +33 -0
- package/dist/components/Modal/ModalDialog.svelte.d.ts +4 -0
- package/dist/components/Modal/index.d.ts +7 -0
- package/dist/components/Modal/index.js +7 -0
- package/dist/components/Portal/Portal.svelte +37 -0
- package/dist/components/Portal/Portal.svelte.d.ts +7 -0
- package/dist/components/Portal/index.d.ts +1 -0
- package/dist/components/Portal/index.js +1 -0
- package/dist/components/Select/Select.svelte +258 -0
- package/dist/components/Select/Select.svelte.d.ts +4 -0
- package/dist/components/Select/Select.types.d.ts +19 -0
- package/dist/components/Select/Select.types.js +1 -0
- package/dist/components/Select/index.d.ts +2 -0
- package/dist/components/Select/index.js +2 -0
- package/dist/components/TextInput/TextInput.svelte +1 -0
- package/dist/components/TextInputBlock/TextInputBlock.svelte +21 -17
- package/dist/components/ThemeProvider/ThemeDefault.js +3 -0
- package/dist/components/ThemeProvider/ThemeProvider.svelte +11 -3
- package/dist/components/ThemeProvider/ThemeProvider.types.d.ts +3 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +4 -0
- package/dist/styles/index.scss +6 -1
- package/dist/styles/root.scss +117 -0
- package/dist/styles/typography.scss +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,13 @@ Sveltekit library, powered by [`sv`](https://npmjs.com/package/sv).
|
|
|
10
10
|
+ TextInput Box
|
|
11
11
|
+ Command
|
|
12
12
|
|
|
13
|
+
+ Icons
|
|
14
|
+
+ Select
|
|
15
|
+
+ Modal Dialog
|
|
16
|
+
|
|
13
17
|
- Alert Dialog
|
|
14
18
|
- Accordion
|
|
15
19
|
- Sheet
|
|
16
20
|
- Menu
|
|
17
21
|
- Card
|
|
18
22
|
- Checkbox
|
|
19
|
-
- Select
|
|
20
|
-
- Dialogs
|
|
21
|
-
- Icons
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let { size=16 }: { size?: number } = $props();
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<div class="loading-indicator" style={`font-size: ${size}px;`}>
|
|
6
|
+
<span class="loading-indicator-item item-1"></span>
|
|
7
|
+
<span class="loading-indicator-item item-2"></span>
|
|
8
|
+
<span class="loading-indicator-item item-3"></span>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style>.loading-indicator {
|
|
12
|
+
display: inline-flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
width: 1em;
|
|
15
|
+
height: 1em;
|
|
16
|
+
padding: 0.375em 0;
|
|
17
|
+
gap: 0.125em;
|
|
18
|
+
}
|
|
19
|
+
.loading-indicator .loading-indicator-item {
|
|
20
|
+
width: 0.25em;
|
|
21
|
+
height: 0.25em;
|
|
22
|
+
background: var(--icon-base-placeholder);
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
transform-origin: center;
|
|
25
|
+
will-change: transform, opacity;
|
|
26
|
+
}
|
|
27
|
+
.loading-indicator .loading-indicator-item.item-1 {
|
|
28
|
+
animation: blink-1 1s ease-in-out 0ms infinite;
|
|
29
|
+
}
|
|
30
|
+
.loading-indicator .loading-indicator-item.item-2 {
|
|
31
|
+
animation: blink-1 1s ease-in-out 160ms infinite;
|
|
32
|
+
}
|
|
33
|
+
.loading-indicator .loading-indicator-item.item-3 {
|
|
34
|
+
animation: blink-1 1s ease-in-out 320ms infinite;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@keyframes blink-1 {
|
|
38
|
+
0% {
|
|
39
|
+
opacity: 0.5;
|
|
40
|
+
transform: scale(0.5);
|
|
41
|
+
}
|
|
42
|
+
20% {
|
|
43
|
+
opacity: 1;
|
|
44
|
+
transform: scale(1);
|
|
45
|
+
}
|
|
46
|
+
40% {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
transform: scale(1);
|
|
49
|
+
}
|
|
50
|
+
60% {
|
|
51
|
+
opacity: 0.5;
|
|
52
|
+
transform: scale(0.5);
|
|
53
|
+
}
|
|
54
|
+
80% {
|
|
55
|
+
opacity: 0;
|
|
56
|
+
transform: scale(0);
|
|
57
|
+
}
|
|
58
|
+
100% {
|
|
59
|
+
opacity: 0;
|
|
60
|
+
transform: scale(0);
|
|
61
|
+
}
|
|
62
|
+
}</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as IconLoading } from './IconLoading.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as IconLoading } from './IconLoading.svelte';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IDialogContentProps } from "./Modal.types";
|
|
3
|
+
|
|
4
|
+
let { children }: IDialogContentProps = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="DialogContent">
|
|
8
|
+
{@render children()}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style>.DialogContent {
|
|
12
|
+
padding: 0.5rem 1.5rem;
|
|
13
|
+
}</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IDialogDescriptionProps } from "./Modal.types";
|
|
3
|
+
let { children }: IDialogDescriptionProps = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="DialogDescription">
|
|
7
|
+
{@render children()}
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<style>.DialogDescription {
|
|
11
|
+
color: var(--text-base-muted);
|
|
12
|
+
font-size: var(--font-size-md);
|
|
13
|
+
}</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IDialogFooterProps } from "./Modal.types";
|
|
3
|
+
|
|
4
|
+
let { children }: IDialogFooterProps = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="DialogFooter">
|
|
8
|
+
{@render children()}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style>.DialogFooter {
|
|
12
|
+
padding: 0.5rem 1.5rem;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: flex-end;
|
|
15
|
+
gap: 0.5rem;
|
|
16
|
+
}</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IDialogHeaderProps } from "./Modal.types";
|
|
3
|
+
|
|
4
|
+
let { children }: IDialogHeaderProps = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<div class="DialogHeader">
|
|
8
|
+
{@render children()}
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<style>.DialogHeader {
|
|
12
|
+
padding: 0.25rem 1.5rem;
|
|
13
|
+
}</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { IDialogTitleProps } from './Modal.types';
|
|
3
|
+
let { children } : IDialogTitleProps = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<h2 class="DialogTitle">
|
|
7
|
+
{@render children()}
|
|
8
|
+
</h2>
|
|
9
|
+
|
|
10
|
+
<style>.DialogTitle {
|
|
11
|
+
font-size: var(--font-size-h2);
|
|
12
|
+
margin: 0 0 0.5em;
|
|
13
|
+
font-weight: 500;
|
|
14
|
+
margin: 0 0 0.25rem;
|
|
15
|
+
}</style>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
|
|
3
|
+
Tech wrapper for Modal Window with Backdrop, Close Button and handlers
|
|
4
|
+
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
|
|
9
|
+
import Portal from '../Portal/Portal.svelte';
|
|
10
|
+
import { CloseFillSystem } from 'svelte-remix';
|
|
11
|
+
import type { IModalProps } from './Modal.types';
|
|
12
|
+
|
|
13
|
+
let { isVisible, hideModal, showCloseButton, hideOnEscape, blackout, closeOnBackdrop, children }: IModalProps= $props();
|
|
14
|
+
|
|
15
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
16
|
+
if (e.key === "Escape" && hideOnEscape && isVisible) {
|
|
17
|
+
hideModal?.();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
$effect(() => {
|
|
22
|
+
if (document) {
|
|
23
|
+
if (isVisible) {
|
|
24
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
25
|
+
document.body.style.overflow = 'hidden';
|
|
26
|
+
} else {
|
|
27
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
28
|
+
document.body.style.overflow = 'auto';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
34
|
+
document.body.style.overflow = 'auto';
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const backdropClick = (e: MouseEvent) => {
|
|
39
|
+
if (closeOnBackdrop && (e.target as HTMLElement).classList.contains("Modal-content")) {
|
|
40
|
+
hideModal?.();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
<Portal>
|
|
48
|
+
<div class="Modal">
|
|
49
|
+
<div class={`Modal-backdrop ${blackout ? "Modal-backdrop-blackout" : ""}`} ></div>
|
|
50
|
+
|
|
51
|
+
<div
|
|
52
|
+
class="Modal-content"
|
|
53
|
+
onmouseup={backdropClick}
|
|
54
|
+
role="dialog"
|
|
55
|
+
tabindex="-1"
|
|
56
|
+
aria-label="Close dialog"
|
|
57
|
+
>
|
|
58
|
+
{#if showCloseButton}
|
|
59
|
+
<div class="Modal-close-button-wrapper" role="button" tabindex="-1" onmouseup={() => hideModal?.()}>
|
|
60
|
+
<button class="Modal-close-button">
|
|
61
|
+
<CloseFillSystem/>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
{@render children()}
|
|
67
|
+
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</Portal>
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
<style>.Modal-backdrop {
|
|
74
|
+
position: fixed;
|
|
75
|
+
display: flex;
|
|
76
|
+
z-index: var(--zindex-modal-backdrop);
|
|
77
|
+
top: 0;
|
|
78
|
+
left: 0;
|
|
79
|
+
right: 0;
|
|
80
|
+
bottom: 0;
|
|
81
|
+
background-color: var(--bg-backdrop);
|
|
82
|
+
}
|
|
83
|
+
.Modal-backdrop.Modal-backdrop-blackout {
|
|
84
|
+
background-color: var(--bg-backdrop-blackout);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.Modal-close-button-wrapper {
|
|
88
|
+
position: fixed;
|
|
89
|
+
right: 0.25rem;
|
|
90
|
+
top: 0.25rem;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.Modal-close-button {
|
|
94
|
+
background-color: transparent;
|
|
95
|
+
position: relative;
|
|
96
|
+
z-index: var(--zindex-modal);
|
|
97
|
+
font-size: 1rem;
|
|
98
|
+
height: 2em;
|
|
99
|
+
width: 2em;
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
display: flex;
|
|
102
|
+
color: white;
|
|
103
|
+
border-radius: var(--border-radius-button);
|
|
104
|
+
transition: var(--transition-base);
|
|
105
|
+
padding: 0.25rem;
|
|
106
|
+
border: none;
|
|
107
|
+
}
|
|
108
|
+
.Modal-close-button:hover {
|
|
109
|
+
background: rgba(255, 255, 255, 0.1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.Modal-content {
|
|
113
|
+
position: fixed;
|
|
114
|
+
z-index: var(--zindex-modal);
|
|
115
|
+
top: 0;
|
|
116
|
+
height: 100vh;
|
|
117
|
+
width: 100%;
|
|
118
|
+
overflow-y: scroll;
|
|
119
|
+
background: transparent;
|
|
120
|
+
display: flex;
|
|
121
|
+
padding: 2rem 0;
|
|
122
|
+
}</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
export interface IModalProps {
|
|
3
|
+
isVisible?: boolean;
|
|
4
|
+
hideModal?: () => unknown;
|
|
5
|
+
showCloseButton?: boolean;
|
|
6
|
+
hideOnEscape?: boolean;
|
|
7
|
+
blackout?: boolean;
|
|
8
|
+
closeOnBackdrop?: boolean;
|
|
9
|
+
children: Snippet;
|
|
10
|
+
}
|
|
11
|
+
export interface IModalDialogProps {
|
|
12
|
+
rounded?: boolean;
|
|
13
|
+
size?: 'sm' | 'md' | 'lg';
|
|
14
|
+
children: Snippet;
|
|
15
|
+
}
|
|
16
|
+
export interface IDialogHeaderProps {
|
|
17
|
+
children: Snippet;
|
|
18
|
+
}
|
|
19
|
+
export interface IDialogFooterProps {
|
|
20
|
+
children: Snippet;
|
|
21
|
+
}
|
|
22
|
+
export interface IDialogTitleProps {
|
|
23
|
+
children: Snippet;
|
|
24
|
+
}
|
|
25
|
+
export interface IDialogDescriptionProps {
|
|
26
|
+
children: Snippet;
|
|
27
|
+
}
|
|
28
|
+
export interface IDialogContentProps {
|
|
29
|
+
children: Snippet;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import {} from "svelte";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
|
|
3
|
+
White panel for Modal Window
|
|
4
|
+
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import { type IModalDialogProps } from './Modal.types';
|
|
9
|
+
let { children, rounded, size='md' } : IModalDialogProps = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class={`ModalDialog ModalDialog-${size}`} style={`border-radius: ${rounded ? 'var(--border-radius-window)' : '0'}`}>
|
|
13
|
+
{@render children()}
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<style>.ModalDialog {
|
|
17
|
+
padding: 1rem 0;
|
|
18
|
+
background-color: white;
|
|
19
|
+
width: 440px;
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
position: relative;
|
|
22
|
+
margin: auto;
|
|
23
|
+
z-index: var(--zindex-modal);
|
|
24
|
+
box-shadow: 0 0 200px rgba(0, 0, 0, 0.1);
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
}
|
|
28
|
+
.ModalDialog.ModalDialog-sm {
|
|
29
|
+
width: 380px;
|
|
30
|
+
}
|
|
31
|
+
.ModalDialog.ModalDialog-lg {
|
|
32
|
+
width: 640px;
|
|
33
|
+
}</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Modal } from './Modal.svelte';
|
|
2
|
+
export { default as ModalDialog } from './ModalDialog.svelte';
|
|
3
|
+
export { default as DialogTitle } from './DialogTitle.svelte';
|
|
4
|
+
export { default as DialogHeader } from './DialogHeader.svelte';
|
|
5
|
+
export { default as DialogDescription } from './DialogDescription.svelte';
|
|
6
|
+
export { default as DialogContent } from './DialogContent.svelte';
|
|
7
|
+
export { default as DialogFooter } from './DialogFooter.svelte';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default as Modal } from './Modal.svelte';
|
|
2
|
+
export { default as ModalDialog } from './ModalDialog.svelte';
|
|
3
|
+
export { default as DialogTitle } from './DialogTitle.svelte';
|
|
4
|
+
export { default as DialogHeader } from './DialogHeader.svelte';
|
|
5
|
+
export { default as DialogDescription } from './DialogDescription.svelte';
|
|
6
|
+
export { default as DialogContent } from './DialogContent.svelte';
|
|
7
|
+
export { default as DialogFooter } from './DialogFooter.svelte';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, type Snippet } from 'svelte';
|
|
3
|
+
let ref: Node;
|
|
4
|
+
let portal: Element | null = null;
|
|
5
|
+
|
|
6
|
+
interface IPortalProps {
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { children } : IPortalProps = $props();
|
|
11
|
+
|
|
12
|
+
onMount(() => {
|
|
13
|
+
portal = document.createElement('div');
|
|
14
|
+
portal.className = 'portal';
|
|
15
|
+
document.body.appendChild(portal);
|
|
16
|
+
portal.appendChild(ref);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
onDestroy(() => {
|
|
20
|
+
if (portal) {
|
|
21
|
+
document.body.removeChild(portal);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
<div class="portal-clone">
|
|
28
|
+
<div bind:this={ref}>
|
|
29
|
+
{@render children?.()}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.portal-clone {
|
|
35
|
+
display: none;
|
|
36
|
+
}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Portal from './Portal.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Portal from './Portal.svelte';
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Select
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
|
|
7
|
+
import type {ISelectGroupData, ISelectItem, ISelectProps} from './Select.types';
|
|
8
|
+
import { ArrowDownSLineArrows } from 'svelte-remix';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CommandRoot,
|
|
12
|
+
CommandList,
|
|
13
|
+
CommandGroup,
|
|
14
|
+
CommandItem,
|
|
15
|
+
CommandInput,
|
|
16
|
+
Menu,
|
|
17
|
+
TextInputBlock,
|
|
18
|
+
type TextInputInstance,
|
|
19
|
+
IconLoading
|
|
20
|
+
} from '../..';
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
options,
|
|
24
|
+
value = $bindable(),
|
|
25
|
+
placeholder,
|
|
26
|
+
disabled,
|
|
27
|
+
fullWidthMenu,
|
|
28
|
+
minWidthMenu,
|
|
29
|
+
menuGap,
|
|
30
|
+
menuMaxHeight,
|
|
31
|
+
searchable=true
|
|
32
|
+
}: ISelectProps = $props();
|
|
33
|
+
|
|
34
|
+
const menuId = `select-menu-${crypto.randomUUID()}`;
|
|
35
|
+
|
|
36
|
+
let currentTitle = $derived<string>(value?.title || '');
|
|
37
|
+
// Нужен для того, чтоб позиционировать Menu относительно parent элемента
|
|
38
|
+
let menuParentElement = $state<HTMLDivElement | null>(null);
|
|
39
|
+
let isMenuVisible = $state<boolean>(false);
|
|
40
|
+
|
|
41
|
+
let textInputBlock: TextInputInstance;
|
|
42
|
+
let flatSelect = $state<boolean>(false);
|
|
43
|
+
|
|
44
|
+
// С помощью этой функции я различаю разные типы options
|
|
45
|
+
const isGroupArray = (options: ISelectItem[] | ISelectGroupData[]) =>
|
|
46
|
+
options.length > 0 && "heading" in options[0];
|
|
47
|
+
|
|
48
|
+
// Определяем тип options - Promise или значение
|
|
49
|
+
let innerGroupOptions = $state<ISelectGroupData[] | null>(null);
|
|
50
|
+
let innerOptions = $state<ISelectItem[] | null>(null);
|
|
51
|
+
let isLoadingOptions = $state(false);
|
|
52
|
+
let loadError = $state<unknown>(null);
|
|
53
|
+
|
|
54
|
+
const normalizeOptions = (opt: ISelectGroupData[] | ISelectItem[]) => {
|
|
55
|
+
if (isGroupArray(opt)) {
|
|
56
|
+
innerGroupOptions = opt as ISelectGroupData[];
|
|
57
|
+
innerOptions = null;
|
|
58
|
+
flatSelect = false;
|
|
59
|
+
} else {
|
|
60
|
+
innerOptions = opt as ISelectItem[];
|
|
61
|
+
innerGroupOptions = null;
|
|
62
|
+
flatSelect = true;
|
|
63
|
+
}
|
|
64
|
+
isLoadingOptions = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let requestId = 0;
|
|
68
|
+
$effect(() => {
|
|
69
|
+
const currentId = ++requestId;
|
|
70
|
+
isLoadingOptions = true;
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const resolved = await options as ISelectItem[] | ISelectGroupData[];
|
|
76
|
+
// если за это время options сменился — выходим
|
|
77
|
+
if (currentId !== requestId) return;
|
|
78
|
+
normalizeOptions(resolved);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (currentId !== requestId) return;
|
|
81
|
+
loadError = err;
|
|
82
|
+
innerOptions = null;
|
|
83
|
+
innerGroupOptions = null;
|
|
84
|
+
} finally {
|
|
85
|
+
if (currentId === requestId) isLoadingOptions = false;
|
|
86
|
+
}
|
|
87
|
+
})();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
const showMenu = () => (isMenuVisible = true);
|
|
92
|
+
const hideMenu = () => (isMenuVisible = false);
|
|
93
|
+
|
|
94
|
+
const focusHandler = () => {
|
|
95
|
+
if (!isMenuVisible) {
|
|
96
|
+
showMenu();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const updateValue = (newValue: ISelectItem) => {
|
|
101
|
+
value = newValue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleItemClick = (item: ISelectItem) => {
|
|
105
|
+
updateValue({
|
|
106
|
+
title: item.title,
|
|
107
|
+
value: item.value
|
|
108
|
+
});
|
|
109
|
+
textInputBlock.focus();
|
|
110
|
+
hideMenu();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const handleCommandInputKeyDown = (e: KeyboardEvent) => {
|
|
114
|
+
if (disabled || isLoadingOptions) return;
|
|
115
|
+
|
|
116
|
+
if (e.key === "Tab") {
|
|
117
|
+
textInputBlock.focus();
|
|
118
|
+
hideMenu();
|
|
119
|
+
} else if (e.key === 'Escape') {
|
|
120
|
+
if (isMenuVisible) {
|
|
121
|
+
textInputBlock.focus();
|
|
122
|
+
hideMenu();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const handleControlKeyDown = (e: KeyboardEvent) => {
|
|
128
|
+
switch (e.key) {
|
|
129
|
+
case 'Enter':
|
|
130
|
+
case ' ':
|
|
131
|
+
case 'ArrowDown':
|
|
132
|
+
case 'ArrowUp': {
|
|
133
|
+
if (!isMenuVisible) {
|
|
134
|
+
showMenu();
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const handleControlClick = () => {
|
|
143
|
+
if (!isMenuVisible) {
|
|
144
|
+
showMenu();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const handleClear = () => {
|
|
149
|
+
hideMenu();
|
|
150
|
+
value = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
</script>
|
|
154
|
+
|
|
155
|
+
<div class={`dropdown-toggler ${isMenuVisible ? "dropdown-toggler-hover" : ""}`}
|
|
156
|
+
bind:this={menuParentElement}
|
|
157
|
+
>
|
|
158
|
+
|
|
159
|
+
<TextInputBlock
|
|
160
|
+
disabled={disabled || isLoadingOptions}
|
|
161
|
+
readonly
|
|
162
|
+
onFocus={focusHandler}
|
|
163
|
+
onKeyDown={handleControlKeyDown}
|
|
164
|
+
onClear={handleClear}
|
|
165
|
+
pseudoFocus={isMenuVisible}
|
|
166
|
+
onClick={handleControlClick}
|
|
167
|
+
variant="contained"
|
|
168
|
+
size="lg"
|
|
169
|
+
placeholder={placeholder}
|
|
170
|
+
bind:value={currentTitle}
|
|
171
|
+
bind:this={textInputBlock}
|
|
172
|
+
|
|
173
|
+
ariaHasPopup="listbox"
|
|
174
|
+
ariaExpanded={isMenuVisible}
|
|
175
|
+
ariaControls={menuId}
|
|
176
|
+
>
|
|
177
|
+
{#snippet suffix()}
|
|
178
|
+
<div class="suffix-wrapper">
|
|
179
|
+
{#if isLoadingOptions}
|
|
180
|
+
<IconLoading/>
|
|
181
|
+
{/if}
|
|
182
|
+
<ArrowDownSLineArrows size="1em" onclick={(e: MouseEvent) => { e.stopPropagation(); handleControlClick(); }} />
|
|
183
|
+
</div>
|
|
184
|
+
{/snippet}
|
|
185
|
+
</TextInputBlock>
|
|
186
|
+
|
|
187
|
+
<Menu
|
|
188
|
+
id={menuId}
|
|
189
|
+
parentElement={menuParentElement}
|
|
190
|
+
appearanceOnHover={false}
|
|
191
|
+
isVisible={isMenuVisible}
|
|
192
|
+
hideMenu={hideMenu}
|
|
193
|
+
minWidth={minWidthMenu}
|
|
194
|
+
fullWidth={fullWidthMenu}
|
|
195
|
+
{menuGap}
|
|
196
|
+
maxHeight={menuMaxHeight}
|
|
197
|
+
>
|
|
198
|
+
<div class="menu" role="listbox">
|
|
199
|
+
<CommandRoot maxHeight={menuMaxHeight} >
|
|
200
|
+
<CommandInput onKeyDown={handleCommandInputKeyDown} autoFocus={true} visible={searchable} placeholder={placeholder} ></CommandInput>
|
|
201
|
+
<CommandList>
|
|
202
|
+
{#if isLoadingOptions}
|
|
203
|
+
<div class="px-3 py-2">Loading...</div>
|
|
204
|
+
{:else if loadError}
|
|
205
|
+
<div class="px-3 py-2 text-danger">Load options error</div>
|
|
206
|
+
{:else}
|
|
207
|
+
|
|
208
|
+
{#if flatSelect}
|
|
209
|
+
{#if innerOptions}
|
|
210
|
+
<CommandGroup>
|
|
211
|
+
{#each innerOptions as option (option.value)}
|
|
212
|
+
<CommandItem onClick={ () => handleItemClick(option) }>{option.title}</CommandItem>
|
|
213
|
+
{/each}
|
|
214
|
+
</CommandGroup>
|
|
215
|
+
{/if}
|
|
216
|
+
{:else}
|
|
217
|
+
{#if innerGroupOptions}
|
|
218
|
+
{#each innerGroupOptions as option (option.heading)}
|
|
219
|
+
<CommandGroup heading={option.heading}>
|
|
220
|
+
{#each option.items as item (item.value)}
|
|
221
|
+
<CommandItem onClick={ () => handleItemClick(item) }>{item.title}</CommandItem>
|
|
222
|
+
{/each}
|
|
223
|
+
</CommandGroup>
|
|
224
|
+
{/each}
|
|
225
|
+
{/if}
|
|
226
|
+
{/if}
|
|
227
|
+
|
|
228
|
+
{/if}
|
|
229
|
+
</CommandList>
|
|
230
|
+
|
|
231
|
+
</CommandRoot>
|
|
232
|
+
</div>
|
|
233
|
+
</Menu>
|
|
234
|
+
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
<style>.dropdown-toggler {
|
|
239
|
+
color: var(--text-base-placeholder);
|
|
240
|
+
transition: color 0.3s ease-in-out;
|
|
241
|
+
padding: 0;
|
|
242
|
+
margin: 0 0 1rem;
|
|
243
|
+
display: flex;
|
|
244
|
+
width: 100%;
|
|
245
|
+
position: relative;
|
|
246
|
+
}
|
|
247
|
+
.dropdown-toggler:hover {
|
|
248
|
+
color: var(--text-base-main);
|
|
249
|
+
}
|
|
250
|
+
.dropdown-toggler.dropdown-toggler-hover {
|
|
251
|
+
color: var(--text-base-main);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.suffix-wrapper {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
gap: 0.5rem;
|
|
258
|
+
}</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ISelectItem {
|
|
2
|
+
title: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ISelectGroupData {
|
|
6
|
+
heading: string;
|
|
7
|
+
items: ISelectItem[];
|
|
8
|
+
}
|
|
9
|
+
export interface ISelectProps {
|
|
10
|
+
searchable?: boolean;
|
|
11
|
+
options: ISelectGroupData[] | ISelectItem[] | Promise<(ISelectGroupData[] | ISelectItem[])>;
|
|
12
|
+
value: ISelectItem | null;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
fullWidthMenu?: boolean;
|
|
16
|
+
minWidthMenu?: number;
|
|
17
|
+
menuGap?: number;
|
|
18
|
+
menuMaxHeight?: number;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -55,18 +55,20 @@
|
|
|
55
55
|
/>
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
<div class="suffix">
|
|
59
|
+
{#if onClear && !disabled && value !== ''}
|
|
60
|
+
<!-- I deliberately excluded this button from tabindex -->
|
|
61
|
+
<button tabindex="-1" type="button" class="clear-value" style={`right: ${suffixWidth + 12}px;`} onmouseup={handleClear}>
|
|
62
|
+
<CloseCircleFillSystem size="1em" />
|
|
63
|
+
</button>
|
|
64
|
+
{/if}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
{#if suffix}
|
|
67
|
+
<div class="suffix-el" bind:this={suffixEl}>
|
|
68
|
+
{@render suffix()}
|
|
69
|
+
</div>
|
|
70
|
+
{/if}
|
|
71
|
+
</div>
|
|
70
72
|
</div>
|
|
71
73
|
|
|
72
74
|
<style>.TextInputBlock {
|
|
@@ -82,6 +84,7 @@
|
|
|
82
84
|
height: 100%;
|
|
83
85
|
display: flex;
|
|
84
86
|
align-items: center;
|
|
87
|
+
gap: 0.25rem;
|
|
85
88
|
}
|
|
86
89
|
.TextInputBlock .prefix {
|
|
87
90
|
left: 0;
|
|
@@ -92,18 +95,19 @@
|
|
|
92
95
|
padding-right: 0.75em;
|
|
93
96
|
}
|
|
94
97
|
.TextInputBlock .clear-value {
|
|
95
|
-
position: absolute;
|
|
96
|
-
z-index: 1;
|
|
97
|
-
top: 4px;
|
|
98
|
-
bottom: 4px;
|
|
99
98
|
height: calc(100% - 8px);
|
|
100
99
|
cursor: pointer;
|
|
101
100
|
border: 1px solid transparent;
|
|
102
101
|
border-radius: 8px;
|
|
103
102
|
padding: 0 4px;
|
|
103
|
+
background: transparent;
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
}
|
|
107
|
+
.TextInputBlock .clear-value:hover {
|
|
108
|
+
background: var(--bg-base-100);
|
|
104
109
|
}
|
|
105
110
|
.TextInputBlock .clear-value:focus {
|
|
106
111
|
outline: none;
|
|
107
|
-
background
|
|
108
|
-
border-color: #D3D5DC;
|
|
112
|
+
background: var(--bg-base-200);
|
|
109
113
|
}</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const themeDefault = {
|
|
2
|
+
'--font-base': `"Onest", sans-serif`,
|
|
2
3
|
'--panel-base': '#f3f5f6',
|
|
3
4
|
'--panel-primary': '#FFF9E5',
|
|
4
5
|
'--panel-success': '#DDF9E6',
|
|
@@ -13,6 +14,7 @@ export const themeDefault = {
|
|
|
13
14
|
'--line-danger': '#FBC4BF',
|
|
14
15
|
'--line-secondary': '#BEB6F9',
|
|
15
16
|
'--bg-base': '#fff',
|
|
17
|
+
'--bg-backdrop': 'rgba(0,0,0,0.5)',
|
|
16
18
|
'--bg-base-100': '#EDEEF0',
|
|
17
19
|
'--bg-base-200': '#E2E4E8',
|
|
18
20
|
'--bg-base-300': '#D3D5DC',
|
|
@@ -92,4 +94,5 @@ export const themeDefault = {
|
|
|
92
94
|
'--line-height-md': '20px',
|
|
93
95
|
'--font-size-lg': '16px',
|
|
94
96
|
'--line-height-lg': '24px',
|
|
97
|
+
'--font-size-h2': '20px',
|
|
95
98
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import { browser } from '$app/environment';
|
|
3
3
|
import type { Snippet } from 'svelte';
|
|
4
4
|
import type { ITheme } from './ThemeProvider.types'
|
|
5
5
|
|
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
.join(';');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
+
$effect(() => {
|
|
15
|
+
if (!browser || !theme) return;
|
|
16
|
+
|
|
17
|
+
for (const [k, v] of Object.entries(theme)) {
|
|
18
|
+
// ключи должны быть вида `--color-bg`
|
|
19
|
+
document.documentElement.style.setProperty(k, String(v));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
14
22
|
</script>
|
|
15
23
|
|
|
16
|
-
<div style={cssVars} class="ThemeProvider">
|
|
24
|
+
<!-- <div style={cssVars} class="ThemeProvider"> -->
|
|
17
25
|
{@render children()}
|
|
18
|
-
</div>
|
|
26
|
+
<!-- </div> -->
|
|
19
27
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type ThemeVars = Record<string, string | number>;
|
|
2
2
|
export interface ITheme {
|
|
3
|
+
'--font-base': string;
|
|
3
4
|
'--panel-base': string;
|
|
4
5
|
'--panel-primary': string;
|
|
5
6
|
'--panel-success': string;
|
|
@@ -14,6 +15,7 @@ export interface ITheme {
|
|
|
14
15
|
'--line-danger': string;
|
|
15
16
|
'--line-secondary': string;
|
|
16
17
|
'--bg-base': string;
|
|
18
|
+
'--bg-backdrop': string;
|
|
17
19
|
'--bg-base-100': string;
|
|
18
20
|
'--bg-base-200': string;
|
|
19
21
|
'--bg-base-300': string;
|
|
@@ -93,4 +95,5 @@ export interface ITheme {
|
|
|
93
95
|
'--line-height-md': string;
|
|
94
96
|
'--font-size-lg': string;
|
|
95
97
|
'--line-height-lg': string;
|
|
98
|
+
'--font-size-h2': string;
|
|
96
99
|
}
|
package/dist/components/index.js
CHANGED
package/dist/styles/index.scss
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Please sync this with ThemeProvider
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--font-base: "Geist", sans-serif;
|
|
5
|
+
|
|
6
|
+
--panel-base: #f3f5f6;
|
|
7
|
+
--panel-primary: #FFF9E5;
|
|
8
|
+
--panel-success: #DDF9E6;
|
|
9
|
+
--panel-danger: #FFEDEB;
|
|
10
|
+
--panel-secondary: #F6F5FF;
|
|
11
|
+
|
|
12
|
+
--line-base: #D3D5DC;
|
|
13
|
+
--line-control-100: #ABABB0;
|
|
14
|
+
--line-base-emp: #151617;
|
|
15
|
+
--line-primary: #FFE27B;
|
|
16
|
+
--line-primary-emp: #F0C013;
|
|
17
|
+
--line-success: #B6F7CB;
|
|
18
|
+
--line-danger: #FBC4BF;
|
|
19
|
+
--line-secondary: #BEB6F9;
|
|
20
|
+
|
|
21
|
+
--bg-base: #fff;
|
|
22
|
+
--bg-backdrop: rgba(0,0,0,0.5);
|
|
23
|
+
|
|
24
|
+
--bg-base-100: #EDEEF0;
|
|
25
|
+
--bg-base-200: #E2E4E8;
|
|
26
|
+
--bg-base-300: #D3D5DC;
|
|
27
|
+
--bg-base-emp-100: #28282B;
|
|
28
|
+
--bg-base-emp-200: #1E1F21;
|
|
29
|
+
--bg-base-emp-300: #151617;
|
|
30
|
+
|
|
31
|
+
--bg-primary-100: #FFF6D6;
|
|
32
|
+
--bg-primary-200: #FCF0C5;
|
|
33
|
+
--bg-primary-300: #FAEBB4;
|
|
34
|
+
--bg-primary-emp-100: #FFCC14;
|
|
35
|
+
--bg-primary-emp-200: #F7C614;
|
|
36
|
+
--bg-primary-emp-300: #F0C013;
|
|
37
|
+
|
|
38
|
+
--bg-success-100: #C3F6D4;
|
|
39
|
+
--bg-success-200: #B1F0C6;
|
|
40
|
+
--bg-success-300: #9FEBB9;
|
|
41
|
+
--bg-success-emp-100: #1BB44C;
|
|
42
|
+
--bg-success-emp-200: #12AA43;
|
|
43
|
+
--bg-success-emp-300: #0DA13C;
|
|
44
|
+
|
|
45
|
+
--bg-danger-100: #FCDFDC;
|
|
46
|
+
--bg-danger-200: #FAD4CF;
|
|
47
|
+
--bg-danger-300: #F7C7C1;
|
|
48
|
+
--bg-danger-emp-100: #F35749;
|
|
49
|
+
--bg-danger-emp-200: #ED4637;
|
|
50
|
+
--bg-danger-emp-300: #EB3A2A;
|
|
51
|
+
|
|
52
|
+
--bg-secondary-100: #E8E6FC;
|
|
53
|
+
--bg-secondary-200: #DCD9F9;
|
|
54
|
+
--bg-secondary-300: #D1CCF9;
|
|
55
|
+
--bg-secondary-emp-100: #5A49F3;
|
|
56
|
+
--bg-secondary-emp-200: #4F3DF2;
|
|
57
|
+
--bg-secondary-emp-300: #4431F5;
|
|
58
|
+
|
|
59
|
+
--text-base-main: #26272B;
|
|
60
|
+
--text-base-inv: #fff;
|
|
61
|
+
--text-base-muted: #515256;
|
|
62
|
+
--text-base-inv-muted: #C1C3CA;
|
|
63
|
+
--text-base-placeholder: #9C9EA3;
|
|
64
|
+
--text-base-link: #6846E1;
|
|
65
|
+
--text-secondary-main: #2F19FB;
|
|
66
|
+
--text-primary-main: #C29B09;
|
|
67
|
+
--text-success-main: #077129;
|
|
68
|
+
--text-danger-main: #C42A1A;
|
|
69
|
+
|
|
70
|
+
--icon-base-main: #26272B;
|
|
71
|
+
--icon-base-inv: #fff;
|
|
72
|
+
--icon-base-muted: #515256;
|
|
73
|
+
--icon-base-inv-muted: #C1C3CA;
|
|
74
|
+
--icon-base-placeholder: #9C9EA3;
|
|
75
|
+
--icon-base-link: #6846E1;
|
|
76
|
+
--icon-secondary-main: #2F19FB;
|
|
77
|
+
--icon-primary-main: #C29B09;
|
|
78
|
+
--icon-success-main: #077129;
|
|
79
|
+
--icon-danger-main: #C42A1A;
|
|
80
|
+
|
|
81
|
+
--border-radius-button-sm: 5px;
|
|
82
|
+
--border-radius-button: 8px;
|
|
83
|
+
--border-radius-panel: 12px;
|
|
84
|
+
--border-radius-window: 20px;
|
|
85
|
+
|
|
86
|
+
--stroke-base: 1px;
|
|
87
|
+
--stroke-button: var(--stroke-base);
|
|
88
|
+
|
|
89
|
+
--spacing-xs-1: 4;
|
|
90
|
+
--spacing-xs-1-5: 6;
|
|
91
|
+
--spacing-sm-2: 8;
|
|
92
|
+
--spacing-md-3: 12;
|
|
93
|
+
--spacing-lg-5: 20;
|
|
94
|
+
--spacing-xl-10: 40;
|
|
95
|
+
|
|
96
|
+
--zindex-dropdown: 1060;
|
|
97
|
+
--zindex-sticky: 1020;
|
|
98
|
+
--zindex-fixed: 1030;
|
|
99
|
+
--zindex-sheet: 1035;
|
|
100
|
+
--zindex-offcanvas-backdrop: 1040;
|
|
101
|
+
--zindex-offcanvas: 1045;
|
|
102
|
+
--zindex-modal-backdrop: 1050;
|
|
103
|
+
--zindex-modal: 1055;
|
|
104
|
+
--zindex-popover: 1070;
|
|
105
|
+
--zindex-tooltip: 1080;
|
|
106
|
+
--zindex-toast: 1090;
|
|
107
|
+
|
|
108
|
+
--font-size-sm: 12px;
|
|
109
|
+
--line-height-sm: 14px;
|
|
110
|
+
--font-size-md: 14px;
|
|
111
|
+
--line-height-md: 20px;
|
|
112
|
+
--font-size-lg: 16px;
|
|
113
|
+
--line-height-lg: 24px;
|
|
114
|
+
|
|
115
|
+
--font-size-h2: 20px;
|
|
116
|
+
}
|
|
117
|
+
|