@astrake/lumora-ui 0.1.6 → 0.2.0
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/CHANGELOG.md +46 -1
- package/package.json +9 -1
- package/src/components/LuAlert.vue +33 -0
- package/src/components/LuBreadcrumb.vue +63 -0
- package/src/components/LuCard.vue +8 -1
- package/src/components/LuCheckbox.vue +94 -0
- package/src/components/LuCodeBlock.vue +168 -0
- package/src/components/LuInput.vue +20 -0
- package/src/components/LuMenu.vue +86 -0
- package/src/components/LuMenuItem.vue +37 -0
- package/src/components/LuModal.vue +115 -0
- package/src/components/LuPagination.vue +118 -0
- package/src/components/LuRadio.vue +55 -0
- package/src/components/LuRadioGroup.types.ts +10 -0
- package/src/components/LuRadioGroup.vue +66 -0
- package/src/components/LuSkeleton.vue +15 -0
- package/src/components/LuSpinner.vue +36 -0
- package/src/components/LuSwitch.vue +8 -6
- package/src/components/LuTag.vue +35 -0
- package/src/components/LuTextarea.vue +62 -0
- package/src/components/LuToggleButton.vue +35 -0
- package/src/components/LuToggleGroup.vue +27 -0
- package/src/components/index.ts +16 -0
- package/src/context.ts +8 -5
- package/src/index.ts +2 -2
- package/src/layout/LuDock.vue +53 -20
- package/src/layout/LuDockItem.vue +3 -1
- package/src/layout/LuFill.vue +15 -5
- package/src/layout/LuFixed.vue +15 -5
- package/src/layout/LuGrid.vue +28 -5
- package/src/layout/LuScroll.vue +3 -1
- package/src/layout/LuSplitPane.vue +3 -3
- package/src/layout/LuSplitResizer.vue +5 -3
- package/src/layout/LuStack.vue +16 -11
- package/src/lumora.css +16 -0
- package/src/plugin.ts +3 -2
- package/src/shell/desktop/LuDesktopRailItem.vue +2 -2
- package/src/shell/desktop/LuDesktopShell.vue +3 -3
- package/src/shell/embedded/LuEmbeddedShell.vue +2 -2
- package/src/shell/embedded/LuEmbeddedStatusBar.vue +16 -0
- package/src/shell/embedded/LuEmbeddedTopBar.vue +17 -0
- package/src/shell/index.ts +4 -1
- package/src/shell/mobile/LuMobileHeader.vue +17 -0
- package/src/shell/mobile/LuMobileNavBar.vue +15 -0
- package/src/shell/mobile/LuMobileShell.vue +2 -2
- package/src/skins/default.ts +361 -29
- package/src/tailwind.ts +25 -0
- package/src/utils.ts +95 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<transition
|
|
4
|
+
enter-active-class="transition ease-out duration-200"
|
|
5
|
+
enter-from-class="opacity-0"
|
|
6
|
+
enter-to-class="opacity-100"
|
|
7
|
+
leave-active-class="transition ease-in duration-150"
|
|
8
|
+
leave-from-class="opacity-100"
|
|
9
|
+
leave-to-class="opacity-0"
|
|
10
|
+
>
|
|
11
|
+
<div v-if="modelValue" :class="resolvedOverlaySkin" @click="handleOverlayClick" aria-modal="true" role="dialog" tabindex="-1">
|
|
12
|
+
<transition
|
|
13
|
+
enter-active-class="transition ease-out duration-200"
|
|
14
|
+
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
15
|
+
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
|
16
|
+
leave-active-class="transition ease-in duration-150"
|
|
17
|
+
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
|
18
|
+
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
19
|
+
appear
|
|
20
|
+
>
|
|
21
|
+
<div v-if="modelValue" :class="resolvedSkin" @click.stop>
|
|
22
|
+
<div v-if="$slots.header || title" :class="resolvedHeaderSkin">
|
|
23
|
+
<slot name="header">
|
|
24
|
+
<LuText variant="section-title">{{ title }}</LuText>
|
|
25
|
+
</slot>
|
|
26
|
+
<button v-if="closable" type="button" :class="resolvedCloseButtonSkin" @click="close" aria-label="Close modal">
|
|
27
|
+
<LuIcon name="x" class="h-5 w-5" />
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div :class="resolvedContentSkin">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div v-if="$slots.footer" :class="resolvedFooterSkin">
|
|
36
|
+
<slot name="footer" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</transition>
|
|
40
|
+
</div>
|
|
41
|
+
</transition>
|
|
42
|
+
</Teleport>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
import { computed, onMounted, onBeforeUnmount, watch } from "vue";
|
|
47
|
+
import { useLumoraConfig } from "../context";
|
|
48
|
+
import LuText from "./LuText.vue";
|
|
49
|
+
import LuIcon from "./LuIcon.vue";
|
|
50
|
+
|
|
51
|
+
const props = withDefaults(defineProps<{
|
|
52
|
+
modelValue: boolean;
|
|
53
|
+
title?: string;
|
|
54
|
+
variant?: string;
|
|
55
|
+
closable?: boolean;
|
|
56
|
+
closeOnOverlayClick?: boolean;
|
|
57
|
+
}>(), {
|
|
58
|
+
closable: true,
|
|
59
|
+
closeOnOverlayClick: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const emit = defineEmits<{
|
|
63
|
+
(e: "update:modelValue", value: boolean): void;
|
|
64
|
+
(e: "close"): void;
|
|
65
|
+
}>();
|
|
66
|
+
|
|
67
|
+
const { resolveSkin } = useLumoraConfig();
|
|
68
|
+
|
|
69
|
+
const resolvedOverlaySkin = computed(() => resolveSkin("LuModalOverlay", props.variant));
|
|
70
|
+
const resolvedSkin = computed(() => resolveSkin("LuModal", props.variant));
|
|
71
|
+
const resolvedHeaderSkin = computed(() => resolveSkin("LuModalHeader", props.variant));
|
|
72
|
+
const resolvedContentSkin = computed(() => resolveSkin("LuModalContent", props.variant));
|
|
73
|
+
const resolvedFooterSkin = computed(() => resolveSkin("LuModalFooter", props.variant));
|
|
74
|
+
const resolvedCloseButtonSkin = computed(() => resolveSkin("LuModalCloseButton", props.variant));
|
|
75
|
+
|
|
76
|
+
const close = () => {
|
|
77
|
+
if (props.closable) {
|
|
78
|
+
emit("update:modelValue", false);
|
|
79
|
+
emit("close");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleOverlayClick = () => {
|
|
84
|
+
if (props.closeOnOverlayClick) {
|
|
85
|
+
close();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const handleEscKey = (e: KeyboardEvent) => {
|
|
90
|
+
if (e.key === 'Escape' && props.modelValue) {
|
|
91
|
+
close();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
watch(() => props.modelValue, (isOpen) => {
|
|
96
|
+
if (typeof document !== 'undefined') {
|
|
97
|
+
if (isOpen) {
|
|
98
|
+
document.body.style.overflow = 'hidden';
|
|
99
|
+
} else {
|
|
100
|
+
document.body.style.overflow = '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, { immediate: true });
|
|
104
|
+
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
document.addEventListener('keydown', handleEscKey);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
onBeforeUnmount(() => {
|
|
110
|
+
document.removeEventListener('keydown', handleEscKey);
|
|
111
|
+
if (typeof document !== 'undefined') {
|
|
112
|
+
document.body.style.overflow = '';
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
</script>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav aria-label="Pagination" :class="resolvedSkin">
|
|
3
|
+
<LuButton
|
|
4
|
+
variant="ghost"
|
|
5
|
+
:disabled="modelValue <= 1"
|
|
6
|
+
@click="prevPage"
|
|
7
|
+
:class="resolvedButtonSkin"
|
|
8
|
+
aria-label="Previous page"
|
|
9
|
+
>
|
|
10
|
+
<LuIcon name="chevron-left" class="h-4 w-4" />
|
|
11
|
+
</LuButton>
|
|
12
|
+
|
|
13
|
+
<div :class="resolvedPagesSkin">
|
|
14
|
+
<template v-for="page in pages" :key="page">
|
|
15
|
+
<span v-if="page === '...'" :class="resolvedEllipsisSkin">...</span>
|
|
16
|
+
<LuButton
|
|
17
|
+
v-else
|
|
18
|
+
:variant="page === modelValue ? 'primary' : 'ghost'"
|
|
19
|
+
@click="goToPage(page as number)"
|
|
20
|
+
:class="resolvedPageButtonSkin"
|
|
21
|
+
:aria-current="page === modelValue ? 'page' : undefined"
|
|
22
|
+
>
|
|
23
|
+
{{ page }}
|
|
24
|
+
</LuButton>
|
|
25
|
+
</template>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<LuButton
|
|
29
|
+
variant="ghost"
|
|
30
|
+
:disabled="modelValue >= totalPages"
|
|
31
|
+
@click="nextPage"
|
|
32
|
+
:class="resolvedButtonSkin"
|
|
33
|
+
aria-label="Next page"
|
|
34
|
+
>
|
|
35
|
+
<LuIcon name="chevron-right" class="h-4 w-4" />
|
|
36
|
+
</LuButton>
|
|
37
|
+
</nav>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { computed } from "vue";
|
|
42
|
+
import { useLumoraConfig } from "../context";
|
|
43
|
+
import LuButton from "./LuButton.vue";
|
|
44
|
+
import LuIcon from "./LuIcon.vue";
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(defineProps<{
|
|
47
|
+
modelValue: number;
|
|
48
|
+
total: number;
|
|
49
|
+
pageSize?: number;
|
|
50
|
+
siblingCount?: number;
|
|
51
|
+
variant?: string;
|
|
52
|
+
}>(), {
|
|
53
|
+
pageSize: 10,
|
|
54
|
+
siblingCount: 1,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const emit = defineEmits<{
|
|
58
|
+
(e: "update:modelValue", value: number): void;
|
|
59
|
+
(e: "change", value: number): void;
|
|
60
|
+
}>();
|
|
61
|
+
|
|
62
|
+
const totalPages = computed(() => Math.ceil(props.total / props.pageSize));
|
|
63
|
+
|
|
64
|
+
const pages = computed(() => {
|
|
65
|
+
const current = props.modelValue;
|
|
66
|
+
const total = totalPages.value;
|
|
67
|
+
const siblings = props.siblingCount;
|
|
68
|
+
|
|
69
|
+
if (total <= 1) return [1];
|
|
70
|
+
|
|
71
|
+
const leftSiblingIndex = Math.max(current - siblings, 1);
|
|
72
|
+
const rightSiblingIndex = Math.min(current + siblings, total);
|
|
73
|
+
|
|
74
|
+
const showLeftEllipsis = leftSiblingIndex > 2;
|
|
75
|
+
const showRightEllipsis = rightSiblingIndex < total - 1;
|
|
76
|
+
|
|
77
|
+
if (!showLeftEllipsis && showRightEllipsis) {
|
|
78
|
+
const leftItemCount = 3 + 2 * siblings;
|
|
79
|
+
const leftRange = Array.from({ length: Math.min(leftItemCount, total) }, (_, i) => i + 1);
|
|
80
|
+
return [...leftRange, '...', total];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (showLeftEllipsis && !showRightEllipsis) {
|
|
84
|
+
const rightItemCount = 3 + 2 * siblings;
|
|
85
|
+
const rightRange = Array.from({ length: Math.min(rightItemCount, total) }, (_, i) => total - rightItemCount + i + 1);
|
|
86
|
+
return [1, '...', ...rightRange];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (showLeftEllipsis && showRightEllipsis) {
|
|
90
|
+
const middleRange = Array.from({ length: rightSiblingIndex - leftSiblingIndex + 1 }, (_, i) => leftSiblingIndex + i);
|
|
91
|
+
return [1, '...', ...middleRange, '...', total];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const { resolveSkin } = useLumoraConfig();
|
|
98
|
+
|
|
99
|
+
const resolvedSkin = computed(() => resolveSkin("LuPagination", props.variant));
|
|
100
|
+
const resolvedButtonSkin = computed(() => resolveSkin("LuPaginationButton", props.variant));
|
|
101
|
+
const resolvedPagesSkin = computed(() => resolveSkin("LuPaginationPages", props.variant));
|
|
102
|
+
const resolvedPageButtonSkin = computed(() => resolveSkin("LuPaginationPageButton", props.variant));
|
|
103
|
+
const resolvedEllipsisSkin = computed(() => resolveSkin("LuPaginationEllipsis", props.variant));
|
|
104
|
+
|
|
105
|
+
const goToPage = (page: number) => {
|
|
106
|
+
if (page === props.modelValue) return;
|
|
107
|
+
emit("update:modelValue", page);
|
|
108
|
+
emit("change", page);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const prevPage = () => {
|
|
112
|
+
if (props.modelValue > 1) goToPage(props.modelValue - 1);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const nextPage = () => {
|
|
116
|
+
if (props.modelValue < totalPages.value) goToPage(props.modelValue + 1);
|
|
117
|
+
};
|
|
118
|
+
</script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedContainerSkin">
|
|
3
|
+
<input
|
|
4
|
+
type="radio"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
:class="resolvedSkin"
|
|
7
|
+
:name="radioGroup?.name"
|
|
8
|
+
:value="value"
|
|
9
|
+
:checked="isChecked"
|
|
10
|
+
:disabled="radioGroup?.disabled.value || disabled"
|
|
11
|
+
@change="onChange"
|
|
12
|
+
/>
|
|
13
|
+
<label v-if="$slots.default || label" :class="resolvedLabelSkin" @click.prevent="onClick">
|
|
14
|
+
<slot>{{ label }}</slot>
|
|
15
|
+
</label>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, inject } from "vue";
|
|
21
|
+
import { useLumoraConfig } from "../context";
|
|
22
|
+
import { LuRadioGroupContextKey } from "./LuRadioGroup.types";
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
value: string | number;
|
|
26
|
+
variant?: string;
|
|
27
|
+
label?: string;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
}>();
|
|
30
|
+
|
|
31
|
+
const { resolveSkin } = useLumoraConfig();
|
|
32
|
+
const resolvedContainerSkin = computed(() => resolveSkin("LuRadioContainer", props.variant));
|
|
33
|
+
const resolvedSkin = computed(() => resolveSkin("LuRadio", props.variant));
|
|
34
|
+
const resolvedLabelSkin = computed(() => resolveSkin("LuRadioLabel", props.variant));
|
|
35
|
+
|
|
36
|
+
const radioGroup = inject(LuRadioGroupContextKey, null);
|
|
37
|
+
|
|
38
|
+
if (!radioGroup) {
|
|
39
|
+
console.warn("LuRadio must be used within a LuRadioGroup");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isChecked = computed(() => {
|
|
43
|
+
return radioGroup?.modelValue.value === props.value;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const onChange = () => {
|
|
47
|
+
if (props.disabled || radioGroup?.disabled.value) return;
|
|
48
|
+
radioGroup?.updateValue(props.value);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const onClick = () => {
|
|
52
|
+
if (props.disabled || radioGroup?.disabled.value) return;
|
|
53
|
+
radioGroup?.updateValue(props.value);
|
|
54
|
+
};
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InjectionKey, Ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
export interface LuRadioGroupContext {
|
|
4
|
+
name: string;
|
|
5
|
+
modelValue: Ref<string | number | undefined>;
|
|
6
|
+
updateValue: (value: string | number) => void;
|
|
7
|
+
disabled: Ref<boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const LuRadioGroupContextKey: InjectionKey<LuRadioGroupContext> = Symbol('LuRadioGroupContext');
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedSkin" role="radiogroup" :aria-disabled="disabled || formContext?.disabled.value">
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, inject, onMounted, onUnmounted, provide, ref, watch } from "vue";
|
|
9
|
+
import { useLumoraConfig } from "../context";
|
|
10
|
+
import { LuFormContextKey } from "./LuForm.types";
|
|
11
|
+
import { LuRadioGroupContextKey } from "./LuRadioGroup.types";
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
modelValue?: string | number;
|
|
15
|
+
name: string;
|
|
16
|
+
variant?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{
|
|
21
|
+
(e: "update:modelValue", value: string | number): void;
|
|
22
|
+
(e: "change", value: string | number): void;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const { resolveSkin } = useLumoraConfig();
|
|
26
|
+
const resolvedSkin = computed(() => resolveSkin("LuRadioGroup", props.variant));
|
|
27
|
+
|
|
28
|
+
const formContext = inject(LuFormContextKey, null);
|
|
29
|
+
const internalValue = ref<string | number | undefined>(props.modelValue);
|
|
30
|
+
|
|
31
|
+
watch(() => props.modelValue, (newVal) => {
|
|
32
|
+
internalValue.value = newVal;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const updateValue = (value: string | number) => {
|
|
36
|
+
if (props.disabled || formContext?.disabled.value) return;
|
|
37
|
+
internalValue.value = value;
|
|
38
|
+
emit("update:modelValue", value);
|
|
39
|
+
emit("change", value);
|
|
40
|
+
|
|
41
|
+
if (props.name && formContext && (formContext.validateOn.value === "blur" || formContext.validateOn.value === "both")) {
|
|
42
|
+
// validation
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
provide(LuRadioGroupContextKey, {
|
|
47
|
+
name: props.name,
|
|
48
|
+
modelValue: computed(() => internalValue.value),
|
|
49
|
+
updateValue,
|
|
50
|
+
disabled: computed(() => !!props.disabled || !!formContext?.disabled.value)
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
onMounted(() => {
|
|
54
|
+
if (!props.name || !formContext) return;
|
|
55
|
+
formContext.register({
|
|
56
|
+
name: props.name,
|
|
57
|
+
getValue: () => internalValue.value,
|
|
58
|
+
setValue: (v) => { internalValue.value = v as string | number; },
|
|
59
|
+
setError: (_msg) => {},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
onUnmounted(() => {
|
|
64
|
+
if (props.name && formContext) formContext.unregister(props.name);
|
|
65
|
+
});
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedSkin" aria-hidden="true" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { computed } from "vue";
|
|
7
|
+
import { useLumoraConfig } from "../context";
|
|
8
|
+
|
|
9
|
+
const props = defineProps<{
|
|
10
|
+
variant?: string;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const { resolveSkin } = useLumoraConfig();
|
|
14
|
+
const resolvedSkin = computed(() => resolveSkin("LuSkeleton", props.variant));
|
|
15
|
+
</script>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg
|
|
3
|
+
:class="resolvedSkin"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
fill="none"
|
|
6
|
+
viewBox="0 0 24 24"
|
|
7
|
+
role="status"
|
|
8
|
+
aria-label="Loading"
|
|
9
|
+
>
|
|
10
|
+
<circle
|
|
11
|
+
class="opacity-25"
|
|
12
|
+
cx="12"
|
|
13
|
+
cy="12"
|
|
14
|
+
r="10"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
stroke-width="4"
|
|
17
|
+
></circle>
|
|
18
|
+
<path
|
|
19
|
+
class="opacity-75"
|
|
20
|
+
fill="currentColor"
|
|
21
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
22
|
+
></path>
|
|
23
|
+
</svg>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import { computed } from "vue";
|
|
28
|
+
import { useLumoraConfig } from "../context";
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
variant?: string;
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
const { resolveSkin } = useLumoraConfig();
|
|
35
|
+
const resolvedSkin = computed(() => resolveSkin("LuSpinner", props.variant));
|
|
36
|
+
</script>
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
role="switch"
|
|
4
4
|
type="button"
|
|
5
5
|
:name="name"
|
|
6
|
-
:aria-checked="
|
|
6
|
+
:aria-checked="isChecked"
|
|
7
7
|
:disabled="mergedDisabled"
|
|
8
|
-
:class="
|
|
8
|
+
:class="cn(resolvedSkin, isChecked ? activeSkin : '')"
|
|
9
9
|
@click="toggle"
|
|
10
10
|
@blur="onBlur"
|
|
11
11
|
>
|
|
12
|
-
<span :class="
|
|
12
|
+
<span :class="cn(thumbSkin, isChecked ? thumbActiveSkin : '')" />
|
|
13
13
|
</button>
|
|
14
14
|
</template>
|
|
15
15
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { computed, inject, onMounted, onUnmounted, ref } from "vue";
|
|
18
18
|
import { useLumoraConfig } from "../context";
|
|
19
19
|
import { LuFormContextKey } from "./LuForm.types";
|
|
20
|
+
import { cn } from "../utils";
|
|
20
21
|
|
|
21
22
|
const props = defineProps<{
|
|
22
23
|
modelValue?: boolean;
|
|
@@ -34,19 +35,20 @@ const emit = defineEmits<{
|
|
|
34
35
|
const { resolveSkin } = useLumoraConfig();
|
|
35
36
|
|
|
36
37
|
const resolvedSkin = computed(() => resolveSkin("LuSwitch", props.variant));
|
|
37
|
-
const activeSkin = computed(() => resolveSkin("LuSwitch", "
|
|
38
|
+
const activeSkin = computed(() => resolveSkin("LuSwitch", "checked"));
|
|
38
39
|
|
|
39
40
|
const thumbSkin = computed(() => resolveSkin("LuSwitchThumb", props.variant));
|
|
40
|
-
const thumbActiveSkin = computed(() => resolveSkin("LuSwitchThumb", "
|
|
41
|
+
const thumbActiveSkin = computed(() => resolveSkin("LuSwitchThumb", "checked"));
|
|
41
42
|
|
|
42
43
|
const formContext = inject(LuFormContextKey, null);
|
|
43
44
|
const internalValue = ref<boolean | undefined>(props.modelValue);
|
|
45
|
+
const isChecked = computed(() => props.modelValue !== undefined ? props.modelValue : !!internalValue.value);
|
|
44
46
|
|
|
45
47
|
const mergedDisabled = computed(() => props.disabled || formContext?.disabled.value);
|
|
46
48
|
|
|
47
49
|
const toggle = () => {
|
|
48
50
|
if (mergedDisabled.value) return;
|
|
49
|
-
const newValue = !
|
|
51
|
+
const newValue = !isChecked.value;
|
|
50
52
|
internalValue.value = newValue;
|
|
51
53
|
emit("update:modelValue", newValue);
|
|
52
54
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span :class="resolvedSkin">
|
|
3
|
+
<slot />
|
|
4
|
+
<button
|
|
5
|
+
v-if="closable"
|
|
6
|
+
type="button"
|
|
7
|
+
:class="resolvedCloseButtonSkin"
|
|
8
|
+
@click.stop="emit('close')"
|
|
9
|
+
aria-label="Remove"
|
|
10
|
+
>
|
|
11
|
+
<LuIcon name="x" :class="resolvedIconSkin" />
|
|
12
|
+
</button>
|
|
13
|
+
</span>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from "vue";
|
|
18
|
+
import { useLumoraConfig } from "../context";
|
|
19
|
+
import LuIcon from "./LuIcon.vue";
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
variant?: string;
|
|
23
|
+
closable?: boolean;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const emit = defineEmits<{
|
|
27
|
+
(e: "close"): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const { resolveSkin } = useLumoraConfig();
|
|
31
|
+
|
|
32
|
+
const resolvedSkin = computed(() => resolveSkin("LuTag", props.variant));
|
|
33
|
+
const resolvedCloseButtonSkin = computed(() => resolveSkin("LuTagCloseButton", props.variant));
|
|
34
|
+
const resolvedIconSkin = computed(() => resolveSkin("LuTagIcon", props.variant));
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<textarea
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
:class="resolvedSkin"
|
|
5
|
+
:value="modelValue"
|
|
6
|
+
:name="name"
|
|
7
|
+
:disabled="formContext?.disabled.value"
|
|
8
|
+
@input="onInput"
|
|
9
|
+
@blur="onBlur"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { computed, inject, onMounted, onUnmounted, ref } from "vue";
|
|
15
|
+
import { useLumoraConfig } from "../context";
|
|
16
|
+
import { LuFormContextKey } from "./LuForm.types";
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
modelValue?: string | number;
|
|
20
|
+
variant?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
error?: string | null;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
(e: "update:modelValue", value: string): void;
|
|
27
|
+
(e: "blur"): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const { resolveSkin } = useLumoraConfig();
|
|
31
|
+
const resolvedSkin = computed(() => resolveSkin("LuTextarea", props.variant));
|
|
32
|
+
|
|
33
|
+
const formContext = inject(LuFormContextKey, null);
|
|
34
|
+
const internalValue = ref<string | number | undefined>(props.modelValue);
|
|
35
|
+
|
|
36
|
+
const onInput = (event: Event) => {
|
|
37
|
+
const value = (event.target as HTMLTextAreaElement).value;
|
|
38
|
+
internalValue.value = value;
|
|
39
|
+
emit("update:modelValue", value);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const onBlur = () => {
|
|
43
|
+
if (props.name && formContext && (formContext.validateOn.value === "blur" || formContext.validateOn.value === "both")) {
|
|
44
|
+
// trigger single-field validation — handled by parent LuForm
|
|
45
|
+
}
|
|
46
|
+
emit("blur");
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
onMounted(() => {
|
|
50
|
+
if (!props.name || !formContext) return;
|
|
51
|
+
formContext.register({
|
|
52
|
+
name: props.name,
|
|
53
|
+
getValue: () => internalValue.value,
|
|
54
|
+
setValue: (v) => { internalValue.value = v as string; },
|
|
55
|
+
setError: (_msg) => { /* error display handled via formContext.getError in template if desired */ },
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
onUnmounted(() => {
|
|
60
|
+
if (props.name && formContext) formContext.unregister(props.name);
|
|
61
|
+
});
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
v-bind="$attrs"
|
|
4
|
+
:class="[resolvedSkin, isActive ? activeSkin : '']"
|
|
5
|
+
@click="onClick"
|
|
6
|
+
:aria-pressed="isActive"
|
|
7
|
+
>
|
|
8
|
+
<slot />
|
|
9
|
+
</button>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { computed, inject, type Ref } from "vue";
|
|
14
|
+
import { useLumoraConfig } from "../context";
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
value: string | number | boolean;
|
|
18
|
+
variant?: string;
|
|
19
|
+
}>();
|
|
20
|
+
|
|
21
|
+
const groupValue = inject<Ref<string | number | boolean | undefined>>("lu-toggle-value");
|
|
22
|
+
const setGroupValue = inject<(value: string | number | boolean) => void>("lu-toggle-set");
|
|
23
|
+
|
|
24
|
+
const isActive = computed(() => groupValue?.value === props.value);
|
|
25
|
+
|
|
26
|
+
const onClick = () => {
|
|
27
|
+
if (setGroupValue) {
|
|
28
|
+
setGroupValue(props.value);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const { resolveSkin } = useLumoraConfig();
|
|
33
|
+
const resolvedSkin = computed(() => resolveSkin("LuToggleButton", props.variant));
|
|
34
|
+
const activeSkin = computed(() => resolveSkin("LuToggleButton", "active"));
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-bind="$attrs" :class="resolvedSkin" role="group">
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
import { computed, provide, type Ref } from "vue";
|
|
9
|
+
import { useLumoraConfig } from "../context";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
modelValue?: string | number | boolean;
|
|
13
|
+
variant?: string;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: "update:modelValue", value: string | number | boolean): void;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
provide("lu-toggle-value", computed(() => props.modelValue));
|
|
21
|
+
provide("lu-toggle-set", (value: string | number | boolean) => {
|
|
22
|
+
emit("update:modelValue", value);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const { resolveSkin } = useLumoraConfig();
|
|
26
|
+
const resolvedSkin = computed(() => resolveSkin("LuToggleGroup", props.variant));
|
|
27
|
+
</script>
|
package/src/components/index.ts
CHANGED
|
@@ -26,4 +26,20 @@ export { default as LuTableRow } from "./LuTableRow.vue";
|
|
|
26
26
|
export { default as LuTableHeadCell } from "./LuTableHeadCell.vue";
|
|
27
27
|
export { default as LuTableCell } from "./LuTableCell.vue";
|
|
28
28
|
export { default as LuForm } from "./LuForm.vue";
|
|
29
|
+
export { default as LuTextarea } from "./LuTextarea.vue";
|
|
30
|
+
export { default as LuCheckbox } from "./LuCheckbox.vue";
|
|
31
|
+
export { default as LuRadioGroup } from "./LuRadioGroup.vue";
|
|
32
|
+
export { default as LuRadio } from "./LuRadio.vue";
|
|
33
|
+
export { default as LuAlert } from "./LuAlert.vue";
|
|
34
|
+
export { default as LuSpinner } from "./LuSpinner.vue";
|
|
35
|
+
export { default as LuSkeleton } from "./LuSkeleton.vue";
|
|
36
|
+
export { default as LuTag } from "./LuTag.vue";
|
|
37
|
+
export { default as LuBreadcrumb } from "./LuBreadcrumb.vue";
|
|
38
|
+
export { default as LuMenu } from "./LuMenu.vue";
|
|
39
|
+
export { default as LuMenuItem } from "./LuMenuItem.vue";
|
|
40
|
+
export { default as LuPagination } from "./LuPagination.vue";
|
|
41
|
+
export { default as LuModal } from "./LuModal.vue";
|
|
42
|
+
export { default as LuToggleGroup } from "./LuToggleGroup.vue";
|
|
43
|
+
export { default as LuToggleButton } from "./LuToggleButton.vue";
|
|
44
|
+
export { default as LuCodeBlock } from "./LuCodeBlock.vue";
|
|
29
45
|
export type { LuFormRules, LuFormErrors, LuFormValidator, LuFormContext } from "./LuForm.types";
|