@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
package/CHANGELOG.md
CHANGED
|
@@ -4,13 +4,58 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.2.0] — 2026-04-27
|
|
8
|
+
|
|
9
|
+
### Maintenance
|
|
10
|
+
|
|
11
|
+
- bump version to 0.2.0 (`d52e5c1`)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## [0.2.0] — 2026-04-27
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `LuCodeBlock` — Native Shiki-powered syntax highlighter with `default` and `preview` variants (supporting `tabbed` and `split` layouts).
|
|
20
|
+
- `LuToggleGroup` & `LuToggleButton` — New layout primitives for grouped button selections.
|
|
21
|
+
- `LuMenu` — Refactored and renamed from legacy `LuDropdown` to align with the core primitives naming convention.
|
|
22
|
+
- New showcase documentation dedicated to Toggle Group and components.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- WPF-like edge-docking regressions in `LuDock` via a newly implemented programmatic render function.
|
|
26
|
+
- Reactivity bugs in `LuSplit` (`LuSplitPane`, `LuSplitResizer`) failing to unwrap injected `direction` refs.
|
|
27
|
+
- Missing `LuSplit` component skins.
|
|
28
|
+
- Various visual hierarchy issues in documentation via typography `LuText` adjustments.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **Zero-Raw-HTML Adherence**: Removed all raw DOM nodes (`div`, `span`, etc.) and `PreviewBlock` from the showcase application in favor of 100% native `Lu*` primitives.
|
|
32
|
+
- Integrated Shiki to `@astrake/lumora-ui` core package dependencies.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## [0.1.7] — 2026-04-26
|
|
37
|
+
|
|
38
|
+
### Breaking Changes (structurally additive — no API surface removed)
|
|
39
|
+
- Framework now ships `lumora.css` — consumers must add `import '@astrake/lumora-ui/style'` to their app entry
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- Shell components (`LuDesktopShell`, `LuMobileShell`, `LuEmbeddedShell`) no longer require Tailwind CSS to be configured for `node_modules` — structural layout is now framework-owned CSS
|
|
43
|
+
- Layout primitives (`LuDock`, `LuStack`, `LuFill`, `LuScroll`, `LuGrid`, etc.) structural classes are now framework-owned, not Tailwind fallbacks
|
|
44
|
+
- `LuDesktopRailItem` icon and label structural defaults are now framework-owned
|
|
45
|
+
- Plugin config is now `shallowReactive` — dynamic skin switching and `config.skin = newSkin` mutations now correctly trigger reactive re-renders in all components
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
- `lumora.css` — zero-dependency CSS baseline for all structural/layout defaults; exported at `@astrake/lumora-ui/style`
|
|
49
|
+
- `tailwindContent()` helper exported at `@astrake/lumora-ui/tailwind` — resolves correct `node_modules` source glob for Tailwind `content` config
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
7
53
|
## [0.1.6] — 2026-04-25
|
|
8
54
|
|
|
9
55
|
### Maintenance
|
|
10
56
|
|
|
11
57
|
- bump version to 0.1.6 - add LuForm validation orchestrator (`1f11734`)
|
|
12
58
|
|
|
13
|
-
|
|
14
59
|
---
|
|
15
60
|
|
|
16
61
|
## [0.1.6] — 2026-04-26
|
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrake/lumora-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Headless Vue 3 component framework for three surface targets — Mobile, Desktop, and Embedded — with a unified --lu-* design token system.",
|
|
5
5
|
"author": "Anuvab Chakraborty (https://github.com/madlybong)",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./src/index.ts",
|
|
10
|
+
"./style": "./src/lumora.css",
|
|
11
|
+
"./tailwind": "./src/tailwind.ts",
|
|
10
12
|
"./layout": "./src/layout/index.ts",
|
|
11
13
|
"./shell": "./src/shell/index.ts",
|
|
12
14
|
"./components": "./src/components/index.ts",
|
|
@@ -56,6 +58,12 @@
|
|
|
56
58
|
"check": "vue-tsc -p ./tsconfig.json"
|
|
57
59
|
},
|
|
58
60
|
"peerDependencies": {
|
|
61
|
+
"tailwindcss": "^4.0.0",
|
|
59
62
|
"vue": "^3.5.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"clsx": "^2.1.1",
|
|
66
|
+
"shiki": "^4.0.2",
|
|
67
|
+
"tailwind-merge": "^3.5.0"
|
|
60
68
|
}
|
|
61
69
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedSkin" role="alert">
|
|
3
|
+
<div v-if="$slots.icon || icon" :class="resolvedIconWrapperSkin">
|
|
4
|
+
<slot name="icon">
|
|
5
|
+
<LuIcon v-if="icon" :name="icon" />
|
|
6
|
+
</slot>
|
|
7
|
+
</div>
|
|
8
|
+
<div :class="resolvedContentSkin">
|
|
9
|
+
<slot />
|
|
10
|
+
</div>
|
|
11
|
+
<div v-if="$slots.action" :class="resolvedActionSkin">
|
|
12
|
+
<slot name="action" />
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed } from "vue";
|
|
19
|
+
import { useLumoraConfig } from "../context";
|
|
20
|
+
import LuIcon from "./LuIcon.vue";
|
|
21
|
+
|
|
22
|
+
const props = defineProps<{
|
|
23
|
+
variant?: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const { resolveSkin } = useLumoraConfig();
|
|
28
|
+
|
|
29
|
+
const resolvedSkin = computed(() => resolveSkin("LuAlert", props.variant));
|
|
30
|
+
const resolvedIconWrapperSkin = computed(() => resolveSkin("LuAlertIcon", props.variant));
|
|
31
|
+
const resolvedContentSkin = computed(() => resolveSkin("LuAlertContent", props.variant));
|
|
32
|
+
const resolvedActionSkin = computed(() => resolveSkin("LuAlertAction", props.variant));
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<nav aria-label="breadcrumb">
|
|
3
|
+
<ol :class="resolvedSkin">
|
|
4
|
+
<li v-for="(item, index) in items" :key="index" :class="resolvedItemSkin">
|
|
5
|
+
<LuLink
|
|
6
|
+
v-if="item.href || item.to"
|
|
7
|
+
:href="item.href"
|
|
8
|
+
:to="item.to"
|
|
9
|
+
:variant="item.current ? 'nav-active' : 'nav'"
|
|
10
|
+
:aria-current="item.current ? 'page' : undefined"
|
|
11
|
+
:class="resolvedLinkSkin"
|
|
12
|
+
>
|
|
13
|
+
<LuIcon v-if="item.icon" :name="item.icon" class="mr-2 h-4 w-4" />
|
|
14
|
+
{{ item.label }}
|
|
15
|
+
</LuLink>
|
|
16
|
+
<span
|
|
17
|
+
v-else
|
|
18
|
+
:class="resolvedPageSkin"
|
|
19
|
+
:aria-current="item.current ? 'page' : undefined"
|
|
20
|
+
>
|
|
21
|
+
<LuIcon v-if="item.icon" :name="item.icon" class="mr-2 h-4 w-4" />
|
|
22
|
+
{{ item.label }}
|
|
23
|
+
</span>
|
|
24
|
+
<LuIcon
|
|
25
|
+
v-if="index < items.length - 1"
|
|
26
|
+
:name="separatorIcon"
|
|
27
|
+
:class="resolvedSeparatorSkin"
|
|
28
|
+
/>
|
|
29
|
+
</li>
|
|
30
|
+
</ol>
|
|
31
|
+
</nav>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { computed } from "vue";
|
|
36
|
+
import { useLumoraConfig } from "../context";
|
|
37
|
+
import LuLink from "./LuLink.vue";
|
|
38
|
+
import LuIcon from "./LuIcon.vue";
|
|
39
|
+
|
|
40
|
+
export interface LuBreadcrumbItem {
|
|
41
|
+
label: string;
|
|
42
|
+
href?: string;
|
|
43
|
+
to?: any;
|
|
44
|
+
current?: boolean;
|
|
45
|
+
icon?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const props = withDefaults(defineProps<{
|
|
49
|
+
items: LuBreadcrumbItem[];
|
|
50
|
+
variant?: string;
|
|
51
|
+
separatorIcon?: string;
|
|
52
|
+
}>(), {
|
|
53
|
+
separatorIcon: 'chevron-right'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { resolveSkin } = useLumoraConfig();
|
|
57
|
+
|
|
58
|
+
const resolvedSkin = computed(() => resolveSkin("LuBreadcrumb", props.variant));
|
|
59
|
+
const resolvedItemSkin = computed(() => resolveSkin("LuBreadcrumbItem", props.variant));
|
|
60
|
+
const resolvedLinkSkin = computed(() => resolveSkin("LuBreadcrumbLink", props.variant));
|
|
61
|
+
const resolvedPageSkin = computed(() => resolveSkin("LuBreadcrumbPage", props.variant));
|
|
62
|
+
const resolvedSeparatorSkin = computed(() => resolveSkin("LuBreadcrumbSeparator", props.variant));
|
|
63
|
+
</script>
|
|
@@ -7,14 +7,21 @@
|
|
|
7
7
|
<script setup lang="ts">
|
|
8
8
|
import { computed } from "vue";
|
|
9
9
|
import { useLumoraConfig } from "../context";
|
|
10
|
+
import { resolveLayoutProps, cn } from "../utils";
|
|
10
11
|
|
|
11
12
|
const props = withDefaults(defineProps<{
|
|
12
13
|
variant?: string;
|
|
13
14
|
as?: string;
|
|
15
|
+
padding?: string | number;
|
|
16
|
+
width?: string;
|
|
17
|
+
height?: string;
|
|
14
18
|
}>(), {
|
|
15
19
|
as: "div"
|
|
16
20
|
});
|
|
17
21
|
|
|
18
22
|
const { resolveSkin } = useLumoraConfig();
|
|
19
|
-
const resolvedSkin = computed(() =>
|
|
23
|
+
const resolvedSkin = computed(() => cn(
|
|
24
|
+
resolveSkin("LuCard", props.variant),
|
|
25
|
+
resolveLayoutProps(props)
|
|
26
|
+
));
|
|
20
27
|
</script>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedContainerSkin">
|
|
3
|
+
<input
|
|
4
|
+
type="checkbox"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
:class="resolvedSkin"
|
|
7
|
+
:checked="modelValue"
|
|
8
|
+
:name="name"
|
|
9
|
+
:disabled="formContext?.disabled.value"
|
|
10
|
+
@change="onChange"
|
|
11
|
+
@blur="onBlur"
|
|
12
|
+
/>
|
|
13
|
+
<label v-if="$slots.default || label" :class="resolvedLabelSkin" @click.prevent="toggle">
|
|
14
|
+
<slot>{{ label }}</slot>
|
|
15
|
+
</label>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, inject, onMounted, onUnmounted, ref, watch } from "vue";
|
|
21
|
+
import { useLumoraConfig } from "../context";
|
|
22
|
+
import { LuFormContextKey } from "./LuForm.types";
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
modelValue?: boolean;
|
|
26
|
+
variant?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
error?: string | null;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: "update:modelValue", value: boolean): void;
|
|
34
|
+
(e: "change", value: boolean): void;
|
|
35
|
+
(e: "blur"): void;
|
|
36
|
+
}>();
|
|
37
|
+
|
|
38
|
+
const { resolveSkin } = useLumoraConfig();
|
|
39
|
+
const resolvedContainerSkin = computed(() => resolveSkin("LuCheckboxContainer", props.variant));
|
|
40
|
+
const resolvedSkin = computed(() => resolveSkin("LuCheckbox", props.variant));
|
|
41
|
+
const resolvedLabelSkin = computed(() => resolveSkin("LuCheckboxLabel", props.variant));
|
|
42
|
+
|
|
43
|
+
const formContext = inject(LuFormContextKey, null);
|
|
44
|
+
const internalValue = ref<boolean>(!!props.modelValue);
|
|
45
|
+
|
|
46
|
+
watch(() => props.modelValue, (newVal) => {
|
|
47
|
+
internalValue.value = !!newVal;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const onChange = (event: Event) => {
|
|
51
|
+
const target = event.target as HTMLInputElement;
|
|
52
|
+
const value = target.checked;
|
|
53
|
+
internalValue.value = value;
|
|
54
|
+
emit("update:modelValue", value);
|
|
55
|
+
emit("change", value);
|
|
56
|
+
|
|
57
|
+
if (props.name && formContext && (formContext.validateOn.value === "blur" || formContext.validateOn.value === "both")) {
|
|
58
|
+
// trigger validation handled by parent
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const toggle = () => {
|
|
63
|
+
if (formContext?.disabled.value || ("disabled" in props && (props as any).disabled !== false)) return;
|
|
64
|
+
const newValue = !internalValue.value;
|
|
65
|
+
internalValue.value = newValue;
|
|
66
|
+
emit("update:modelValue", newValue);
|
|
67
|
+
emit("change", newValue);
|
|
68
|
+
|
|
69
|
+
if (props.name && formContext && (formContext.validateOn.value === "blur" || formContext.validateOn.value === "both")) {
|
|
70
|
+
// trigger validation handled by parent
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const onBlur = () => {
|
|
75
|
+
if (props.name && formContext && (formContext.validateOn.value === "blur" || formContext.validateOn.value === "both")) {
|
|
76
|
+
// trigger validation handled by parent
|
|
77
|
+
}
|
|
78
|
+
emit("blur");
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
onMounted(() => {
|
|
82
|
+
if (!props.name || !formContext) return;
|
|
83
|
+
formContext.register({
|
|
84
|
+
name: props.name,
|
|
85
|
+
getValue: () => internalValue.value,
|
|
86
|
+
setValue: (v) => { internalValue.value = !!v; },
|
|
87
|
+
setError: (_msg) => {},
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
onUnmounted(() => {
|
|
92
|
+
if (props.name && formContext) formContext.unregister(props.name);
|
|
93
|
+
});
|
|
94
|
+
</script>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-bind="$attrs" :class="resolvedSkin.container">
|
|
3
|
+
<div v-if="title || description" :class="resolvedSkin.header">
|
|
4
|
+
<h3 v-if="title" :class="resolvedSkin.title">{{ title }}</h3>
|
|
5
|
+
<p v-if="description" :class="resolvedSkin.description">{{ description }}</p>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<template v-if="variant === 'preview'">
|
|
9
|
+
<div :class="resolvedSkin.card">
|
|
10
|
+
<template v-if="layout === 'tabbed'">
|
|
11
|
+
<LuTabs v-model="activeTab" variant="default">
|
|
12
|
+
<LuTabList variant="card-header">
|
|
13
|
+
<LuTab value="preview">Preview</LuTab>
|
|
14
|
+
<LuTab value="code">Code</LuTab>
|
|
15
|
+
</LuTabList>
|
|
16
|
+
</LuTabs>
|
|
17
|
+
|
|
18
|
+
<div v-if="activeTab === 'preview'" :class="resolvedSkin.previewArea">
|
|
19
|
+
<slot name="preview" />
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div v-if="activeTab === 'code'" :class="resolvedSkin.codeArea">
|
|
23
|
+
<div :class="resolvedSkin.codeHeader">
|
|
24
|
+
<div :class="resolvedSkin.badge">{{ lang }}</div>
|
|
25
|
+
<button @click="copyCode" :class="resolvedSkin.copyButton" aria-label="Copy code">
|
|
26
|
+
<LuIcon :name="copied ? 'check' : 'copy'" class="w-4 h-4" />
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
<div :class="resolvedSkin.codeContent" v-html="html"></div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<template v-else-if="layout === 'split'">
|
|
34
|
+
<div :class="resolvedSkin.splitContainer">
|
|
35
|
+
<div :class="resolvedSkin.previewArea">
|
|
36
|
+
<slot name="preview" />
|
|
37
|
+
</div>
|
|
38
|
+
<div :class="resolvedSkin.splitCodeArea">
|
|
39
|
+
<div :class="resolvedSkin.codeHeader">
|
|
40
|
+
<div :class="resolvedSkin.badge">{{ lang }}</div>
|
|
41
|
+
<button @click="copyCode" :class="resolvedSkin.copyButton" aria-label="Copy code">
|
|
42
|
+
<LuIcon :name="copied ? 'check' : 'copy'" class="w-4 h-4" />
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
<div :class="resolvedSkin.codeContent" v-html="html"></div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<template v-else>
|
|
53
|
+
<div :class="resolvedSkin.card">
|
|
54
|
+
<div :class="resolvedSkin.codeArea">
|
|
55
|
+
<div :class="resolvedSkin.codeHeader">
|
|
56
|
+
<div :class="resolvedSkin.badge">{{ lang }}</div>
|
|
57
|
+
<button @click="copyCode" :class="resolvedSkin.copyButton" aria-label="Copy code">
|
|
58
|
+
<LuIcon :name="copied ? 'check' : 'copy'" class="w-4 h-4" />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
<div :class="resolvedSkin.codeContent" v-html="html"></div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script lang="ts">
|
|
69
|
+
import { createHighlighter, type Highlighter } from 'shiki';
|
|
70
|
+
|
|
71
|
+
let globalHighlighter: Highlighter | null = null;
|
|
72
|
+
let highlighterPromise: Promise<Highlighter> | null = null;
|
|
73
|
+
|
|
74
|
+
async function getHighlighter() {
|
|
75
|
+
if (globalHighlighter) return globalHighlighter;
|
|
76
|
+
if (!highlighterPromise) {
|
|
77
|
+
highlighterPromise = createHighlighter({
|
|
78
|
+
themes: ['one-dark-pro'],
|
|
79
|
+
langs: ['bash', 'vue', 'ts', 'html', 'css', 'json']
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
globalHighlighter = await highlighterPromise;
|
|
83
|
+
return globalHighlighter;
|
|
84
|
+
}
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<script setup lang="ts">
|
|
88
|
+
import { ref, watch, onMounted, computed } from "vue";
|
|
89
|
+
import { useLumoraConfig } from "../context";
|
|
90
|
+
import LuTabs from "./LuTabs.vue";
|
|
91
|
+
import LuTabList from "./LuTabList.vue";
|
|
92
|
+
import LuTab from "./LuTab.vue";
|
|
93
|
+
import LuIcon from "./LuIcon.vue";
|
|
94
|
+
|
|
95
|
+
const props = withDefaults(defineProps<{
|
|
96
|
+
code: string;
|
|
97
|
+
lang?: string;
|
|
98
|
+
variant?: "default" | "preview";
|
|
99
|
+
layout?: "tabbed" | "split";
|
|
100
|
+
title?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
}>(), {
|
|
103
|
+
lang: "vue",
|
|
104
|
+
variant: "default",
|
|
105
|
+
layout: "tabbed"
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { resolveSkin } = useLumoraConfig();
|
|
109
|
+
const resolvedSkin = computed(() => {
|
|
110
|
+
return {
|
|
111
|
+
container: resolveSkin("LuCodeBlock", "container"),
|
|
112
|
+
header: resolveSkin("LuCodeBlock", "header"),
|
|
113
|
+
title: resolveSkin("LuCodeBlock", "title"),
|
|
114
|
+
description: resolveSkin("LuCodeBlock", "description"),
|
|
115
|
+
card: resolveSkin("LuCodeBlock", "card"),
|
|
116
|
+
previewArea: resolveSkin("LuCodeBlock", "previewArea"),
|
|
117
|
+
codeArea: resolveSkin("LuCodeBlock", "codeArea"),
|
|
118
|
+
splitCodeArea: resolveSkin("LuCodeBlock", "splitCodeArea"),
|
|
119
|
+
splitContainer: resolveSkin("LuCodeBlock", "splitContainer"),
|
|
120
|
+
codeHeader: resolveSkin("LuCodeBlock", "codeHeader"),
|
|
121
|
+
badge: resolveSkin("LuCodeBlock", "badge"),
|
|
122
|
+
copyButton: resolveSkin("LuCodeBlock", "copyButton"),
|
|
123
|
+
codeContent: resolveSkin("LuCodeBlock", "codeContent"),
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const html = ref('');
|
|
128
|
+
const activeTab = ref<'preview' | 'code'>('preview');
|
|
129
|
+
const copied = ref(false);
|
|
130
|
+
|
|
131
|
+
const highlight = async () => {
|
|
132
|
+
if (!props.code) {
|
|
133
|
+
html.value = '';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const highlighter = await getHighlighter();
|
|
139
|
+
|
|
140
|
+
html.value = highlighter.codeToHtml(props.code, {
|
|
141
|
+
lang: props.lang || 'vue',
|
|
142
|
+
theme: 'one-dark-pro'
|
|
143
|
+
});
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('Failed to highlight code', e);
|
|
146
|
+
// fallback to plain pre
|
|
147
|
+
html.value = `<pre><code>${props.code.replace(/</g, '<').replace(/>/g, '>')}</code></pre>`;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
onMounted(() => {
|
|
152
|
+
highlight();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
watch(() => [props.code, props.lang], () => {
|
|
156
|
+
highlight();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const copyCode = async () => {
|
|
160
|
+
try {
|
|
161
|
+
await navigator.clipboard.writeText(props.code);
|
|
162
|
+
copied.value = true;
|
|
163
|
+
setTimeout(() => copied.value = false, 2000);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error('Failed to copy', err);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
</script>
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<div class="relative w-full" v-if="$slots.prepend || $slots.append">
|
|
3
|
+
<div v-if="$slots.prepend" :class="prependSkin">
|
|
4
|
+
<slot name="prepend" />
|
|
5
|
+
</div>
|
|
6
|
+
<input
|
|
7
|
+
v-bind="$attrs"
|
|
8
|
+
:class="[resolvedSkin, $slots.prepend && 'pl-9', $slots.append && 'pr-9']"
|
|
9
|
+
:value="modelValue"
|
|
10
|
+
:name="name"
|
|
11
|
+
:disabled="formContext?.disabled.value"
|
|
12
|
+
@input="onInput"
|
|
13
|
+
@blur="onBlur"
|
|
14
|
+
/>
|
|
15
|
+
<div v-if="$slots.append" :class="appendSkin">
|
|
16
|
+
<slot name="append" />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
2
19
|
<input
|
|
20
|
+
v-else
|
|
3
21
|
v-bind="$attrs"
|
|
4
22
|
:class="resolvedSkin"
|
|
5
23
|
:value="modelValue"
|
|
@@ -29,6 +47,8 @@ const emit = defineEmits<{
|
|
|
29
47
|
|
|
30
48
|
const { resolveSkin } = useLumoraConfig();
|
|
31
49
|
const resolvedSkin = computed(() => resolveSkin("LuInput", props.variant));
|
|
50
|
+
const prependSkin = computed(() => resolveSkin("LuInputPrepend", props.variant));
|
|
51
|
+
const appendSkin = computed(() => resolveSkin("LuInputAppend", props.variant));
|
|
32
52
|
|
|
33
53
|
const formContext = inject(LuFormContextKey, null);
|
|
34
54
|
const internalValue = ref<string | number | undefined>(props.modelValue);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="resolvedSkin" ref="dropdownRef">
|
|
3
|
+
<div @click="toggle" :class="resolvedTriggerSkin" aria-haspopup="true" :aria-expanded="isOpen">
|
|
4
|
+
<slot name="trigger">
|
|
5
|
+
<LuButton variant="default">Options <LuIcon name="chevron-down" class="ml-2 h-4 w-4" /></LuButton>
|
|
6
|
+
</slot>
|
|
7
|
+
</div>
|
|
8
|
+
<transition
|
|
9
|
+
enter-active-class="transition ease-out duration-100"
|
|
10
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
11
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
12
|
+
leave-active-class="transition ease-in duration-75"
|
|
13
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
14
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
15
|
+
>
|
|
16
|
+
<div v-if="isOpen" :class="[resolvedContentSkin, alignClass]">
|
|
17
|
+
<div :class="resolvedGroupSkin" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</transition>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
|
27
|
+
import { useLumoraConfig } from "../context";
|
|
28
|
+
import LuButton from "./LuButton.vue";
|
|
29
|
+
import LuIcon from "./LuIcon.vue";
|
|
30
|
+
|
|
31
|
+
const props = withDefaults(defineProps<{
|
|
32
|
+
variant?: string;
|
|
33
|
+
align?: 'left' | 'right';
|
|
34
|
+
}>(), {
|
|
35
|
+
align: 'left'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{
|
|
39
|
+
(e: "open"): void;
|
|
40
|
+
(e: "close"): void;
|
|
41
|
+
}>();
|
|
42
|
+
|
|
43
|
+
const isOpen = ref(false);
|
|
44
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
45
|
+
|
|
46
|
+
const { resolveSkin } = useLumoraConfig();
|
|
47
|
+
|
|
48
|
+
const resolvedSkin = computed(() => resolveSkin("LuMenu", props.variant));
|
|
49
|
+
const resolvedTriggerSkin = computed(() => resolveSkin("LuMenuTrigger", props.variant));
|
|
50
|
+
const resolvedContentSkin = computed(() => resolveSkin("LuMenuContent", props.variant));
|
|
51
|
+
const resolvedGroupSkin = computed(() => resolveSkin("LuMenuGroup", props.variant));
|
|
52
|
+
|
|
53
|
+
const alignClass = computed(() => {
|
|
54
|
+
return props.align === 'right' ? 'right-0 origin-top-right' : 'left-0 origin-top-left';
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const toggle = () => {
|
|
58
|
+
isOpen.value = !isOpen.value;
|
|
59
|
+
if (isOpen.value) {
|
|
60
|
+
emit("open");
|
|
61
|
+
} else {
|
|
62
|
+
emit("close");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const close = () => {
|
|
67
|
+
if (isOpen.value) {
|
|
68
|
+
isOpen.value = false;
|
|
69
|
+
emit("close");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
74
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
75
|
+
close();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
onMounted(() => {
|
|
80
|
+
document.addEventListener('click', handleClickOutside);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
onBeforeUnmount(() => {
|
|
84
|
+
document.removeEventListener('click', handleClickOutside);
|
|
85
|
+
});
|
|
86
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
:class="resolvedSkin"
|
|
5
|
+
role="menuitem"
|
|
6
|
+
:disabled="disabled"
|
|
7
|
+
:data-disabled="disabled ? '' : undefined"
|
|
8
|
+
@click="onClick"
|
|
9
|
+
>
|
|
10
|
+
<slot />
|
|
11
|
+
</button>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed } from "vue";
|
|
16
|
+
import { useLumoraConfig } from "../context";
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
variant?: string;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
const emit = defineEmits<{
|
|
24
|
+
(e: "click", event: MouseEvent): void;
|
|
25
|
+
}>();
|
|
26
|
+
|
|
27
|
+
const { resolveSkin } = useLumoraConfig();
|
|
28
|
+
const resolvedSkin = computed(() => resolveSkin("LuMenuItem", props.variant));
|
|
29
|
+
|
|
30
|
+
const onClick = (event: MouseEvent) => {
|
|
31
|
+
if (props.disabled) {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
emit("click", event);
|
|
36
|
+
};
|
|
37
|
+
</script>
|