@leavepulse/ui 0.12.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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/component-names.d.ts +1 -0
- package/dist/components/LayoutCanvas.vue.d.ts +28 -0
- package/dist/components/LayoutNode.vue.d.ts +40 -0
- package/dist/components/LpAlert.vue.d.ts +44 -0
- package/dist/components/LpAppShell.vue.d.ts +83 -0
- package/dist/components/LpAutocomplete.vue.d.ts +50 -0
- package/dist/components/LpAvatar.vue.d.ts +20 -0
- package/dist/components/LpBadge.vue.d.ts +47 -0
- package/dist/components/LpBreadcrumbs.vue.d.ts +14 -0
- package/dist/components/LpButton.vue.d.ts +113 -0
- package/dist/components/LpCalendar.vue.d.ts +20 -0
- package/dist/components/LpCard.vue.d.ts +29 -0
- package/dist/components/LpCheckbox.vue.d.ts +28 -0
- package/dist/components/LpCodeBlock.vue.d.ts +39 -0
- package/dist/components/LpCommandPalette.vue.d.ts +42 -0
- package/dist/components/LpConfirmDialog.vue.d.ts +32 -0
- package/dist/components/LpContextMenu.vue.d.ts +30 -0
- package/dist/components/LpDatePicker.vue.d.ts +25 -0
- package/dist/components/LpDisclosure.vue.d.ts +37 -0
- package/dist/components/LpDivider.vue.d.ts +17 -0
- package/dist/components/LpDrawer.vue.d.ts +88 -0
- package/dist/components/LpDropdownMenu.vue.d.ts +23 -0
- package/dist/components/LpEmptyState.vue.d.ts +18 -0
- package/dist/components/LpFormField.vue.d.ts +21 -0
- package/dist/components/LpIcon.vue.d.ts +8 -0
- package/dist/components/LpInfraNode.vue.d.ts +21 -0
- package/dist/components/LpInput.vue.d.ts +68 -0
- package/dist/components/LpLaneNode.vue.d.ts +24 -0
- package/dist/components/LpLink.vue.d.ts +27 -0
- package/dist/components/LpLogViewer.vue.d.ts +92 -0
- package/dist/components/LpModal.vue.d.ts +43 -0
- package/dist/components/LpNotificationBell.vue.d.ts +51 -0
- package/dist/components/LpNumberField.vue.d.ts +14 -0
- package/dist/components/LpOtpInput.vue.d.ts +27 -0
- package/dist/components/LpPagination.vue.d.ts +25 -0
- package/dist/components/LpPasswordInput.vue.d.ts +28 -0
- package/dist/components/LpPhoneInput.vue.d.ts +37 -0
- package/dist/components/LpPopover.vue.d.ts +34 -0
- package/dist/components/LpProgress.vue.d.ts +12 -0
- package/dist/components/LpRadio.vue.d.ts +18 -0
- package/dist/components/LpRadioGroup.vue.d.ts +26 -0
- package/dist/components/LpScrollArea.vue.d.ts +24 -0
- package/dist/components/LpSegmented.vue.d.ts +24 -0
- package/dist/components/LpSelect.vue.d.ts +45 -0
- package/dist/components/LpServiceNode.vue.d.ts +14 -0
- package/dist/components/LpSidebar.vue.d.ts +85 -0
- package/dist/components/LpSidebarNav.vue.d.ts +34 -0
- package/dist/components/LpSkeleton.vue.d.ts +6 -0
- package/dist/components/LpSlider.vue.d.ts +23 -0
- package/dist/components/LpStat.vue.d.ts +22 -0
- package/dist/components/LpStepper.vue.d.ts +10 -0
- package/dist/components/LpSwitch.vue.d.ts +11 -0
- package/dist/components/LpTable.vue.d.ts +63 -0
- package/dist/components/LpTableOfContents.vue.d.ts +19 -0
- package/dist/components/LpTabs.vue.d.ts +45 -0
- package/dist/components/LpTextarea.vue.d.ts +17 -0
- package/dist/components/LpThemeSwitcher.vue.d.ts +40 -0
- package/dist/components/LpToaster.vue.d.ts +3 -0
- package/dist/components/LpTooltip.vue.d.ts +19 -0
- package/dist/components/LpTopologyCanvas.vue.d.ts +154 -0
- package/dist/components/LpUptimeBar.vue.d.ts +30 -0
- package/dist/components/codeHighlight.d.ts +8 -0
- package/dist/components/countries-data.d.ts +2 -0
- package/dist/components/countries.d.ts +20 -0
- package/dist/components/dropdown.d.ts +29 -0
- package/dist/components/sidebar.d.ts +18 -0
- package/dist/composables/useInputFilter.d.ts +14 -0
- package/dist/composables/usePillTransition.d.ts +9 -0
- package/dist/composables/useToast.d.ts +39 -0
- package/dist/countries-data-9rgqJWHo.js +203 -0
- package/dist/icons.d.ts +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +17600 -0
- package/dist/layout/registry.d.ts +26 -0
- package/dist/layout/tree.d.ts +75 -0
- package/dist/layout/useLayout.d.ts +39 -0
- package/dist/theme/config.d.ts +16 -0
- package/dist/theme/define.d.ts +12 -0
- package/dist/theme/presets.d.ts +20 -0
- package/dist/theme/tokens.d.ts +72 -0
- package/dist/theme/useTheme.d.ts +36 -0
- package/package.json +93 -0
- package/src/canvas.css +67 -0
- package/src/component-names.ts +65 -0
- package/src/components/LayoutCanvas.vue +76 -0
- package/src/components/LayoutNode.vue +248 -0
- package/src/components/LpAlert.vue +59 -0
- package/src/components/LpAppShell.vue +210 -0
- package/src/components/LpAutocomplete.vue +244 -0
- package/src/components/LpAvatar.vue +67 -0
- package/src/components/LpBadge.vue +32 -0
- package/src/components/LpBreadcrumbs.vue +35 -0
- package/src/components/LpButton.vue +111 -0
- package/src/components/LpCalendar.vue +134 -0
- package/src/components/LpCard.vue +71 -0
- package/src/components/LpCheckbox.vue +44 -0
- package/src/components/LpCodeBlock.vue +298 -0
- package/src/components/LpCommandPalette.vue +323 -0
- package/src/components/LpConfirmDialog.vue +49 -0
- package/src/components/LpContextMenu.vue +111 -0
- package/src/components/LpDatePicker.vue +117 -0
- package/src/components/LpDisclosure.vue +92 -0
- package/src/components/LpDivider.vue +20 -0
- package/src/components/LpDrawer.vue +402 -0
- package/src/components/LpDropdownMenu.vue +54 -0
- package/src/components/LpEmptyState.vue +21 -0
- package/src/components/LpFormField.vue +31 -0
- package/src/components/LpIcon.vue +49 -0
- package/src/components/LpInfraNode.vue +108 -0
- package/src/components/LpInput.vue +147 -0
- package/src/components/LpLaneNode.vue +48 -0
- package/src/components/LpLink.vue +48 -0
- package/src/components/LpLogViewer.vue +569 -0
- package/src/components/LpModal.vue +123 -0
- package/src/components/LpNotificationBell.vue +238 -0
- package/src/components/LpNumberField.vue +56 -0
- package/src/components/LpOtpInput.vue +81 -0
- package/src/components/LpPagination.vue +165 -0
- package/src/components/LpPasswordInput.vue +50 -0
- package/src/components/LpPhoneInput.vue +241 -0
- package/src/components/LpPopover.vue +42 -0
- package/src/components/LpProgress.vue +30 -0
- package/src/components/LpRadio.vue +28 -0
- package/src/components/LpRadioGroup.vue +43 -0
- package/src/components/LpScrollArea.vue +82 -0
- package/src/components/LpSegmented.vue +83 -0
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpServiceNode.vue +57 -0
- package/src/components/LpSidebar.vue +200 -0
- package/src/components/LpSidebarNav.vue +118 -0
- package/src/components/LpSkeleton.vue +16 -0
- package/src/components/LpSlider.vue +53 -0
- package/src/components/LpStat.vue +90 -0
- package/src/components/LpStepper.vue +46 -0
- package/src/components/LpSwitch.vue +19 -0
- package/src/components/LpTable.vue +252 -0
- package/src/components/LpTableOfContents.vue +183 -0
- package/src/components/LpTabs.vue +120 -0
- package/src/components/LpTextarea.vue +39 -0
- package/src/components/LpThemeSwitcher.vue +168 -0
- package/src/components/LpToaster.vue +216 -0
- package/src/components/LpTooltip.vue +30 -0
- package/src/components/LpTopologyCanvas.vue +370 -0
- package/src/components/LpUptimeBar.vue +126 -0
- package/src/components/codeHighlight.ts +137 -0
- package/src/components/countries-data.ts +207 -0
- package/src/components/countries.ts +67 -0
- package/src/components/dropdown.ts +64 -0
- package/src/components/sidebar.ts +26 -0
- package/src/composables/useInputFilter.ts +89 -0
- package/src/composables/usePillTransition.ts +19 -0
- package/src/composables/useToast.ts +92 -0
- package/src/icons.ts +11 -0
- package/src/index.css +21 -0
- package/src/index.ts +146 -0
- package/src/layout/registry.ts +44 -0
- package/src/layout/tree.ts +288 -0
- package/src/layout/useLayout.ts +134 -0
- package/src/nuxt.ts +22 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/define.ts +50 -0
- package/src/theme/presets.ts +279 -0
- package/src/theme/tokens.ts +151 -0
- package/src/theme/useTheme.ts +288 -0
- package/src/tokens/scrollbar.css +138 -0
- package/src/tokens/tokens.css +333 -0
- package/src/tokens/utilities.css +23 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Vue Flow node for LpTopologyCanvas: an infra host/service card on kit
|
|
4
|
+
* tokens. Not used directly by consumers — LpTopologyCanvas registers it as the
|
|
5
|
+
* `infra` node type. Exported so apps can override the renderer if needed.
|
|
6
|
+
*
|
|
7
|
+
* Styled with Tailwind utilities (no scoped <style>) so the consumer's
|
|
8
|
+
* `@source` over the kit emits these classes — scoped CSS in a published
|
|
9
|
+
* component doesn't reach apps that only import the kit's token css.
|
|
10
|
+
*/
|
|
11
|
+
import { Handle, Position } from "@vue-flow/core"
|
|
12
|
+
import { computed } from "vue"
|
|
13
|
+
|
|
14
|
+
export interface InfraNodeData {
|
|
15
|
+
name: string
|
|
16
|
+
role: string
|
|
17
|
+
overlay?: string
|
|
18
|
+
online?: boolean
|
|
19
|
+
kind?: string
|
|
20
|
+
project?: string
|
|
21
|
+
/** Control-plane firewall rule count on this host (badge when > 0). */
|
|
22
|
+
firewallCount?: number
|
|
23
|
+
/** Service count on this host (badge when defined — services toggle on). */
|
|
24
|
+
serviceCount?: number
|
|
25
|
+
/** Fade the node back (search miss / not part of the focused node's graph). */
|
|
26
|
+
dimmed?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = defineProps<{ data: InfraNodeData; selected?: boolean }>()
|
|
30
|
+
|
|
31
|
+
const roleVar: Record<string, string> = {
|
|
32
|
+
hypervisor: "var(--color-accent)",
|
|
33
|
+
game: "var(--color-action)",
|
|
34
|
+
router: "var(--color-muted-strong)",
|
|
35
|
+
edge: "var(--color-brand)",
|
|
36
|
+
}
|
|
37
|
+
const accent = computed(() => roleVar[props.data.role] ?? "var(--color-line-strong)")
|
|
38
|
+
const roleIcon: Record<string, string> = {
|
|
39
|
+
hypervisor: "▣",
|
|
40
|
+
game: "◈",
|
|
41
|
+
router: "⇄",
|
|
42
|
+
edge: "☁",
|
|
43
|
+
}
|
|
44
|
+
const icon = computed(() => roleIcon[props.data.role] ?? "●")
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<div
|
|
49
|
+
class="w-[168px] overflow-hidden rounded-card border bg-surface-raised shadow-panel transition-[border-color,box-shadow]"
|
|
50
|
+
:class="selected ? 'border-[var(--accent)]' : 'border-line'"
|
|
51
|
+
:style="{
|
|
52
|
+
'--accent': accent,
|
|
53
|
+
opacity: data.dimmed ? 0.3 : data.online === false ? 0.5 : 1,
|
|
54
|
+
...(selected
|
|
55
|
+
? { boxShadow: '0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), var(--shadow-panel)' }
|
|
56
|
+
: {}),
|
|
57
|
+
}"
|
|
58
|
+
>
|
|
59
|
+
<Handle
|
|
60
|
+
type="target"
|
|
61
|
+
:position="Position.Left"
|
|
62
|
+
class="!size-2 !border-2 !border-[var(--accent)] !bg-surface"
|
|
63
|
+
/>
|
|
64
|
+
<Handle
|
|
65
|
+
type="source"
|
|
66
|
+
:position="Position.Right"
|
|
67
|
+
class="!size-2 !border-2 !border-[var(--accent)] !bg-surface"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<div class="h-[3px] bg-[var(--accent)]" />
|
|
71
|
+
<div class="px-[11px] pb-2.5 pt-[9px]">
|
|
72
|
+
<div class="flex items-center gap-[7px]">
|
|
73
|
+
<span class="text-[13px] text-[var(--accent)]">{{ icon }}</span>
|
|
74
|
+
<span class="flex-1 truncate text-[13px] font-semibold text-ink">{{ data.name }}</span>
|
|
75
|
+
<span
|
|
76
|
+
v-if="data.online !== undefined"
|
|
77
|
+
class="size-[7px] rounded-full"
|
|
78
|
+
:class="data.online ? 'bg-action shadow-[0_0_6px_var(--color-action)]' : 'bg-danger'"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
<div class="mt-1.5 flex items-center justify-between">
|
|
82
|
+
<span class="text-[10px] font-semibold uppercase tracking-wide text-[var(--accent)]">
|
|
83
|
+
{{ data.role }}
|
|
84
|
+
</span>
|
|
85
|
+
<span v-if="data.overlay" class="font-mono text-[10px] text-muted">{{ data.overlay }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div
|
|
88
|
+
v-if="data.firewallCount || data.serviceCount !== undefined"
|
|
89
|
+
class="mt-1.5 flex items-center gap-1.5"
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
v-if="data.firewallCount"
|
|
93
|
+
class="inline-flex items-center gap-1 rounded-pill bg-danger-soft px-1.5 py-0.5 text-[9px] font-medium text-danger"
|
|
94
|
+
:title="`${data.firewallCount} firewall rule(s)`"
|
|
95
|
+
>
|
|
96
|
+
<span>⛨</span>{{ data.firewallCount }}
|
|
97
|
+
</span>
|
|
98
|
+
<span
|
|
99
|
+
v-if="data.serviceCount !== undefined"
|
|
100
|
+
class="inline-flex items-center gap-1 rounded-pill bg-surface-soft px-1.5 py-0.5 text-[9px] font-medium text-muted"
|
|
101
|
+
:title="`${data.serviceCount} service(s)`"
|
|
102
|
+
>
|
|
103
|
+
<span>◇</span>{{ data.serviceCount }}
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
export default { inheritAttrs: false }
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { tv, type VariantProps } from "tailwind-variants"
|
|
7
|
+
import { computed, ref, useAttrs, useSlots } from "vue"
|
|
8
|
+
import { useInputFilter } from "../composables/useInputFilter"
|
|
9
|
+
import LpFormField from "./LpFormField.vue"
|
|
10
|
+
import LpIcon from "./LpIcon.vue"
|
|
11
|
+
|
|
12
|
+
// Single source of truth for the field shell — bare input and adorned input
|
|
13
|
+
// (e.g. LpPasswordInput) both use it, so the look never drifts.
|
|
14
|
+
const shell = tv({
|
|
15
|
+
base: [
|
|
16
|
+
"flex w-full items-center bg-surface-soft text-ink",
|
|
17
|
+
"rounded-control border border-line",
|
|
18
|
+
"transition-colors duration-[var(--duration-fast)]",
|
|
19
|
+
"focus-within:border-brand focus-within:ring-2 focus-within:ring-ring",
|
|
20
|
+
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-55",
|
|
21
|
+
],
|
|
22
|
+
variants: {
|
|
23
|
+
size: {
|
|
24
|
+
sm: "h-(--size-control-sm) text-xs",
|
|
25
|
+
md: "h-(--size-control-md) text-sm",
|
|
26
|
+
lg: "h-(--size-control-lg) text-sm",
|
|
27
|
+
},
|
|
28
|
+
invalid: {
|
|
29
|
+
true: "border-danger focus-within:border-danger focus-within:ring-danger-soft",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: { size: "md" },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const padX = { sm: "px-2.5", md: "px-3", lg: "px-3.5" } as const
|
|
36
|
+
|
|
37
|
+
type InputVariants = VariantProps<typeof shell>
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(
|
|
40
|
+
defineProps<{
|
|
41
|
+
modelValue?: string
|
|
42
|
+
size?: InputVariants["size"]
|
|
43
|
+
invalid?: boolean
|
|
44
|
+
placeholder?: string
|
|
45
|
+
type?: string
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
// Optional label/hint/error wraps the field in LpFormField. `icon` renders
|
|
48
|
+
// a leading icon (ignored when the `leading` slot is provided).
|
|
49
|
+
label?: string
|
|
50
|
+
hint?: string
|
|
51
|
+
error?: string
|
|
52
|
+
icon?: string
|
|
53
|
+
/**
|
|
54
|
+
* RegExp/string the whole value must match to be valid. Validation only —
|
|
55
|
+
* never blocks typing; a non-matching non-empty value shows the invalid
|
|
56
|
+
* style + aria-invalid. A bare string is anchored, like HTML `pattern`.
|
|
57
|
+
*/
|
|
58
|
+
pattern?: RegExp | string
|
|
59
|
+
/**
|
|
60
|
+
* One-char RegExp of allowed characters (e.g. /[0-9]/). Hard-blocks
|
|
61
|
+
* disallowed input/paste at the source — cursor- and IME-safe.
|
|
62
|
+
*/
|
|
63
|
+
restrict?: RegExp
|
|
64
|
+
}>(),
|
|
65
|
+
{ type: "text", size: "md" },
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
69
|
+
|
|
70
|
+
const { isInvalid, onBeforeInput, onPaste } = useInputFilter({
|
|
71
|
+
pattern: () => props.pattern,
|
|
72
|
+
restrict: () => props.restrict,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Pattern failure folds into the same invalid state as the explicit prop/error.
|
|
76
|
+
const patternInvalid = computed(() => isInvalid(props.modelValue))
|
|
77
|
+
|
|
78
|
+
const slots = useSlots()
|
|
79
|
+
const attrs = useAttrs()
|
|
80
|
+
const hasLeading = computed(() => !!slots.leading || !!props.icon)
|
|
81
|
+
const hasTrailing = computed(() => !!slots.trailing)
|
|
82
|
+
const hasField = computed(
|
|
83
|
+
() => !!props.label || !!props.hint || !!props.error,
|
|
84
|
+
)
|
|
85
|
+
const isInvalidState = computed(
|
|
86
|
+
() => props.invalid || !!props.error || patternInvalid.value,
|
|
87
|
+
)
|
|
88
|
+
const shellClass = computed(() =>
|
|
89
|
+
shell({ size: props.size, invalid: isInvalidState.value }),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// class/style belong to the wrapper (so `<LpInput class="...">` lays out the
|
|
93
|
+
// whole field); every other attr (autocomplete, name, @keyup, inputmode…) goes
|
|
94
|
+
// to the real <input>.
|
|
95
|
+
const rootClass = computed(() => attrs.class)
|
|
96
|
+
const rootStyle = computed(() => attrs.style)
|
|
97
|
+
const inputAttrs = computed(() => {
|
|
98
|
+
const { class: _c, style: _s, ...rest } = attrs
|
|
99
|
+
return rest
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const inputEl = ref<HTMLInputElement | null>(null)
|
|
103
|
+
defineExpose({
|
|
104
|
+
focus: (options?: FocusOptions) => inputEl.value?.focus(options),
|
|
105
|
+
blur: () => inputEl.value?.blur(),
|
|
106
|
+
select: () => inputEl.value?.select(),
|
|
107
|
+
input: inputEl,
|
|
108
|
+
})
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<component
|
|
113
|
+
:is="hasField ? LpFormField : 'div'"
|
|
114
|
+
:label="hasField ? label : undefined"
|
|
115
|
+
:hint="hasField ? hint : undefined"
|
|
116
|
+
:error="hasField ? error : undefined"
|
|
117
|
+
:class="hasField ? rootClass : undefined"
|
|
118
|
+
:style="hasField ? rootStyle : undefined"
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
:class="[shellClass, hasLeading ? 'pl-2.5' : '', hasTrailing ? 'pr-1.5' : '', hasField ? '' : rootClass]"
|
|
122
|
+
:style="hasField ? undefined : rootStyle"
|
|
123
|
+
>
|
|
124
|
+
<span v-if="hasLeading" class="flex shrink-0 items-center text-muted">
|
|
125
|
+
<slot name="leading"><LpIcon v-if="icon" :name="icon" :size="16" /></slot>
|
|
126
|
+
</span>
|
|
127
|
+
<input
|
|
128
|
+
ref="inputEl"
|
|
129
|
+
v-bind="inputAttrs"
|
|
130
|
+
:type="type"
|
|
131
|
+
:value="modelValue"
|
|
132
|
+
:placeholder="placeholder"
|
|
133
|
+
:disabled="disabled"
|
|
134
|
+
:aria-invalid="isInvalidState || undefined"
|
|
135
|
+
:class="[
|
|
136
|
+
'min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted',
|
|
137
|
+
hasLeading ? 'pl-2' : padX[size ?? 'md'],
|
|
138
|
+
hasTrailing ? '' : padX[size ?? 'md'],
|
|
139
|
+
]"
|
|
140
|
+
@beforeinput="onBeforeInput"
|
|
141
|
+
@paste="onPaste"
|
|
142
|
+
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
143
|
+
/>
|
|
144
|
+
<slot name="trailing" />
|
|
145
|
+
</div>
|
|
146
|
+
</component>
|
|
147
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Custom Vue Flow node for a project SWIMLANE on LpTopologyCanvas: a sized
|
|
4
|
+
* background zone that groups the nodes of one project (overlay network). It is
|
|
5
|
+
* a NODE (not an overlay) so Vue Flow transforms it with pan/zoom for free.
|
|
6
|
+
*
|
|
7
|
+
* Purely decorative: the lane never intercepts pointer events (pan/drag pass
|
|
8
|
+
* through to the canvas), is non-selectable / non-draggable (set on the node
|
|
9
|
+
* object by the canvas), and sits below real nodes via a low z-index. The label
|
|
10
|
+
* tab stays clickable-looking but is inert. Tailwind utilities only so styles
|
|
11
|
+
* reach consumer apps via @source.
|
|
12
|
+
*/
|
|
13
|
+
export interface LaneNodeData {
|
|
14
|
+
label: string
|
|
15
|
+
/** Optional accent colour token for the lane border + label. */
|
|
16
|
+
accent?: string
|
|
17
|
+
width: number
|
|
18
|
+
height: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{ data: LaneNodeData }>()
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div
|
|
26
|
+
class="pointer-events-none rounded-card border border-dashed"
|
|
27
|
+
:style="{
|
|
28
|
+
width: `${props.data.width}px`,
|
|
29
|
+
height: `${props.data.height}px`,
|
|
30
|
+
borderColor: props.data.accent ?? 'var(--color-line)',
|
|
31
|
+
background:
|
|
32
|
+
'color-mix(in srgb, ' +
|
|
33
|
+
(props.data.accent ?? 'var(--color-line-strong)') +
|
|
34
|
+
' 6%, transparent)',
|
|
35
|
+
}"
|
|
36
|
+
>
|
|
37
|
+
<div
|
|
38
|
+
class="inline-flex translate-y-[-50%] items-center gap-1.5 rounded-pill border bg-surface px-2.5 py-0.5 text-[11px] font-semibold tracking-wide"
|
|
39
|
+
:style="{
|
|
40
|
+
borderColor: props.data.accent ?? 'var(--color-line)',
|
|
41
|
+
color: props.data.accent ?? 'var(--color-muted-strong)',
|
|
42
|
+
marginLeft: '14px',
|
|
43
|
+
}"
|
|
44
|
+
>
|
|
45
|
+
{{ props.data.label }}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue"
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
href?: string
|
|
7
|
+
external?: boolean
|
|
8
|
+
muted?: boolean
|
|
9
|
+
/**
|
|
10
|
+
* "default" — plain hover underline.
|
|
11
|
+
* "reveal" — an underline bar that grows in from the left on hover
|
|
12
|
+
* (origin-left scale-x 0→1). The LeavePulse landing "monitoring" link feel;
|
|
13
|
+
* good for inline prose links and nav items that want a bit of motion.
|
|
14
|
+
*/
|
|
15
|
+
variant?: "default" | "reveal"
|
|
16
|
+
}>(),
|
|
17
|
+
{ variant: "default" },
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const rel = computed(() => (props.external ? "noopener noreferrer" : undefined))
|
|
21
|
+
const target = computed(() => (props.external ? "_blank" : undefined))
|
|
22
|
+
|
|
23
|
+
const colorClass = computed(() =>
|
|
24
|
+
props.muted ? "text-muted hover:text-ink" : "text-brand hover:text-brand-hover",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// The reveal bar: an ::after pseudo-element pinned to the baseline, full-width
|
|
28
|
+
// but scaled to 0 on the x-axis from the left, growing to 1 on hover/focus. The
|
|
29
|
+
// bar colour follows the link colour (brand, or ink when muted) so it re-skins
|
|
30
|
+
// with the theme.
|
|
31
|
+
const revealClass =
|
|
32
|
+
"relative after:absolute after:inset-x-0 after:-bottom-0.5 after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[var(--duration-medium)] hover:after:scale-x-100 focus-visible:after:scale-x-100"
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<a
|
|
37
|
+
:href="href"
|
|
38
|
+
:target="target"
|
|
39
|
+
:rel="rel"
|
|
40
|
+
class="inline-flex items-center gap-1 underline-offset-2 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring"
|
|
41
|
+
:class="[
|
|
42
|
+
colorClass,
|
|
43
|
+
variant === 'reveal' ? revealClass : 'hover:underline',
|
|
44
|
+
]"
|
|
45
|
+
>
|
|
46
|
+
<slot />
|
|
47
|
+
</a>
|
|
48
|
+
</template>
|