lutra 0.1.0 → 0.1.4
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/dist/components/Avatar.svelte +105 -0
- package/dist/components/Avatar.svelte.d.ts +14 -0
- package/dist/components/Close.svelte +76 -0
- package/dist/components/Close.svelte.d.ts +7 -0
- package/dist/components/ContextTip.svelte +41 -0
- package/dist/components/ContextTip.svelte.d.ts +7 -0
- package/dist/components/Icon.svelte +62 -0
- package/dist/components/Icon.svelte.d.ts +8 -0
- package/dist/components/IconButton.svelte +120 -0
- package/dist/components/IconButton.svelte.d.ts +16 -0
- package/dist/components/Image.svelte +172 -0
- package/dist/components/Image.svelte.d.ts +56 -0
- package/dist/components/Indicator.svelte +387 -0
- package/dist/components/Indicator.svelte.d.ts +12 -0
- package/dist/components/Inset.svelte +23 -0
- package/dist/components/Inset.svelte.d.ts +7 -0
- package/dist/components/Layout.svelte +2 -1
- package/dist/components/MenuDropdown.svelte +195 -0
- package/dist/components/MenuDropdown.svelte.d.ts +16 -0
- package/dist/components/MenuItem.svelte +155 -0
- package/dist/components/MenuItem.svelte.d.ts +11 -0
- package/dist/components/MenuItemContent.svelte +25 -0
- package/dist/components/MenuItemContent.svelte.d.ts +10 -0
- package/dist/components/MenuTypes.d.ts +72 -0
- package/dist/components/MenuTypes.js +1 -0
- package/dist/components/Modal.svelte +149 -0
- package/dist/components/Modal.svelte.d.ts +16 -0
- package/dist/components/Notification.svelte +115 -0
- package/dist/components/Notification.svelte.d.ts +12 -0
- package/dist/components/Overlay.svelte +31 -0
- package/dist/components/Overlay.svelte.d.ts +14 -0
- package/dist/components/OverlayContainer.svelte +31 -0
- package/dist/components/OverlayContainer.svelte.d.ts +18 -0
- package/dist/components/OverlayLayer.svelte +168 -0
- package/dist/components/OverlayLayer.svelte.d.ts +8 -0
- package/dist/components/TabbedContent.svelte +74 -0
- package/dist/components/TabbedContent.svelte.d.ts +11 -0
- package/dist/components/TabbedContentItem.svelte +33 -0
- package/dist/components/TabbedContentItem.svelte.d.ts +10 -0
- package/dist/components/Table.svelte +41 -0
- package/dist/components/Table.svelte.d.ts +13 -0
- package/dist/components/Tabs.svelte +216 -0
- package/dist/components/Tabs.svelte.d.ts +20 -0
- package/dist/components/Tag.svelte +120 -0
- package/dist/components/Tag.svelte.d.ts +21 -0
- package/dist/components/Theme.svelte +32 -14
- package/dist/components/Tooltip.svelte +8 -8
- package/dist/components/UIContent.svelte +19 -0
- package/dist/components/UIContent.svelte.d.ts +7 -0
- package/dist/components/index.d.ts +28 -0
- package/dist/components/index.js +29 -0
- package/dist/components/notifications.svelte.d.ts +21 -0
- package/dist/components/notifications.svelte.js +30 -0
- package/dist/components/overlays.svelte.d.ts +36 -0
- package/dist/components/overlays.svelte.js +44 -0
- package/dist/css/1-props.css +389 -724
- package/dist/css/2-base.css +257 -123
- package/dist/css/3-typo.css +75 -34
- package/dist/css/4-layout.css +364 -1
- package/dist/css/5-media.css +106 -11
- package/dist/css/lutra.css +2 -1
- package/dist/css/themes/DefaultTheme.css +209 -0
- package/dist/form/Button.svelte +58 -0
- package/dist/form/Button.svelte.d.ts +15 -0
- package/dist/form/Datepicker.svelte +311 -0
- package/dist/form/Datepicker.svelte.d.ts +9 -0
- package/dist/form/FieldContent.svelte +178 -0
- package/dist/form/FieldContent.svelte.d.ts +21 -0
- package/dist/form/FieldError.svelte +24 -0
- package/dist/form/FieldError.svelte.d.ts +7 -0
- package/dist/form/Fieldset.svelte +103 -0
- package/dist/form/Fieldset.svelte.d.ts +20 -0
- package/dist/form/Form.svelte +220 -0
- package/dist/form/Form.svelte.d.ts +38 -0
- package/dist/form/FormActions.svelte +80 -0
- package/dist/form/FormActions.svelte.d.ts +9 -0
- package/dist/form/FormSection.svelte +96 -0
- package/dist/form/FormSection.svelte.d.ts +9 -0
- package/dist/form/ImageUpload.svelte +299 -0
- package/dist/form/ImageUpload.svelte.d.ts +20 -0
- package/dist/form/Input.svelte +444 -0
- package/dist/form/Input.svelte.d.ts +108 -0
- package/dist/form/InputLength.svelte +42 -0
- package/dist/form/InputLength.svelte.d.ts +9 -0
- package/dist/form/Label.svelte +88 -0
- package/dist/form/Label.svelte.d.ts +16 -0
- package/dist/form/LogoUpload.svelte +115 -0
- package/dist/form/LogoUpload.svelte.d.ts +18 -0
- package/dist/form/Select.svelte +186 -0
- package/dist/form/Select.svelte.d.ts +59 -0
- package/dist/form/Textarea.svelte +265 -0
- package/dist/form/Textarea.svelte.d.ts +95 -0
- package/dist/form/Toggle.svelte +4 -0
- package/dist/form/Toggle.svelte.d.ts +18 -0
- package/dist/form/client.svelte.d.ts +45 -0
- package/dist/form/client.svelte.js +102 -0
- package/dist/form/form.d.ts +55 -0
- package/dist/form/form.js +345 -0
- package/dist/form/index.d.ts +17 -0
- package/dist/form/index.js +17 -0
- package/dist/form/types.d.ts +55 -0
- package/dist/form/types.js +1 -0
- package/dist/icons/IconAlert.svelte +3 -0
- package/dist/icons/IconAlert.svelte.d.ts +26 -0
- package/dist/icons/IconCopy.svelte +3 -0
- package/dist/icons/IconCopy.svelte.d.ts +26 -0
- package/dist/icons/IconDone.svelte +3 -0
- package/dist/icons/IconDone.svelte.d.ts +26 -0
- package/dist/icons/IconError.svelte +3 -0
- package/dist/icons/IconError.svelte.d.ts +26 -0
- package/dist/icons/IconHelp.svelte +3 -0
- package/dist/icons/IconHelp.svelte.d.ts +26 -0
- package/dist/icons/IconHide.svelte +3 -0
- package/dist/icons/IconHide.svelte.d.ts +26 -0
- package/dist/icons/IconInfo.svelte +3 -0
- package/dist/icons/IconInfo.svelte.d.ts +26 -0
- package/dist/icons/IconLink.svelte +3 -0
- package/dist/icons/IconLink.svelte.d.ts +26 -0
- package/dist/icons/IconMenuBurger.svelte +3 -0
- package/dist/icons/IconMenuBurger.svelte.d.ts +26 -0
- package/dist/icons/IconMenuDots.svelte +3 -0
- package/dist/icons/IconMenuDots.svelte.d.ts +26 -0
- package/dist/icons/IconSearch.svelte +3 -0
- package/dist/icons/IconSearch.svelte.d.ts +26 -0
- package/dist/icons/IconShow.svelte +3 -0
- package/dist/icons/IconShow.svelte.d.ts +26 -0
- package/dist/icons/IconSuccess.svelte +3 -0
- package/dist/icons/IconSuccess.svelte.d.ts +26 -0
- package/dist/icons/IconWarning.svelte +3 -0
- package/dist/icons/IconWarning.svelte.d.ts +26 -0
- package/dist/icons/index.d.ts +14 -0
- package/dist/icons/index.js +14 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.js +3 -5
- package/dist/util/StringOrComponent.svelte +20 -0
- package/dist/util/StringOrComponent.svelte.d.ts +8 -0
- package/dist/util/StringOrSnippet.svelte +16 -0
- package/dist/util/StringOrSnippet.svelte.d.ts +8 -0
- package/dist/util/attr.d.ts +5 -0
- package/dist/util/attr.js +21 -0
- package/dist/util/color.d.ts +51 -0
- package/dist/util/color.js +97 -0
- package/dist/util/dom.d.ts +15 -0
- package/dist/util/dom.js +73 -0
- package/dist/util/keyboard.svelte.d.ts +22 -0
- package/dist/util/keyboard.svelte.js +161 -0
- package/dist/util/locale.d.ts +1 -0
- package/dist/util/locale.js +47 -0
- package/dist/util/settings.d.ts +4 -0
- package/dist/util/settings.js +1 -0
- package/package.json +20 -11
- package/dist/css/0-layers.css +0 -1
@@ -0,0 +1,168 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { type OverlayItem, type OverlayPosition } from "./overlays.svelte.js";
|
3
|
+
import { slidefade } from "../util/transitions.js";
|
4
|
+
import { BROWSER } from "esm-env";
|
5
|
+
import { untrack } from "svelte";
|
6
|
+
|
7
|
+
let {
|
8
|
+
position,
|
9
|
+
items,
|
10
|
+
}: {
|
11
|
+
position?: OverlayPosition;
|
12
|
+
items: OverlayItem[]
|
13
|
+
} = $props();
|
14
|
+
|
15
|
+
const fudge = 8;
|
16
|
+
|
17
|
+
let contentEls: Record<string, HTMLElement> = $state({});
|
18
|
+
let scrollX = $state(BROWSER ? window.scrollX : 0);
|
19
|
+
let scrollY = $state(BROWSER ? window.scrollY : 0);
|
20
|
+
let innerWidth = $state(BROWSER ? window.innerWidth : 0);
|
21
|
+
let innerHeight = $state(BROWSER ? window.innerHeight : 0);
|
22
|
+
|
23
|
+
let positions = $derived.by(() => {
|
24
|
+
|
25
|
+
innerWidth;
|
26
|
+
innerHeight;
|
27
|
+
scrollX;
|
28
|
+
scrollY;
|
29
|
+
contentEls;
|
30
|
+
|
31
|
+
return items.map((item) => {
|
32
|
+
const contentEl = contentEls[item.id];
|
33
|
+
if(!item.anchor || !contentEl) return { left: 0, top: 0, index: 1 };
|
34
|
+
const triggerPos = item.anchor.getBoundingClientRect();
|
35
|
+
|
36
|
+
const height = contentEl.clientHeight;
|
37
|
+
const width = contentEl.clientWidth;
|
38
|
+
|
39
|
+
const isOffBottom = triggerPos.bottom + height > window.innerHeight - fudge;
|
40
|
+
const isOffRight = triggerPos.left + width > window.innerWidth - fudge;
|
41
|
+
|
42
|
+
let left = triggerPos.left;
|
43
|
+
let top = triggerPos.top + triggerPos.height + fudge;
|
44
|
+
|
45
|
+
if(isOffRight) left = left - width + triggerPos.width;
|
46
|
+
if(isOffBottom) top = top - height - triggerPos.height - (fudge * 2);
|
47
|
+
|
48
|
+
return {
|
49
|
+
left,
|
50
|
+
top,
|
51
|
+
index: 1,
|
52
|
+
};
|
53
|
+
});
|
54
|
+
});
|
55
|
+
|
56
|
+
let originCache: Record<string, string> = $state({});
|
57
|
+
|
58
|
+
/**
|
59
|
+
* The origin of the menu content based on the trigger position.
|
60
|
+
*/
|
61
|
+
let origins = $derived.by(() => {
|
62
|
+
return items.map((item, index) => {
|
63
|
+
if (!item.anchor || item.position !== "anchor") {
|
64
|
+
return item.position || "center";
|
65
|
+
}
|
66
|
+
|
67
|
+
const triggerPos = item.anchor!.getBoundingClientRect();
|
68
|
+
const contentEl = document.getElementById(item.id);
|
69
|
+
const contentRect = contentEl?.getBoundingClientRect();
|
70
|
+
|
71
|
+
const height = contentRect!.height;
|
72
|
+
const width = contentRect!.width;
|
73
|
+
|
74
|
+
const isOffBottom = triggerPos.bottom + height > innerHeight - fudge;
|
75
|
+
const isOffRight = triggerPos.left + width > innerWidth - fudge;
|
76
|
+
|
77
|
+
let text = "top left";
|
78
|
+
|
79
|
+
if (isOffRight) text = text.replace("left", "right");
|
80
|
+
if (isOffBottom) text = text.replace("top", "bottom");
|
81
|
+
|
82
|
+
return text;
|
83
|
+
});
|
84
|
+
});
|
85
|
+
|
86
|
+
$effect(() => {
|
87
|
+
origins;
|
88
|
+
origins.forEach((origin, index) => {
|
89
|
+
originCache[untrack(() => items[index].id)] = origin;
|
90
|
+
});
|
91
|
+
});
|
92
|
+
|
93
|
+
function introstart(item: OverlayItem) {
|
94
|
+
document.getElementById(item.id)!.style.pointerEvents = "none";
|
95
|
+
}
|
96
|
+
|
97
|
+
function introend(item: OverlayItem) {
|
98
|
+
document.getElementById(item.id)!.style.pointerEvents = "auto";
|
99
|
+
}
|
100
|
+
</script>
|
101
|
+
|
102
|
+
<svelte:window bind:scrollX bind:scrollY bind:innerWidth bind:innerHeight />
|
103
|
+
|
104
|
+
<div class="Layer {position}">
|
105
|
+
{#each items as item, index (item.id)}
|
106
|
+
<div
|
107
|
+
id={item.id}
|
108
|
+
bind:this={contentEls[item.id]}
|
109
|
+
class="LayerItem"
|
110
|
+
class:anchor={item.anchor ? true : false}
|
111
|
+
onintrostart={() => introstart(item)}
|
112
|
+
onintroend={() => introend(item)}
|
113
|
+
transition:slidefade|global={{ duration: 150, origin: originCache[item.id] || origins[index], noMargin: !!!item.anchor }}
|
114
|
+
style="--index: {index}; --z: {item.z}; --left: {positions[index].left}px; --top: {positions[index].top}px;"
|
115
|
+
>
|
116
|
+
{#if item.component}
|
117
|
+
<item.component {...item.props} />
|
118
|
+
{:else if item.snippet}
|
119
|
+
{@render item.snippet()}
|
120
|
+
{/if}
|
121
|
+
</div>
|
122
|
+
{/each}
|
123
|
+
</div>
|
124
|
+
|
125
|
+
<style>
|
126
|
+
.Layer {
|
127
|
+
pointer-events: auto;
|
128
|
+
position: absolute;
|
129
|
+
display: flex;
|
130
|
+
flex-direction: column-reverse;
|
131
|
+
gap: 0.75rem;
|
132
|
+
}
|
133
|
+
.Layer.center {
|
134
|
+
left: 50%;
|
135
|
+
right: auto;
|
136
|
+
transform: translateX(-50%);
|
137
|
+
}
|
138
|
+
.Layer.top {
|
139
|
+
top: calc(1rem + env(safe-area-inset-top));
|
140
|
+
bottom: unset;
|
141
|
+
}
|
142
|
+
.Layer.bottom {
|
143
|
+
top: unset;
|
144
|
+
bottom: calc(1rem + env(safe-area-inset-bottom));
|
145
|
+
}
|
146
|
+
.Layer.right {
|
147
|
+
left: unset;
|
148
|
+
right: calc(1rem + env(safe-area-inset-right));
|
149
|
+
}
|
150
|
+
.Layer.left {
|
151
|
+
left: calc(1rem + env(safe-area-inset-left));
|
152
|
+
right: unset;
|
153
|
+
}
|
154
|
+
.Layer.center:not(.top):not(.bottom):not(.anchor) {
|
155
|
+
top: 50%;
|
156
|
+
bottom: auto;
|
157
|
+
transform: translate(calc(-50% + env(safe-area-inset-left) + env(safe-area-inset-right)), calc(-50% + env(safe-area-inset-top) + env(safe-area-inset-bottom)));
|
158
|
+
}
|
159
|
+
.LayerItem {
|
160
|
+
position: relative;
|
161
|
+
z-index: calc(100 + var(--z, 1) - var(--index, 0));
|
162
|
+
}
|
163
|
+
.LayerItem.anchor {
|
164
|
+
position: absolute;
|
165
|
+
top: var(--top, 0);
|
166
|
+
left: var(--left, 0);
|
167
|
+
}
|
168
|
+
</style>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import { type OverlayItem, type OverlayPosition } from "./overlays.svelte.js";
|
2
|
+
type $$ComponentProps = {
|
3
|
+
position?: OverlayPosition;
|
4
|
+
items: OverlayItem[];
|
5
|
+
};
|
6
|
+
declare const OverlayLayer: import("svelte").Component<$$ComponentProps, {}, "">;
|
7
|
+
type OverlayLayer = ReturnType<typeof OverlayLayer>;
|
8
|
+
export default OverlayLayer;
|
@@ -0,0 +1,74 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { page } from "$app/stores";
|
3
|
+
import { setContext, type Snippet } from "svelte";
|
4
|
+
import type { TabbedContentItem } from "./MenuTypes.js";
|
5
|
+
import Tabs from "./Tabs.svelte";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* @description
|
9
|
+
* Tabbed content that will display the content of the selected tab.
|
10
|
+
* @cssprop --gap - The gap between the tabs and the content. (Default: 1.5rem)
|
11
|
+
* @example
|
12
|
+
* {#snippet c1()}
|
13
|
+
* <p>foo</p>
|
14
|
+
* {/snippet}
|
15
|
+
* {#snippet c2()}
|
16
|
+
* <p>bar</p>
|
17
|
+
* {/snippet}
|
18
|
+
* <TabbedContent contained rounded items={[
|
19
|
+
* {
|
20
|
+
* label: 'Tab 1',
|
21
|
+
* hash: '#tab1',
|
22
|
+
* content: c1,
|
23
|
+
* },
|
24
|
+
* {
|
25
|
+
* label: 'Tab 2',
|
26
|
+
* hash: '#tab2',
|
27
|
+
* content: c2,
|
28
|
+
* },
|
29
|
+
* ]} />
|
30
|
+
*/
|
31
|
+
let {
|
32
|
+
contained,
|
33
|
+
rounded,
|
34
|
+
children,
|
35
|
+
}: {
|
36
|
+
/** Contain the element in a box. */
|
37
|
+
contained?: boolean;
|
38
|
+
/** Round the corners of the element if contained. */
|
39
|
+
rounded?: boolean;
|
40
|
+
children: Snippet;
|
41
|
+
} = $props();
|
42
|
+
|
43
|
+
function formatHash(hash: string) {
|
44
|
+
hash = hash.toLowerCase().replace(/\s/g, '-');
|
45
|
+
if(!hash.startsWith('#')) {
|
46
|
+
hash = `#${hash}`;
|
47
|
+
}
|
48
|
+
return hash;
|
49
|
+
};
|
50
|
+
|
51
|
+
/**
|
52
|
+
* Construct the tabs from the children. Each child calls the setTabItem function.
|
53
|
+
*/
|
54
|
+
let items = $state<{ id: string, label: string, href?: string }[]>([]);
|
55
|
+
|
56
|
+
function setTabItem(id: string, label: string, href?: string) {
|
57
|
+
items.push({ id, label, href });
|
58
|
+
}
|
59
|
+
|
60
|
+
setContext('TabbedContent.setTabItem', setTabItem);
|
61
|
+
</script>
|
62
|
+
|
63
|
+
<div>
|
64
|
+
<Tabs items={items} {contained} {rounded} />
|
65
|
+
{@render children()}
|
66
|
+
</div>
|
67
|
+
|
68
|
+
<style>
|
69
|
+
div {
|
70
|
+
display: flex;
|
71
|
+
flex-direction: column;
|
72
|
+
gap: var(--gap, 1.5rem);
|
73
|
+
}
|
74
|
+
</style>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { type Snippet } from "svelte";
|
2
|
+
type $$ComponentProps = {
|
3
|
+
/** Contain the element in a box. */
|
4
|
+
contained?: boolean;
|
5
|
+
/** Round the corners of the element if contained. */
|
6
|
+
rounded?: boolean;
|
7
|
+
children: Snippet;
|
8
|
+
};
|
9
|
+
declare const TabbedContent: import("svelte").Component<$$ComponentProps, {}, "">;
|
10
|
+
type TabbedContent = ReturnType<typeof TabbedContent>;
|
11
|
+
export default TabbedContent;
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { getContext, type Snippet } from "svelte";
|
3
|
+
|
4
|
+
let {
|
5
|
+
id,
|
6
|
+
label,
|
7
|
+
href,
|
8
|
+
children,
|
9
|
+
}: {
|
10
|
+
id?: string;
|
11
|
+
label: string;
|
12
|
+
href?: string;
|
13
|
+
children: Snippet;
|
14
|
+
} = $props();
|
15
|
+
|
16
|
+
if(!id) {
|
17
|
+
id = label.toLowerCase().replace(/ /g, '-');
|
18
|
+
}
|
19
|
+
|
20
|
+
if(href && !href.startsWith('#')) {
|
21
|
+
href = `#${encodeURIComponent(href)}`;
|
22
|
+
}
|
23
|
+
|
24
|
+
const setTabItem = getContext<((id: string, label: string, href?: string) => void)>('TabbedContent.setTabItem');
|
25
|
+
setTabItem(id, label, href);
|
26
|
+
</script>
|
27
|
+
|
28
|
+
<div class="TabbedContentItem">
|
29
|
+
{@render children()}
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<style>
|
33
|
+
</style>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { type Snippet } from "svelte";
|
2
|
+
type $$ComponentProps = {
|
3
|
+
id?: string;
|
4
|
+
label: string;
|
5
|
+
href?: string;
|
6
|
+
children: Snippet;
|
7
|
+
};
|
8
|
+
declare const TabbedContentItem: import("svelte").Component<$$ComponentProps, {}, "">;
|
9
|
+
type TabbedContentItem = ReturnType<typeof TabbedContentItem>;
|
10
|
+
export default TabbedContentItem;
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { getContext, type Snippet } from "svelte";
|
3
|
+
|
4
|
+
let {
|
5
|
+
colored,
|
6
|
+
contained,
|
7
|
+
rounded,
|
8
|
+
hoverable,
|
9
|
+
hang,
|
10
|
+
fullWidth,
|
11
|
+
children,
|
12
|
+
}: {
|
13
|
+
colored?: boolean;
|
14
|
+
contained?: boolean;
|
15
|
+
rounded?: boolean;
|
16
|
+
hang?: boolean;
|
17
|
+
hoverable?: boolean;
|
18
|
+
fullWidth?: boolean;
|
19
|
+
children: Snippet;
|
20
|
+
} = $props();
|
21
|
+
|
22
|
+
if(contained === undefined) { contained = getContext('lutra.table.contained') ?? getContext('lutra.contained') ?? false; }
|
23
|
+
</script>
|
24
|
+
|
25
|
+
<div
|
26
|
+
class="table-container"
|
27
|
+
class:hang
|
28
|
+
class:contained
|
29
|
+
class:rounded
|
30
|
+
>
|
31
|
+
<table
|
32
|
+
class:colored
|
33
|
+
class:contained
|
34
|
+
class:rounded
|
35
|
+
class:hang
|
36
|
+
class:fullWidth
|
37
|
+
class:hoverable
|
38
|
+
>
|
39
|
+
{@render children()}
|
40
|
+
</table>
|
41
|
+
</div>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { type Snippet } from "svelte";
|
2
|
+
type $$ComponentProps = {
|
3
|
+
colored?: boolean;
|
4
|
+
contained?: boolean;
|
5
|
+
rounded?: boolean;
|
6
|
+
hang?: boolean;
|
7
|
+
hoverable?: boolean;
|
8
|
+
fullWidth?: boolean;
|
9
|
+
children: Snippet;
|
10
|
+
};
|
11
|
+
declare const Table: import("svelte").Component<$$ComponentProps, {}, "">;
|
12
|
+
type Table = ReturnType<typeof Table>;
|
13
|
+
export default Table;
|
@@ -0,0 +1,216 @@
|
|
1
|
+
<script lang="ts">
|
2
|
+
import { browser } from "$app/environment";
|
3
|
+
import { page } from "$app/stores";
|
4
|
+
import type { TabItem } from "./MenuTypes.js";
|
5
|
+
import { onMount } from "svelte";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* @description
|
9
|
+
* A tabbed navigation menu used to navigate between pages or sections of content.
|
10
|
+
* Clicking a tab will change the URL or trigger a JavaScript event.
|
11
|
+
* @example
|
12
|
+
* <Tabs contained rounded items={[
|
13
|
+
* {
|
14
|
+
* label: 'Tab 1',
|
15
|
+
* href: '#tab1',
|
16
|
+
* },
|
17
|
+
* {
|
18
|
+
* label: 'Tab 2',
|
19
|
+
* href: '#tab2',
|
20
|
+
* },
|
21
|
+
* {
|
22
|
+
* label: 'Tab 3',
|
23
|
+
* href: '#tab3',
|
24
|
+
* },
|
25
|
+
* ]} />
|
26
|
+
*/
|
27
|
+
let {
|
28
|
+
items,
|
29
|
+
underline,
|
30
|
+
contained,
|
31
|
+
rounded,
|
32
|
+
selected = $bindable<{ label: string, href?: string, index: number } | null>(null)
|
33
|
+
}: {
|
34
|
+
/** Tab items to display. */
|
35
|
+
items: TabItem[];
|
36
|
+
/** Underline the active tab. The underline will slide to the selected tab unless reduced motion is enabled. */
|
37
|
+
underline?: boolean;
|
38
|
+
/** Contain the element in a box. */
|
39
|
+
contained?: boolean;
|
40
|
+
/** Round the corners of the element if contained. */
|
41
|
+
rounded?: boolean;
|
42
|
+
/** The index of the selected tab (bindable). */
|
43
|
+
selected?: { label: string, href?: string, index: number } | null;
|
44
|
+
} = $props();
|
45
|
+
|
46
|
+
const id = $state(`Tabs-${Math.random().toString(36).slice(2)}`);
|
47
|
+
|
48
|
+
const itemsWithId = $derived(items.map((item, index) => {
|
49
|
+
return { ...item, id: `Tab-${id}-${index}` };
|
50
|
+
}));
|
51
|
+
|
52
|
+
function onClickButton(event: MouseEvent, item: TabItem, index: number) {
|
53
|
+
if(item.onclick) {
|
54
|
+
item.onclick(event, item, index);
|
55
|
+
}
|
56
|
+
selected = { label: item.label, href: item.href, index };
|
57
|
+
}
|
58
|
+
|
59
|
+
let activeIndex = $derived.by(() => {
|
60
|
+
return items.findIndex((item, index) => {
|
61
|
+
console.log(item.href);
|
62
|
+
if(!item.href) return false;
|
63
|
+
// check if href ends in *
|
64
|
+
if(item.href.endsWith('*')) {
|
65
|
+
return $page.url.pathname.startsWith(item.href.slice(0, -1));
|
66
|
+
} else {
|
67
|
+
return $page.url.pathname.endsWith(item.href);
|
68
|
+
}
|
69
|
+
})
|
70
|
+
});
|
71
|
+
|
72
|
+
$effect(() => {
|
73
|
+
console.log(activeIndex);
|
74
|
+
});
|
75
|
+
|
76
|
+
let loaded = $state(false);
|
77
|
+
|
78
|
+
onMount(async () => {
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
80
|
+
loaded = true;
|
81
|
+
});
|
82
|
+
|
83
|
+
let underlineStyle = $derived.by(() => {
|
84
|
+
if(browser && underline && loaded) {
|
85
|
+
let active = items[activeIndex];
|
86
|
+
let activeElement: HTMLElement | null = document.querySelector(`#${id} li[data-index="${activeIndex}"]`);
|
87
|
+
if(activeElement) {
|
88
|
+
return `
|
89
|
+
opacity: 1;
|
90
|
+
width: ${activeElement.clientWidth}px;
|
91
|
+
transform: translateX(${activeElement.offsetLeft}px)
|
92
|
+
`
|
93
|
+
}
|
94
|
+
}
|
95
|
+
return 'height: 0;';
|
96
|
+
|
97
|
+
});
|
98
|
+
|
99
|
+
function removePossibleStar(href: string) {
|
100
|
+
return href.endsWith('*') ? href.slice(0, -1) : href;
|
101
|
+
}
|
102
|
+
|
103
|
+
</script>
|
104
|
+
|
105
|
+
<svelte:window onresize={() => { loaded = false; loaded = true; }} />
|
106
|
+
|
107
|
+
<nav class="Tabs" {id} class:contained class:rounded>
|
108
|
+
<menu>
|
109
|
+
{#each items as item, index}
|
110
|
+
<li data-index={index} aria-current={item.active || activeIndex === index ? 'page' : undefined}>
|
111
|
+
{#if item.href}
|
112
|
+
<a draggable="false" href={removePossibleStar(item.href)}>
|
113
|
+
{item.label}
|
114
|
+
</a>
|
115
|
+
{:else if item.onclick}
|
116
|
+
<button type="button" draggable="false" onclick={(event) => onClickButton(event, item, index)}>
|
117
|
+
{item.label}
|
118
|
+
</button>
|
119
|
+
{/if}
|
120
|
+
</li>
|
121
|
+
{/each}
|
122
|
+
<div class="Underline" style={underlineStyle}></div>
|
123
|
+
</menu>
|
124
|
+
</nav>
|
125
|
+
|
126
|
+
<style>
|
127
|
+
.Tabs {
|
128
|
+
display: flex;
|
129
|
+
z-index: 2;
|
130
|
+
background: var(--menu-bg);
|
131
|
+
overflow: clip;
|
132
|
+
}
|
133
|
+
.Tabs.contained {
|
134
|
+
border: var(--menu-border);
|
135
|
+
}
|
136
|
+
.Tabs.rounded {
|
137
|
+
border-radius: var(--border-radius);
|
138
|
+
}
|
139
|
+
menu, li {
|
140
|
+
list-style: none;
|
141
|
+
margin: 0;
|
142
|
+
}
|
143
|
+
menu {
|
144
|
+
position: relative;
|
145
|
+
display: flex;
|
146
|
+
flex-direction: row;
|
147
|
+
flex-wrap: nowrap;
|
148
|
+
align-items: center;
|
149
|
+
justify-content: flex-start;
|
150
|
+
padding: 0;
|
151
|
+
gap: var(--gap, 1rem);
|
152
|
+
inline-size: 100%;
|
153
|
+
border-block-end: var(--menu-border);
|
154
|
+
}
|
155
|
+
.Tabs.contained menu {
|
156
|
+
gap: 0;
|
157
|
+
flex-grow: 1;
|
158
|
+
justify-content: stretch;
|
159
|
+
border-block-end: 0;
|
160
|
+
}
|
161
|
+
a,
|
162
|
+
button {
|
163
|
+
display: block;
|
164
|
+
padding: var(--padding, 0.75rem 0.5rem);
|
165
|
+
color: var(--menu-text);
|
166
|
+
transition: all var(--menu-trans);
|
167
|
+
font-weight: 500;
|
168
|
+
font-size: var(--font-size, 0.9em);
|
169
|
+
letter-spacing: -0.05ch;
|
170
|
+
background: transparent;
|
171
|
+
border: none;
|
172
|
+
border-block-end: 2px solid transparent;
|
173
|
+
cursor: pointer;
|
174
|
+
text-decoration: none;
|
175
|
+
}
|
176
|
+
menu li:first-child > * {
|
177
|
+
padding-inline-start: 0;
|
178
|
+
}
|
179
|
+
.Tabs.contained li {
|
180
|
+
flex-grow: 1;
|
181
|
+
border-inline-end: var(--menu-border);
|
182
|
+
flex-basis: auto;
|
183
|
+
}
|
184
|
+
.Tabs.contained menu li:last-of-type {
|
185
|
+
border-inline-end: 0;
|
186
|
+
}
|
187
|
+
.Tabs.contained a,
|
188
|
+
.Tabs.contained button {
|
189
|
+
flex-grow: 1;
|
190
|
+
inline-size: 100%;
|
191
|
+
text-align: center;
|
192
|
+
padding-block-start: calc(0.75rem + 3px);
|
193
|
+
color: var(--menu-text);
|
194
|
+
font-weight: 600;
|
195
|
+
}
|
196
|
+
a:hover,
|
197
|
+
button:hover {
|
198
|
+
color: color-mix(in hsl shorter hue, var(--menu-text) var(--mix-amount), var(--mix-target));
|
199
|
+
background-color: color-mix(in hsl shorter hue, var(--menu-bg) var(--mix-amount), var(--mix-target));
|
200
|
+
}
|
201
|
+
li[aria-current="page"] a,
|
202
|
+
li[aria-current="page"] button {
|
203
|
+
background: var(--menu-bg-active);
|
204
|
+
color: var(--menu-text-active);
|
205
|
+
opacity: 1;
|
206
|
+
}
|
207
|
+
.Underline {
|
208
|
+
height: 2px;
|
209
|
+
background-color: var(--menu-text-active);
|
210
|
+
position: absolute;
|
211
|
+
bottom: 0;
|
212
|
+
left: 0;
|
213
|
+
transition: all 0.2s ease-out;
|
214
|
+
opacity: 0;
|
215
|
+
}
|
216
|
+
</style>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import type { TabItem } from "./MenuTypes.js";
|
2
|
+
type $$ComponentProps = {
|
3
|
+
/** Tab items to display. */
|
4
|
+
items: TabItem[];
|
5
|
+
/** Underline the active tab. The underline will slide to the selected tab unless reduced motion is enabled. */
|
6
|
+
underline?: boolean;
|
7
|
+
/** Contain the element in a box. */
|
8
|
+
contained?: boolean;
|
9
|
+
/** Round the corners of the element if contained. */
|
10
|
+
rounded?: boolean;
|
11
|
+
/** The index of the selected tab (bindable). */
|
12
|
+
selected?: {
|
13
|
+
label: string;
|
14
|
+
href?: string;
|
15
|
+
index: number;
|
16
|
+
} | null;
|
17
|
+
};
|
18
|
+
declare const Tabs: import("svelte").Component<$$ComponentProps, {}, "selected">;
|
19
|
+
type Tabs = ReturnType<typeof Tabs>;
|
20
|
+
export default Tabs;
|