@leavepulse/ui 0.22.22 → 0.24.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/dist/chunks/{LpAddressInput-peTpNSJK.js → LpAddressInput-BTqnUXZG.js} +1 -1
- package/dist/chunks/{LpAvatar-B1Ez0ZAx.js → LpAvatar-CzCE5cXg.js} +30 -8
- package/dist/chunks/LpBreadcrumbs-5uvViCXi.js +60 -0
- package/dist/chunks/{LpConfirmDialog-DOU_zaTI.js → LpConfirmDialog-CObRah9c.js} +10 -5
- package/dist/chunks/LpEmptyState-BV3q7Qyi.js +49 -0
- package/dist/chunks/{LpFileTree-Cw50eG_y.js → LpFileTree-nU1wRRxZ.js} +2 -2
- package/dist/chunks/{LpFileTreeNode-DFg4PhOK.js → LpFileTreeNode-D7WgUPcJ.js} +23 -21
- package/dist/chunks/{LpInput-BfLIWV-P.js → LpInput-jwqXlAy0.js} +5 -2
- package/dist/chunks/LpLink-Bu01SZzB.js +51 -0
- package/dist/chunks/{LpNotificationBell-a08cl0yY.js → LpNotificationBell-X1M30e0X.js} +1 -1
- package/dist/chunks/{LpPasswordInput-BfjDrvZq.js → LpPasswordInput-Bmo6q9Kd.js} +11 -3
- package/dist/chunks/{LpStepper-5dFTrW21.js → LpStepper-Wtl-6nUh.js} +27 -10
- package/dist/chunks/{LpTextarea-BDl7PR9U.js → LpTextarea-1ENb3ZAO.js} +6 -3
- package/dist/chunks/{LpThemeSwitcher-B_kj0T2b.js → LpThemeSwitcher-BgpJgrnP.js} +27 -0
- package/dist/components/LpAddressInput.vue.js +1 -1
- package/dist/components/LpAvatar.vue.d.ts +17 -3
- package/dist/components/LpAvatar.vue.js +1 -1
- package/dist/components/LpBreadcrumbs.vue.d.ts +13 -1
- package/dist/components/LpBreadcrumbs.vue.js +1 -1
- package/dist/components/LpConfirmDialog.vue.d.ts +8 -0
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpEmptyState.vue.d.ts +5 -0
- package/dist/components/LpEmptyState.vue.js +1 -1
- package/dist/components/LpFileTree.vue.js +1 -1
- package/dist/components/LpFileTreeNode.vue.js +1 -1
- package/dist/components/LpInput.vue.d.ts +12 -0
- package/dist/components/LpInput.vue.js +1 -1
- package/dist/components/LpLink.vue.d.ts +24 -4
- package/dist/components/LpLink.vue.js +1 -1
- package/dist/components/LpNotificationBell.vue.js +1 -1
- package/dist/components/LpPasswordInput.vue.d.ts +8 -0
- package/dist/components/LpPasswordInput.vue.js +1 -1
- package/dist/components/LpStepper.vue.d.ts +15 -1
- package/dist/components/LpStepper.vue.js +1 -1
- package/dist/components/LpTextarea.vue.d.ts +5 -0
- package/dist/components/LpTextarea.vue.js +1 -1
- package/dist/components/LpThemeSwitcher.vue.js +1 -1
- package/dist/index.js +14 -14
- package/dist/theme/tokens.d.ts +3 -0
- package/package.json +1 -1
- package/src/components/LpAvatar.vue +33 -5
- package/src/components/LpBreadcrumbs.vue +30 -7
- package/src/components/LpConfirmDialog.vue +21 -6
- package/src/components/LpEmptyState.vue +18 -5
- package/src/components/LpFileTreeNode.vue +11 -2
- package/src/components/LpInput.vue +8 -0
- package/src/components/LpLink.vue +48 -10
- package/src/components/LpPasswordInput.vue +16 -1
- package/src/components/LpStepper.vue +49 -9
- package/src/components/LpTextarea.vue +7 -0
- package/src/theme/presets.ts +24 -0
- package/src/theme/tokens.ts +6 -0
- package/src/tokens/tokens.css +3 -0
- package/dist/chunks/LpBreadcrumbs-B7Aqatzw.js +0 -42
- package/dist/chunks/LpEmptyState-CrfyRJUG.js +0 -47
- package/dist/chunks/LpLink-DHwOEp4e.js +0 -36
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { Component } from "vue"
|
|
2
3
|
import LpIcon from "./LpIcon.vue"
|
|
3
4
|
|
|
4
5
|
export interface Crumb {
|
|
5
6
|
label: string
|
|
6
7
|
href?: string
|
|
8
|
+
/** Destination for router links; passed straight to the `as` component. */
|
|
9
|
+
to?: string | Record<string, unknown>
|
|
10
|
+
/** Leading icon, for crumbs that carry a glyph (a home root, a section). */
|
|
11
|
+
icon?: string
|
|
7
12
|
}
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
items: Crumb[]
|
|
17
|
+
/**
|
|
18
|
+
* Root element/component for linked crumbs. Defaults to a native <a>;
|
|
19
|
+
* pass a router link component to keep client-side navigation.
|
|
20
|
+
*/
|
|
21
|
+
as?: string | Component
|
|
22
|
+
}>(),
|
|
23
|
+
{ as: "a" },
|
|
24
|
+
)
|
|
10
25
|
defineEmits<{ (e: "navigate", item: Crumb, index: number): void }>()
|
|
11
26
|
</script>
|
|
12
27
|
|
|
@@ -19,15 +34,23 @@ defineEmits<{ (e: "navigate", item: Crumb, index: number): void }>()
|
|
|
19
34
|
:size="14"
|
|
20
35
|
class="text-muted"
|
|
21
36
|
/>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
<component
|
|
38
|
+
:is="as"
|
|
39
|
+
v-if="(item.href || item.to) && i < items.length - 1"
|
|
40
|
+
:href="as === 'a' ? item.href : undefined"
|
|
41
|
+
:to="as === 'a' ? undefined : item.to"
|
|
42
|
+
class="inline-flex items-center gap-1.5 rounded-xs text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring"
|
|
26
43
|
@click="$emit('navigate', item, i)"
|
|
27
44
|
>
|
|
45
|
+
<LpIcon v-if="item.icon" :name="item.icon" :size="14" />
|
|
28
46
|
{{ item.label }}
|
|
29
|
-
</
|
|
30
|
-
<span
|
|
47
|
+
</component>
|
|
48
|
+
<span
|
|
49
|
+
v-else
|
|
50
|
+
class="inline-flex items-center gap-1.5"
|
|
51
|
+
:class="i === items.length - 1 ? 'text-ink' : 'text-muted'"
|
|
52
|
+
>
|
|
53
|
+
<LpIcon v-if="item.icon" :name="item.icon" :size="14" />
|
|
31
54
|
{{ item.label }}
|
|
32
55
|
</span>
|
|
33
56
|
</template>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import LpButton from "./LpButton.vue"
|
|
4
4
|
import LpModal from "./LpModal.vue"
|
|
5
5
|
|
|
6
|
-
withDefaults(
|
|
6
|
+
const props = withDefaults(
|
|
7
7
|
defineProps<{
|
|
8
8
|
open?: boolean
|
|
9
9
|
title?: string
|
|
@@ -11,8 +11,15 @@ withDefaults(
|
|
|
11
11
|
confirmLabel?: string
|
|
12
12
|
cancelLabel?: string
|
|
13
13
|
danger?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* The confirm action is in flight: the button disables and swaps to
|
|
16
|
+
* `loadingLabel`, and confirming no longer closes the dialog — the owner
|
|
17
|
+
* closes it once the work settles, so a failure can stay on screen.
|
|
18
|
+
*/
|
|
19
|
+
loading?: boolean
|
|
20
|
+
loadingLabel?: string
|
|
14
21
|
}>(),
|
|
15
|
-
{ confirmLabel: "Confirm", cancelLabel: "Cancel" },
|
|
22
|
+
{ confirmLabel: "Confirm", cancelLabel: "Cancel", loadingLabel: "Working…" },
|
|
16
23
|
)
|
|
17
24
|
|
|
18
25
|
const emit = defineEmits<{
|
|
@@ -23,7 +30,9 @@ const emit = defineEmits<{
|
|
|
23
30
|
|
|
24
31
|
function confirm() {
|
|
25
32
|
emit("confirm")
|
|
26
|
-
|
|
33
|
+
// An async confirm owns its own dismissal; closing here would tear the
|
|
34
|
+
// dialog down before the caller knows whether the action succeeded.
|
|
35
|
+
if (!props.loading) emit("update:open", false)
|
|
27
36
|
}
|
|
28
37
|
function cancel() {
|
|
29
38
|
emit("cancel")
|
|
@@ -40,9 +49,15 @@ function cancel() {
|
|
|
40
49
|
>
|
|
41
50
|
<slot />
|
|
42
51
|
<template #footer>
|
|
43
|
-
<LpButton variant="ghost" @click="cancel">
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
<LpButton variant="ghost" :disabled="loading" @click="cancel">
|
|
53
|
+
{{ cancelLabel }}
|
|
54
|
+
</LpButton>
|
|
55
|
+
<LpButton
|
|
56
|
+
:variant="danger ? 'danger' : 'solid'"
|
|
57
|
+
:disabled="loading"
|
|
58
|
+
@click="confirm"
|
|
59
|
+
>
|
|
60
|
+
{{ loading ? loadingLabel : confirmLabel }}
|
|
46
61
|
</LpButton>
|
|
47
62
|
</template>
|
|
48
63
|
</LpModal>
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import LpIcon from "./LpIcon.vue"
|
|
3
3
|
|
|
4
|
-
defineProps<{
|
|
4
|
+
defineProps<{
|
|
5
|
+
icon?: string
|
|
6
|
+
title: string
|
|
7
|
+
description?: string
|
|
8
|
+
/**
|
|
9
|
+
* Tightens the block for empty states nested inside a card or a panel,
|
|
10
|
+
* where the full-page padding would dwarf its container.
|
|
11
|
+
*/
|
|
12
|
+
compact?: boolean
|
|
13
|
+
}>()
|
|
5
14
|
</script>
|
|
6
15
|
|
|
7
16
|
<template>
|
|
8
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="flex flex-col items-center px-6 text-center"
|
|
19
|
+
:class="compact ? 'gap-2 py-6' : 'gap-3 py-12'"
|
|
20
|
+
>
|
|
9
21
|
<div
|
|
10
22
|
v-if="icon"
|
|
11
|
-
class="flex
|
|
23
|
+
class="flex items-center justify-center rounded-pill bg-surface-soft text-muted"
|
|
24
|
+
:class="compact ? 'size-9' : 'size-12'"
|
|
12
25
|
>
|
|
13
|
-
<LpIcon :name="icon" :size="22" />
|
|
26
|
+
<LpIcon :name="icon" :size="compact ? 16 : 22" />
|
|
14
27
|
</div>
|
|
15
28
|
<div class="flex flex-col gap-1">
|
|
16
|
-
<p class="font-semibold text-ink">{{ title }}</p>
|
|
29
|
+
<p class="font-semibold text-ink" :class="compact ? 'text-sm' : ''">{{ title }}</p>
|
|
17
30
|
<p v-if="description" class="max-w-sm text-sm text-muted">{{ description }}</p>
|
|
18
31
|
</div>
|
|
19
32
|
<div v-if="$slots.default" class="mt-1"><slot /></div>
|
|
@@ -178,8 +178,17 @@ function onActivate() {
|
|
|
178
178
|
@click.stop="onActivate"
|
|
179
179
|
>
|
|
180
180
|
<!-- Chevron sits in a fixed-width slot so names line up whether or not
|
|
181
|
-
the row can expand. Leaves get the same gutter, left empty.
|
|
182
|
-
|
|
181
|
+
the row can expand. Leaves get the same gutter, left empty.
|
|
182
|
+
|
|
183
|
+
It is its own hit target, like the checkbox: opening a folder to
|
|
184
|
+
see what is in it is a different intent from selecting it, and on
|
|
185
|
+
a tree that drives a details pane the two must not be the same
|
|
186
|
+
click. The row as a whole still does both. -->
|
|
187
|
+
<span
|
|
188
|
+
class="flex size-4 shrink-0 items-center justify-center"
|
|
189
|
+
:class="canExpand ? 'cursor-pointer rounded-sm hover:bg-line' : ''"
|
|
190
|
+
@click.stop="canExpand && !node.disabled ? emit('toggle', node) : undefined"
|
|
191
|
+
>
|
|
183
192
|
<LpIcon
|
|
184
193
|
v-if="isLoading"
|
|
185
194
|
name="lucide:loader-circle"
|
|
@@ -54,6 +54,12 @@ const props = withDefaults(
|
|
|
54
54
|
hint?: string
|
|
55
55
|
error?: string
|
|
56
56
|
icon?: string
|
|
57
|
+
/**
|
|
58
|
+
* Marks the label with the required indicator. The attribute still reaches
|
|
59
|
+
* the underlying <input> for native validation; this only makes the field
|
|
60
|
+
* read as required.
|
|
61
|
+
*/
|
|
62
|
+
required?: boolean
|
|
57
63
|
/**
|
|
58
64
|
* RegExp/string the whole value must match to be valid. Validation only —
|
|
59
65
|
* never blocks typing; a non-matching non-empty value shows the invalid
|
|
@@ -118,6 +124,7 @@ defineExpose({
|
|
|
118
124
|
:label="hasField ? label : undefined"
|
|
119
125
|
:hint="hasField ? hint : undefined"
|
|
120
126
|
:error="hasField ? error : undefined"
|
|
127
|
+
:required="hasField ? required : undefined"
|
|
121
128
|
:class="hasField ? rootClass : undefined"
|
|
122
129
|
:style="hasField ? rootStyle : undefined"
|
|
123
130
|
>
|
|
@@ -136,6 +143,7 @@ defineExpose({
|
|
|
136
143
|
:value="modelValue"
|
|
137
144
|
:placeholder="placeholder"
|
|
138
145
|
:disabled="disabled"
|
|
146
|
+
:required="required || undefined"
|
|
139
147
|
:aria-invalid="isInvalidState || undefined"
|
|
140
148
|
:class="[
|
|
141
149
|
'lp-autofill min-w-0 flex-1 self-stretch bg-transparent outline-none placeholder:text-muted',
|
|
@@ -1,28 +1,61 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from "vue"
|
|
2
|
+
import { computed, type Component } from "vue"
|
|
3
3
|
|
|
4
4
|
const props = withDefaults(
|
|
5
5
|
defineProps<{
|
|
6
6
|
href?: string
|
|
7
7
|
external?: boolean
|
|
8
|
+
/** Shorthand for `tone="muted"`. */
|
|
8
9
|
muted?: boolean
|
|
10
|
+
/**
|
|
11
|
+
* "brand" — the accent link colour (default).
|
|
12
|
+
* "ink" — inherits body text colour; for links that sit inside prose or
|
|
13
|
+
* chrome where an accent would shout (breadcrumbs, footers, nav).
|
|
14
|
+
* "muted" / "soft" — progressively quieter, both resolving to ink on hover.
|
|
15
|
+
*/
|
|
16
|
+
tone?: "brand" | "ink" | "muted" | "soft"
|
|
17
|
+
/**
|
|
18
|
+
* Root element/component. Defaults to a native <a>. Pass a router link
|
|
19
|
+
* component (e.g. NuxtLink or RouterLink) to keep client-side navigation;
|
|
20
|
+
* the kit stays DOM-agnostic, the consumer supplies the link component.
|
|
21
|
+
* Router links take their destination through their own prop (`to`), which
|
|
22
|
+
* falls through as a plain attribute.
|
|
23
|
+
*/
|
|
24
|
+
as?: string | Component
|
|
9
25
|
/**
|
|
10
26
|
* "default" — plain hover underline.
|
|
27
|
+
* "underline" — underlined at rest; for links inside running prose,
|
|
28
|
+
* where hover-only underlines leave the link invisible until pointed at.
|
|
11
29
|
* "reveal" — an underline bar that grows in from the left on hover
|
|
12
30
|
* (origin-left scale-x 0→1). The LeavePulse landing "monitoring" link feel;
|
|
13
31
|
* good for inline prose links and nav items that want a bit of motion.
|
|
14
32
|
*/
|
|
15
|
-
variant?: "default" | "reveal"
|
|
33
|
+
variant?: "default" | "underline" | "reveal"
|
|
16
34
|
}>(),
|
|
17
|
-
{ variant: "default" },
|
|
35
|
+
{ variant: "default", as: "a" },
|
|
18
36
|
)
|
|
19
37
|
|
|
38
|
+
// A router link component owns its destination through its own prop (`to`),
|
|
39
|
+
// so `href` is only bound when this really renders an anchor. `target`/`rel`
|
|
40
|
+
// stay in play either way — router links honour them for external targets.
|
|
41
|
+
const isAnchor = computed(() => props.as === "a")
|
|
42
|
+
|
|
20
43
|
const rel = computed(() => (props.external ? "noopener noreferrer" : undefined))
|
|
21
44
|
const target = computed(() => (props.external ? "_blank" : undefined))
|
|
22
45
|
|
|
23
|
-
const colorClass = computed(() =>
|
|
24
|
-
props.muted ? "
|
|
25
|
-
)
|
|
46
|
+
const colorClass = computed(() => {
|
|
47
|
+
const tone = props.tone ?? (props.muted ? "muted" : "brand")
|
|
48
|
+
switch (tone) {
|
|
49
|
+
case "ink":
|
|
50
|
+
return "text-ink"
|
|
51
|
+
case "muted":
|
|
52
|
+
return "text-muted-strong hover:text-ink"
|
|
53
|
+
case "soft":
|
|
54
|
+
return "text-muted hover:text-ink"
|
|
55
|
+
default:
|
|
56
|
+
return "text-brand hover:text-brand-hover"
|
|
57
|
+
}
|
|
58
|
+
})
|
|
26
59
|
|
|
27
60
|
// The reveal bar: an ::after pseudo-element pinned to the baseline, full-width
|
|
28
61
|
// but scaled to 0 on the x-axis from the left, growing to 1 on hover/focus. The
|
|
@@ -33,16 +66,21 @@ const revealClass =
|
|
|
33
66
|
</script>
|
|
34
67
|
|
|
35
68
|
<template>
|
|
36
|
-
<
|
|
37
|
-
:
|
|
69
|
+
<component
|
|
70
|
+
:is="as"
|
|
71
|
+
:href="isAnchor ? href : undefined"
|
|
38
72
|
:target="target"
|
|
39
73
|
:rel="rel"
|
|
40
74
|
class="inline-flex items-center gap-1 underline-offset-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
41
75
|
:class="[
|
|
42
76
|
colorClass,
|
|
43
|
-
variant === 'reveal'
|
|
77
|
+
variant === 'reveal'
|
|
78
|
+
? revealClass
|
|
79
|
+
: variant === 'underline'
|
|
80
|
+
? 'underline decoration-current/40 hover:decoration-current'
|
|
81
|
+
: 'hover:underline',
|
|
44
82
|
]"
|
|
45
83
|
>
|
|
46
84
|
<slot />
|
|
47
|
-
</
|
|
85
|
+
</component>
|
|
48
86
|
</template>
|
|
@@ -9,7 +9,18 @@ import { ref } from "vue"
|
|
|
9
9
|
import LpIcon from "./LpIcon.vue"
|
|
10
10
|
import LpInput from "./LpInput.vue"
|
|
11
11
|
|
|
12
|
-
defineProps<{
|
|
12
|
+
defineProps<{
|
|
13
|
+
modelValue?: string
|
|
14
|
+
placeholder?: string
|
|
15
|
+
invalid?: boolean
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
// Field wrapping mirrors LpInput: passing any of these wraps the control in
|
|
18
|
+
// LpFormField, so a password field labels itself like every other field.
|
|
19
|
+
label?: string
|
|
20
|
+
hint?: string
|
|
21
|
+
error?: string
|
|
22
|
+
required?: boolean
|
|
23
|
+
}>()
|
|
13
24
|
defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
14
25
|
|
|
15
26
|
const visible = ref(false)
|
|
@@ -29,6 +40,10 @@ defineExpose({
|
|
|
29
40
|
:placeholder="placeholder"
|
|
30
41
|
:invalid="invalid"
|
|
31
42
|
:disabled="disabled"
|
|
43
|
+
:label="label"
|
|
44
|
+
:hint="hint"
|
|
45
|
+
:error="error"
|
|
46
|
+
:required="required"
|
|
32
47
|
@update:model-value="(v) => $emit('update:modelValue', v)"
|
|
33
48
|
>
|
|
34
49
|
<template v-if="$slots.leading" #leading><slot name="leading" /></template>
|
|
@@ -1,45 +1,85 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue"
|
|
2
3
|
import LpIcon from "./LpIcon.vue"
|
|
3
4
|
|
|
4
5
|
export interface Step {
|
|
5
6
|
label: string
|
|
7
|
+
/** Stable id, for flows whose steps are addressed by name rather than order. */
|
|
8
|
+
key?: string
|
|
9
|
+
/** Replaces the step number while the step is pending or current. */
|
|
10
|
+
icon?: string
|
|
6
11
|
}
|
|
7
12
|
|
|
8
|
-
defineProps<{
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
steps: Step[]
|
|
15
|
+
/**
|
|
16
|
+
* The active step: an index, or a step `key` for keyed flows.
|
|
17
|
+
*/
|
|
18
|
+
current: number | string
|
|
19
|
+
/**
|
|
20
|
+
* Which steps count as done. Omit it and everything before `current` is
|
|
21
|
+
* complete, which is the linear case. Pass keys (or indices) when a flow can
|
|
22
|
+
* finish steps out of order — a verification that succeeds before its
|
|
23
|
+
* prerequisite is filled in, say.
|
|
24
|
+
*/
|
|
25
|
+
completed?: Array<string | number>
|
|
26
|
+
}>()
|
|
27
|
+
|
|
28
|
+
const currentIndex = computed(() => {
|
|
29
|
+
if (typeof props.current === "number") return props.current
|
|
30
|
+
return props.steps.findIndex((s) => s.key === props.current)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function isCompleted(step: Step, i: number): boolean {
|
|
34
|
+
if (!props.completed) return i < currentIndex.value
|
|
35
|
+
return props.completed.includes(i) || (!!step.key && props.completed.includes(step.key))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isCurrent(i: number): boolean {
|
|
39
|
+
return i === currentIndex.value
|
|
40
|
+
}
|
|
9
41
|
</script>
|
|
10
42
|
|
|
11
43
|
<template>
|
|
12
44
|
<ol class="flex items-center gap-2">
|
|
13
45
|
<li
|
|
14
46
|
v-for="(step, i) in steps"
|
|
15
|
-
:key="i"
|
|
47
|
+
:key="step.key ?? i"
|
|
16
48
|
class="flex flex-1 items-center gap-2 last:flex-none"
|
|
17
|
-
:aria-current="i
|
|
49
|
+
:aria-current="isCurrent(i) ? 'step' : undefined"
|
|
18
50
|
>
|
|
19
51
|
<div class="flex items-center gap-2">
|
|
20
52
|
<span
|
|
21
53
|
class="flex size-7 shrink-0 items-center justify-center rounded-pill border text-xs font-semibold transition-colors"
|
|
22
|
-
:class="i
|
|
54
|
+
:class="isCompleted(step, i)
|
|
23
55
|
? 'border-transparent bg-brand text-ink-inverse'
|
|
24
|
-
: i
|
|
56
|
+
: isCurrent(i)
|
|
25
57
|
? 'border-brand text-brand'
|
|
26
58
|
: 'border-line text-muted'"
|
|
27
59
|
>
|
|
28
|
-
<LpIcon v-if="i
|
|
60
|
+
<LpIcon v-if="isCompleted(step, i)" name="lucide:check" :size="14" />
|
|
61
|
+
<LpIcon v-else-if="step.icon" :name="step.icon" :size="14" />
|
|
29
62
|
<template v-else>{{ i + 1 }}</template>
|
|
30
63
|
</span>
|
|
31
|
-
<span
|
|
64
|
+
<span
|
|
65
|
+
class="text-sm"
|
|
66
|
+
:class="isCompleted(step, i) || isCurrent(i) ? 'text-ink' : 'text-muted'"
|
|
67
|
+
>
|
|
32
68
|
{{ step.label }}
|
|
33
69
|
<!-- Status for screen readers; the colour/check are visual only. -->
|
|
34
70
|
<span class="sr-only">{{
|
|
35
|
-
|
|
71
|
+
isCompleted(step, i)
|
|
72
|
+
? "(completed)"
|
|
73
|
+
: isCurrent(i)
|
|
74
|
+
? "(current step)"
|
|
75
|
+
: ""
|
|
36
76
|
}}</span>
|
|
37
77
|
</span>
|
|
38
78
|
</div>
|
|
39
79
|
<span
|
|
40
80
|
v-if="i < steps.length - 1"
|
|
41
81
|
class="h-px flex-1"
|
|
42
|
-
:class="i
|
|
82
|
+
:class="isCompleted(step, i) ? 'bg-brand' : 'bg-line'"
|
|
43
83
|
/>
|
|
44
84
|
</li>
|
|
45
85
|
</ol>
|
|
@@ -11,6 +11,11 @@ const props = defineProps<{
|
|
|
11
11
|
label?: string
|
|
12
12
|
hint?: string
|
|
13
13
|
error?: string
|
|
14
|
+
/**
|
|
15
|
+
* Marks the label with the required indicator; the attribute still reaches
|
|
16
|
+
* the <textarea> for native validation.
|
|
17
|
+
*/
|
|
18
|
+
required?: boolean
|
|
14
19
|
}>()
|
|
15
20
|
|
|
16
21
|
defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
@@ -24,6 +29,7 @@ const hasField = computed(() => !!props.label || !!props.hint || !!props.error)
|
|
|
24
29
|
:label="hasField ? label : undefined"
|
|
25
30
|
:hint="hasField ? hint : undefined"
|
|
26
31
|
:error="hasField ? error : undefined"
|
|
32
|
+
:required="hasField ? required : undefined"
|
|
27
33
|
:class="hasField ? undefined : 'contents'"
|
|
28
34
|
>
|
|
29
35
|
<textarea
|
|
@@ -31,6 +37,7 @@ const hasField = computed(() => !!props.label || !!props.hint || !!props.error)
|
|
|
31
37
|
:placeholder="placeholder"
|
|
32
38
|
:rows="rows ?? 4"
|
|
33
39
|
:disabled="disabled"
|
|
40
|
+
:required="required || undefined"
|
|
34
41
|
class="w-full resize-y rounded-control border bg-surface-soft px-3 py-2 text-sm text-ink outline-none transition-colors duration-[var(--duration-fast)] placeholder:text-muted focus:border-brand focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-55"
|
|
35
42
|
:class="(invalid || error) ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line'"
|
|
36
43
|
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
|
package/src/theme/presets.ts
CHANGED
|
@@ -35,7 +35,10 @@ export const dark: TokenSet = {
|
|
|
35
35
|
brandSoft: "rgba(0, 188, 255, 0.16)",
|
|
36
36
|
action: "#39e58c",
|
|
37
37
|
actionHover: "#4dffa1",
|
|
38
|
+
actionSoft: "rgba(57, 229, 140, 0.14)",
|
|
38
39
|
accent: "#f0b35a",
|
|
40
|
+
accentHover: "#f2bf74",
|
|
41
|
+
accentSoft: "rgba(240, 179, 90, 0.14)",
|
|
39
42
|
danger: "#ff6f6f",
|
|
40
43
|
dangerHover: "#ff8585",
|
|
41
44
|
dangerSoft: "rgba(255, 111, 111, 0.14)",
|
|
@@ -64,7 +67,10 @@ export const light: TokenSet = {
|
|
|
64
67
|
brandSoft: "rgba(0, 134, 199, 0.12)",
|
|
65
68
|
action: "#1f9d5f",
|
|
66
69
|
actionHover: "#23b06b",
|
|
70
|
+
actionSoft: "rgba(31, 157, 95, 0.12)",
|
|
67
71
|
accent: "#aa6f1d",
|
|
72
|
+
accentHover: "#b88641",
|
|
73
|
+
accentSoft: "rgba(170, 111, 29, 0.12)",
|
|
68
74
|
danger: "#c2333b",
|
|
69
75
|
dangerHover: "#d23b44",
|
|
70
76
|
dangerSoft: "rgba(194, 51, 59, 0.12)",
|
|
@@ -93,7 +99,10 @@ export const lime: TokenSet = {
|
|
|
93
99
|
brandSoft: "rgba(196, 240, 66, 0.14)",
|
|
94
100
|
action: "#5fd97a",
|
|
95
101
|
actionHover: "#74e88d",
|
|
102
|
+
actionSoft: "rgba(95, 217, 122, 0.14)",
|
|
96
103
|
accent: "#ffb84d",
|
|
104
|
+
accentHover: "#ffc369",
|
|
105
|
+
accentSoft: "rgba(255, 184, 77, 0.14)",
|
|
97
106
|
danger: "#ff6f6f",
|
|
98
107
|
dangerHover: "#ff8585",
|
|
99
108
|
dangerSoft: "rgba(255, 111, 111, 0.14)",
|
|
@@ -148,7 +157,10 @@ export const leavepulse: TokenSet = {
|
|
|
148
157
|
brandSoft: "rgba(0, 188, 255, 0.2)",
|
|
149
158
|
action: "#44c2ad",
|
|
150
159
|
actionHover: "#56d3be",
|
|
160
|
+
actionSoft: "rgba(68, 194, 173, 0.14)",
|
|
151
161
|
accent: "#d36a2c",
|
|
162
|
+
accentHover: "#da824e",
|
|
163
|
+
accentSoft: "rgba(211, 106, 44, 0.14)",
|
|
152
164
|
danger: "#f87171",
|
|
153
165
|
dangerHover: "#fca5a5",
|
|
154
166
|
dangerSoft: "rgba(248, 113, 113, 0.14)",
|
|
@@ -181,7 +193,10 @@ export const rose: TokenSet = {
|
|
|
181
193
|
brandSoft: "rgba(251, 113, 133, 0.14)",
|
|
182
194
|
action: "#34d399",
|
|
183
195
|
actionHover: "#4adeaa",
|
|
196
|
+
actionSoft: "rgba(52, 211, 153, 0.14)",
|
|
184
197
|
accent: "#fbbf24",
|
|
198
|
+
accentHover: "#fcc947",
|
|
199
|
+
accentSoft: "rgba(251, 191, 36, 0.14)",
|
|
185
200
|
danger: "#ef4444",
|
|
186
201
|
dangerHover: "#f56565",
|
|
187
202
|
dangerSoft: "rgba(239, 68, 68, 0.14)",
|
|
@@ -210,7 +225,10 @@ export const amber: TokenSet = {
|
|
|
210
225
|
brandSoft: "rgba(245, 158, 11, 0.14)",
|
|
211
226
|
action: "#4ade80",
|
|
212
227
|
actionHover: "#5cea90",
|
|
228
|
+
actionSoft: "rgba(74, 222, 128, 0.14)",
|
|
213
229
|
accent: "#38bdf8",
|
|
230
|
+
accentHover: "#58c8f9",
|
|
231
|
+
accentSoft: "rgba(56, 189, 248, 0.14)",
|
|
214
232
|
danger: "#f87171",
|
|
215
233
|
dangerHover: "#fa8a8a",
|
|
216
234
|
dangerSoft: "rgba(248, 113, 113, 0.14)",
|
|
@@ -239,7 +257,10 @@ export const violet: TokenSet = {
|
|
|
239
257
|
brandSoft: "rgba(167, 139, 250, 0.14)",
|
|
240
258
|
action: "#34d399",
|
|
241
259
|
actionHover: "#4adeaa",
|
|
260
|
+
actionSoft: "rgba(52, 211, 153, 0.14)",
|
|
242
261
|
accent: "#fbbf24",
|
|
262
|
+
accentHover: "#fcc947",
|
|
263
|
+
accentSoft: "rgba(251, 191, 36, 0.14)",
|
|
243
264
|
danger: "#f87171",
|
|
244
265
|
dangerHover: "#fa8a8a",
|
|
245
266
|
dangerSoft: "rgba(248, 113, 113, 0.14)",
|
|
@@ -268,7 +289,10 @@ export const nord: TokenSet = {
|
|
|
268
289
|
brandSoft: "rgba(136, 192, 208, 0.16)",
|
|
269
290
|
action: "#a3be8c",
|
|
270
291
|
actionHover: "#b3cb9f",
|
|
292
|
+
actionSoft: "rgba(163, 190, 140, 0.16)",
|
|
271
293
|
accent: "#ebcb8b",
|
|
294
|
+
accentHover: "#eed39e",
|
|
295
|
+
accentSoft: "rgba(235, 203, 139, 0.16)",
|
|
272
296
|
danger: "#bf616a",
|
|
273
297
|
dangerHover: "#cb727b",
|
|
274
298
|
dangerSoft: "rgba(191, 97, 106, 0.16)",
|
package/src/theme/tokens.ts
CHANGED
|
@@ -18,7 +18,10 @@ export interface ColorTokens {
|
|
|
18
18
|
brandSoft: string
|
|
19
19
|
action: string
|
|
20
20
|
actionHover: string
|
|
21
|
+
actionSoft: string
|
|
21
22
|
accent: string
|
|
23
|
+
accentHover: string
|
|
24
|
+
accentSoft: string
|
|
22
25
|
danger: string
|
|
23
26
|
dangerHover: string
|
|
24
27
|
dangerSoft: string
|
|
@@ -99,7 +102,10 @@ export const COLOR_VARS: Record<keyof ColorTokens, string> = {
|
|
|
99
102
|
brandSoft: "--color-brand-soft",
|
|
100
103
|
action: "--color-action",
|
|
101
104
|
actionHover: "--color-action-hover",
|
|
105
|
+
actionSoft: "--color-action-soft",
|
|
102
106
|
accent: "--color-accent",
|
|
107
|
+
accentHover: "--color-accent-hover",
|
|
108
|
+
accentSoft: "--color-accent-soft",
|
|
103
109
|
danger: "--color-danger",
|
|
104
110
|
dangerHover: "--color-danger-hover",
|
|
105
111
|
dangerSoft: "--color-danger-soft",
|
package/src/tokens/tokens.css
CHANGED
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
--color-brand-soft: rgba(0, 188, 255, 0.16);
|
|
37
37
|
--color-action: #39e58c;
|
|
38
38
|
--color-action-hover: #4dffa1;
|
|
39
|
+
--color-action-soft: rgba(57, 229, 140, 0.14);
|
|
39
40
|
--color-accent: #f0b35a;
|
|
41
|
+
--color-accent-hover: #f2bf74;
|
|
42
|
+
--color-accent-soft: rgba(240, 179, 90, 0.14);
|
|
40
43
|
--color-danger: #ff6f6f;
|
|
41
44
|
--color-danger-hover: #ff8585;
|
|
42
45
|
--color-danger-soft: rgba(255, 111, 111, 0.14);
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { defineComponent, openBlock, createElementBlock, Fragment, renderList, createBlock, createCommentVNode, toDisplayString, normalizeClass } from "vue";
|
|
2
|
-
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
3
|
-
const _hoisted_1 = {
|
|
4
|
-
class: "flex items-center gap-1.5 text-sm",
|
|
5
|
-
"aria-label": "Breadcrumb"
|
|
6
|
-
};
|
|
7
|
-
const _hoisted_2 = ["href", "onClick"];
|
|
8
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
9
|
-
__name: "LpBreadcrumbs",
|
|
10
|
-
props: {
|
|
11
|
-
items: {}
|
|
12
|
-
},
|
|
13
|
-
emits: ["navigate"],
|
|
14
|
-
setup(__props) {
|
|
15
|
-
return (_ctx, _cache) => {
|
|
16
|
-
return openBlock(), createElementBlock("nav", _hoisted_1, [
|
|
17
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, i) => {
|
|
18
|
-
return openBlock(), createElementBlock(Fragment, { key: i }, [
|
|
19
|
-
i > 0 ? (openBlock(), createBlock(_sfc_main$1, {
|
|
20
|
-
key: 0,
|
|
21
|
-
name: "lucide:chevron-right",
|
|
22
|
-
size: 14,
|
|
23
|
-
class: "text-muted"
|
|
24
|
-
})) : createCommentVNode("", true),
|
|
25
|
-
item.href && i < __props.items.length - 1 ? (openBlock(), createElementBlock("a", {
|
|
26
|
-
key: 1,
|
|
27
|
-
href: item.href,
|
|
28
|
-
class: "rounded-xs text-muted outline-none transition-colors duration-[var(--duration-fast)] hover:text-ink focus-visible:ring-2 focus-visible:ring-ring",
|
|
29
|
-
onClick: ($event) => _ctx.$emit("navigate", item, i)
|
|
30
|
-
}, toDisplayString(item.label), 9, _hoisted_2)) : (openBlock(), createElementBlock("span", {
|
|
31
|
-
key: 2,
|
|
32
|
-
class: normalizeClass(i === __props.items.length - 1 ? "text-ink" : "text-muted")
|
|
33
|
-
}, toDisplayString(item.label), 3))
|
|
34
|
-
], 64);
|
|
35
|
-
}), 128))
|
|
36
|
-
]);
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
export {
|
|
41
|
-
_sfc_main as _
|
|
42
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { defineComponent, openBlock, createElementBlock, createVNode, createCommentVNode, createElementVNode, toDisplayString, renderSlot } from "vue";
|
|
2
|
-
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
3
|
-
const _hoisted_1 = { class: "flex flex-col items-center gap-3 px-6 py-12 text-center" };
|
|
4
|
-
const _hoisted_2 = {
|
|
5
|
-
key: 0,
|
|
6
|
-
class: "flex size-12 items-center justify-center rounded-pill bg-surface-soft text-muted"
|
|
7
|
-
};
|
|
8
|
-
const _hoisted_3 = { class: "flex flex-col gap-1" };
|
|
9
|
-
const _hoisted_4 = { class: "font-semibold text-ink" };
|
|
10
|
-
const _hoisted_5 = {
|
|
11
|
-
key: 0,
|
|
12
|
-
class: "max-w-sm text-sm text-muted"
|
|
13
|
-
};
|
|
14
|
-
const _hoisted_6 = {
|
|
15
|
-
key: 1,
|
|
16
|
-
class: "mt-1"
|
|
17
|
-
};
|
|
18
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
19
|
-
__name: "LpEmptyState",
|
|
20
|
-
props: {
|
|
21
|
-
icon: {},
|
|
22
|
-
title: {},
|
|
23
|
-
description: {}
|
|
24
|
-
},
|
|
25
|
-
setup(__props) {
|
|
26
|
-
return (_ctx, _cache) => {
|
|
27
|
-
return openBlock(), createElementBlock("div", _hoisted_1, [
|
|
28
|
-
__props.icon ? (openBlock(), createElementBlock("div", _hoisted_2, [
|
|
29
|
-
createVNode(_sfc_main$1, {
|
|
30
|
-
name: __props.icon,
|
|
31
|
-
size: 22
|
|
32
|
-
}, null, 8, ["name"])
|
|
33
|
-
])) : createCommentVNode("", true),
|
|
34
|
-
createElementVNode("div", _hoisted_3, [
|
|
35
|
-
createElementVNode("p", _hoisted_4, toDisplayString(__props.title), 1),
|
|
36
|
-
__props.description ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString(__props.description), 1)) : createCommentVNode("", true)
|
|
37
|
-
]),
|
|
38
|
-
_ctx.$slots.default ? (openBlock(), createElementBlock("div", _hoisted_6, [
|
|
39
|
-
renderSlot(_ctx.$slots, "default")
|
|
40
|
-
])) : createCommentVNode("", true)
|
|
41
|
-
]);
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
export {
|
|
46
|
-
_sfc_main as _
|
|
47
|
-
};
|