Bladeren bron

Refine install builder (#4059)

Alec Rust 2 jaren geleden
bovenliggende
commit
d843a4716e

+ 74 - 0
docs/.vitepress/theme/components/CopyToClipboardInput.vue

@@ -0,0 +1,74 @@
+<template>
+	<div class="CopyToClipboardInput" v-bind="$attrs">
+		<input type="text" class="CopyToClipboardInput-input" readonly :value="value" />
+		<button
+			type="button"
+			class="CopyToClipboardInput-button"
+			@click="copyToClipboard"
+			title="Copy to Clipboard"
+		>
+			Copy
+		</button>
+	</div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+
+const { value } = defineProps({
+	value: {
+		type: String,
+		required: true,
+	},
+});
+
+const copyToClipboard = (event) => {
+	navigator.clipboard.writeText(value).then(
+		() => {
+			event.target.textContent = "Copied!";
+			setTimeout(() => {
+				event.target.textContent = "Copy";
+			}, 1000);
+		},
+		(err) => {
+			console.error("Could not copy to clipboard:", err);
+		},
+	);
+};
+</script>
+
+<style scoped>
+.CopyToClipboardInput {
+	position: relative;
+}
+.CopyToClipboardInput-input {
+	font-size: 0.9em;
+	font-family: monospace;
+	border: 1px solid var(--vp-c-border);
+	border-radius: 4px;
+	background-color: var(--vp-c-bg);
+	width: 100%;
+	padding: 8px 13px;
+	padding-right: 53px;
+
+	&:hover {
+		border-color: var(--vp-c-border-hover);
+	}
+
+	&:focus {
+		border-color: var(--vp-c-brand);
+	}
+}
+.CopyToClipboardInput-button {
+	position: absolute;
+	top: 1px;
+	right: 1px;
+	bottom: 1px;
+	border-top-right-radius: 3px;
+	border-bottom-right-radius: 3px;
+	color: var(--vp-c-brand);
+	font-weight: 600;
+	padding: 6px 10px;
+	background-color: var(--vp-c-bg);
+}
+</style>

+ 276 - 0
docs/.vitepress/theme/components/InstallBuilder.vue

@@ -0,0 +1,276 @@
+<template>
+	<div class="InstallBuilder">
+		<div class="container">
+			<div class="output-card">
+				<h2 class="u-text-center">Installation instructions</h2>
+				<p class="u-mb10">
+					Log in to your server e.g.
+					<code>ssh root@your.server</code> and download the installation script:
+				</p>
+				<CopyToClipboardInput
+					class="u-mb10"
+					value="wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh"
+				/>
+				<p class="u-mb10">
+					Check you are running as the <code>root</code> user, configure the options you want below,
+					then run:
+				</p>
+				<CopyToClipboardInput class="u-mb10" :value="installCommand" />
+			</div>
+			<h2 class="u-text-center">Configure options</h2>
+			<ul class="option-list">
+				<li
+					v-for="option in options"
+					:key="option.flag"
+					:class="{
+						'option-item': true,
+						'is-active': selectedOptions[option.flag].enabled,
+						'is-clickable': !option.type || !selectedOptions[option.flag].enabled,
+					}"
+					@click="toggleOption(option)"
+				>
+					<div class="option-header">
+						<div class="form-check">
+							<input
+								type="checkbox"
+								class="form-check-input"
+								:id="option.flag"
+								v-model="selectedOptions[option.flag].enabled"
+							/>
+							<label :for="option.flag" @click.stop>{{ option.label }}</label>
+						</div>
+						<div class="option-icon" v-tooltip="option.description">
+							<i class="fa-solid fa-circle-info"></i>
+						</div>
+					</div>
+					<div v-if="selectedOptions[option.flag].enabled && option.type" class="option-content">
+						<label
+							v-if="option.type && option.type !== 'checkbox'"
+							class="form-label"
+							:for="`${option.flag}-input`"
+						>
+							{{ option.description }}
+						</label>
+						<input
+							v-if="option.type === 'text'"
+							class="form-control"
+							type="text"
+							:id="`${option.flag}-input`"
+							v-model="selectedOptions[option.flag].value"
+						/>
+						<select
+							v-if="option.type === 'select'"
+							class="form-select"
+							:id="`${option.flag}-input`"
+							v-model="selectedOptions[option.flag].value"
+						>
+							<option v-for="opt in option.options" :key="opt.value" :value="opt.value">
+								{{ opt.label }}
+							</option>
+						</select>
+					</div>
+				</li>
+			</ul>
+		</div>
+	</div>
+</template>
+
+<script setup>
+import { ref, watchEffect } from "vue";
+import CopyToClipboardInput from "./CopyToClipboardInput.vue";
+import FloatingVue from "floating-vue";
+
+const { options } = defineProps({
+	options: {
+		type: Array,
+		required: true,
+	},
+});
+
+// Initialize selectedOptions with default values
+const selectedOptions = ref({});
+options.forEach((option) => {
+	selectedOptions.value[option.flag] = {
+		enabled: option.default === "yes",
+		value: option.default !== "yes" && option.default !== "no" ? option.default : null,
+	};
+});
+
+// Handle clicking the entire option "card"
+const toggleOption = (option) => {
+	// Only toggle if option is a standard checkbox, or the option is unchecked
+	if (!option.type || !selectedOptions.value[option.flag].enabled) {
+		selectedOptions.value[option.flag].enabled = !selectedOptions.value[option.flag].enabled;
+	}
+};
+
+// Build the install command
+const installCommand = ref("bash hst-install.sh");
+watchEffect(() => {
+	let cmd = "bash hst-install.sh";
+	for (const [key, { enabled, value }] of Object.entries(selectedOptions.value)) {
+		const opt = options.find((o) => o.flag === key);
+
+		if (!opt.type || opt.type === "checkbox") {
+			if (enabled !== (opt.default === "yes")) {
+				cmd += ` --${key}=${enabled ? "yes" : "no"}`;
+			}
+		} else if (enabled && value !== opt.default) {
+			cmd += ` --${key}=${value}`;
+		}
+	}
+	installCommand.value = cmd;
+});
+</script>
+
+<style scoped>
+.InstallBuilder {
+	padding: 0 24px;
+
+	@media (min-width: 640px) {
+		padding: 0 48px;
+	}
+
+	@media (min-width: 960px) {
+		padding: 0 72px;
+	}
+}
+h2 {
+	font-size: 24px;
+	font-weight: 600;
+	margin-bottom: 25px;
+}
+.container {
+	display: flex;
+	flex-direction: column;
+	margin: 0 auto;
+	max-width: 1152px;
+}
+.output-card {
+	background-color: var(--vp-c-bg-alt);
+	border-radius: 10px;
+	padding: 30px 40px;
+	margin-top: 40px;
+	margin-bottom: 40px;
+}
+.option-list {
+	display: grid;
+	grid-gap: 20px;
+	margin-bottom: 50px;
+
+	@media (min-width: 640px) {
+		grid-template-columns: 1fr 1fr;
+	}
+
+	@media (min-width: 960px) {
+		grid-template-columns: 1fr 1fr 1fr;
+	}
+}
+.option-item {
+	font-size: 0.9em;
+	border-radius: 10px;
+	border: 2px solid transparent;
+	padding: 10px 20px;
+	background-color: var(--vp-c-bg-alt);
+	transition: border-color 0.2s;
+
+	&:hover {
+		border-color: var(--vp-button-brand-hover-bg);
+	}
+
+	&.is-active {
+		border-color: var(--vp-button-brand-active-bg);
+	}
+}
+.option-header {
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+}
+.option-icon {
+	padding: 5px 0 5px 10px;
+	margin-left: 5px;
+
+	& i {
+		opacity: 0.7;
+	}
+
+	&:hover i {
+		opacity: 1;
+	}
+}
+.option-content {
+	margin-top: 5px;
+	margin-bottom: 5px;
+}
+.form-label {
+	display: inline-block;
+	padding-bottom: 5px;
+}
+.form-control {
+	font-size: 0.9em;
+	border: 1px solid var(--vp-c-border);
+	border-radius: 4px;
+	background-color: var(--vp-c-bg);
+	width: 100%;
+	padding: 5px 10px;
+
+	&:hover {
+		border-color: var(--vp-c-border-hover);
+	}
+
+	&:focus {
+		border-color: var(--vp-c-brand);
+	}
+}
+.form-select {
+	appearance: auto;
+	font-size: 0.9em;
+	border: 1px solid var(--vp-c-border);
+	border-radius: 4px;
+	background-color: var(--vp-c-bg);
+	padding: 6px;
+	width: 100%;
+
+	&:hover {
+		border-color: var(--vp-c-border-hover);
+	}
+
+	&:focus {
+		border-color: var(--vp-c-brand);
+	}
+}
+.form-check {
+	flex-grow: 1;
+	position: relative;
+	padding-left: 25px;
+
+	& label {
+		font-size: 16px;
+		font-weight: 600;
+		display: block;
+		line-height: 1.6;
+
+		&:hover {
+			cursor: pointer;
+		}
+	}
+}
+.form-check-input {
+	cursor: pointer;
+	position: absolute;
+	width: 15px;
+	height: 15px;
+	margin-top: 5px;
+	margin-left: -25px;
+}
+.u-mb10 {
+	margin-bottom: 10px !important;
+}
+.u-text-center {
+	text-align: center !important;
+}
+.is-clickable {
+	cursor: pointer;
+}
+</style>

+ 0 - 298
docs/.vitepress/theme/components/InstallScriptGenerator.vue

@@ -1,298 +0,0 @@
-<template>
-	<div class="container">
-		<div class="output-card">
-			<h2 class="u-text-center">Installation instructions</h2>
-			<p class="u-mb10">
-				Log in to your server e.g.
-				<code>ssh root@your.server</code> and download the installation script:
-			</p>
-			<div class="u-pos-relative u-mb10">
-				<input
-					type="text"
-					class="form-control u-monospace"
-					readonly
-					value="wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh"
-				/>
-				<button
-					type="button"
-					class="button-positioned"
-					@click="
-						copyToClipboard(
-							'wget https://raw.githubusercontent.com/hestiacp/hestiacp/release/install/hst-install.sh',
-							$event.target,
-						)
-					"
-					title="Copy to Clipboard"
-				>
-					Copy
-				</button>
-			</div>
-			<p class="u-mb10">
-				Check you are running as the <code>root</code> user, configure the options you want below,
-				then run:
-			</p>
-			<div class="u-pos-relative u-mb10">
-				<input type="text" class="form-control u-monospace" readonly :value="installCommand" />
-				<button
-					type="button"
-					class="button-positioned"
-					@click="copyToClipboard(installCommand, $event.target)"
-					title="Copy to Clipboard"
-				>
-					Copy
-				</button>
-			</div>
-		</div>
-		<h2 class="u-text-center">Configure options</h2>
-		<ul class="option-list">
-			<li
-				v-for="option in options"
-				:key="option.flag"
-				:class="{
-					'option-item': true,
-					'is-active': selectedOptions[option.flag].enabled,
-					'is-clickable': !option.type || !selectedOptions[option.flag].enabled,
-				}"
-				@click="toggleOption(option)"
-			>
-				<div class="form-check u-mb10">
-					<input
-						type="checkbox"
-						class="form-check-input"
-						:id="option.flag"
-						v-model="selectedOptions[option.flag].enabled"
-					/>
-					<label :for="option.flag" @click.stop>{{ option.label }}</label>
-				</div>
-				<div v-if="selectedOptions[option.flag].enabled">
-					<p v-if="!option.type">
-						{{ option.description }}
-					</p>
-					<label v-else class="form-label" :for="`${option.flag}-input`">
-						{{ option.description }}
-					</label>
-					<input
-						v-if="option.type === 'text'"
-						class="form-control"
-						type="text"
-						:id="`${option.flag}-input`"
-						v-model="selectedOptions[option.flag].value"
-					/>
-					<select
-						v-if="option.type === 'select'"
-						class="form-select"
-						:id="`${option.flag}-input`"
-						v-model="selectedOptions[option.flag].value"
-					>
-						<option v-for="opt in option.options" :key="opt.value" :value="opt.value">
-							{{ opt.label }}
-						</option>
-					</select>
-				</div>
-				<div v-else>
-					<p>{{ option.description }}</p>
-				</div>
-			</li>
-		</ul>
-	</div>
-</template>
-
-<script setup>
-import { ref, watchEffect } from "vue";
-
-const { options } = defineProps({
-	options: {
-		type: Array,
-		required: true,
-	},
-});
-
-// Initialize selectedOptions with default values
-const selectedOptions = ref({});
-options.forEach((option) => {
-	selectedOptions.value[option.flag] = {
-		enabled: option.default === "yes",
-		value: option.default !== "yes" && option.default !== "no" ? option.default : null,
-	};
-});
-
-// Handle clicking the entire option "card"
-const toggleOption = (option) => {
-	// Only toggle if option is a standard checkbox, or the option is unchecked
-	if (!option.type || !selectedOptions.value[option.flag].enabled) {
-		selectedOptions.value[option.flag].enabled = !selectedOptions.value[option.flag].enabled;
-	}
-};
-
-// Copy command to clipboard
-const copyToClipboard = (text, button) => {
-	navigator.clipboard.writeText(text).then(
-		() => {
-			button.textContent = "Copied!";
-			setTimeout(() => {
-				button.textContent = "Copy";
-			}, 1000);
-		},
-		(err) => {
-			console.error("Could not copy to clipboard:", err);
-		},
-	);
-};
-
-// Build the install command
-const installCommand = ref("bash hst-install.sh");
-watchEffect(() => {
-	let cmd = "bash hst-install.sh";
-	for (const [key, { enabled, value }] of Object.entries(selectedOptions.value)) {
-		const opt = options.find((o) => o.flag === key);
-
-		if (!opt.type || opt.type === "checkbox") {
-			if (enabled !== (opt.default === "yes")) {
-				cmd += ` --${key}=${enabled ? "yes" : "no"}`;
-			}
-		} else if (enabled && value !== opt.default) {
-			cmd += ` --${key}=${value}`;
-		}
-	}
-	installCommand.value = cmd;
-});
-</script>
-
-<style scoped>
-h2 {
-	font-size: 24px;
-	font-weight: 600;
-	margin-bottom: 25px;
-}
-.container {
-	display: flex;
-	flex-direction: column;
-	margin: 0 auto;
-	max-width: 1152px;
-}
-.output-card {
-	background-color: var(--vp-c-bg-alt);
-	border-radius: 10px;
-	padding: 30px 40px;
-	margin-top: 40px;
-	margin-bottom: 40px;
-
-	& .form-control {
-		padding-right: 53px;
-	}
-}
-.option-list {
-	display: grid;
-	grid-gap: 20px;
-	margin-bottom: 50px;
-
-	@media (min-width: 640px) {
-		grid-template-columns: 1fr 1fr;
-	}
-
-	@media (min-width: 960px) {
-		grid-template-columns: 1fr 1fr 1fr;
-	}
-}
-.option-item {
-	font-size: 0.9em;
-	border-radius: 10px;
-	border: 2px solid transparent;
-	padding: 15px 20px;
-	background-color: var(--vp-c-bg-alt);
-	transition: border-color 0.2s;
-
-	&:hover {
-		border-color: var(--vp-button-brand-hover-bg);
-	}
-
-	&.is-active {
-		border-color: var(--vp-button-brand-active-bg);
-	}
-}
-.form-label {
-	display: inline-block;
-	padding-bottom: 5px;
-}
-.form-control {
-	font-size: 0.9em;
-	border: 1px solid var(--vp-c-border);
-	border-radius: 4px;
-	background-color: var(--vp-c-bg);
-	width: 100%;
-	padding: 5px 10px;
-
-	&:hover {
-		border-color: var(--vp-c-border-hover);
-	}
-
-	&:focus {
-		border-color: var(--vp-c-brand);
-	}
-}
-.form-select {
-	appearance: auto;
-	font-size: 0.9em;
-	border: 1px solid var(--vp-c-border);
-	border-radius: 4px;
-	background-color: var(--vp-c-bg);
-	padding: 6px;
-	width: 100%;
-
-	&:hover {
-		border-color: var(--vp-c-border-hover);
-	}
-
-	&:focus {
-		border-color: var(--vp-c-brand);
-	}
-}
-.form-check {
-	position: relative;
-	padding-left: 20px;
-	margin-left: 3px;
-	min-height: 24px;
-
-	& label {
-		font-weight: 600;
-		display: block;
-
-		&:hover {
-			cursor: pointer;
-		}
-	}
-}
-.form-check-input {
-	cursor: pointer;
-	position: absolute;
-	margin-top: 5px;
-	margin-left: -20px;
-}
-.button-positioned {
-	position: absolute;
-	top: 1px;
-	right: 1px;
-	bottom: 1px;
-	border-top-right-radius: 3px;
-	border-bottom-right-radius: 3px;
-	color: var(--vp-c-brand);
-	font-weight: 600;
-	padding: 6px 10px;
-	background-color: var(--vp-c-bg);
-}
-.u-mb10 {
-	margin-bottom: 10px !important;
-}
-.u-text-center {
-	text-align: center !important;
-}
-.u-monospace {
-	font-family: monospace !important;
-}
-.u-pos-relative {
-	position: relative !important;
-}
-.is-clickable {
-	cursor: pointer;
-}
-</style>

+ 3 - 0
docs/.vitepress/theme/index.js

@@ -3,12 +3,15 @@ import '@fortawesome/fontawesome-free/css/fontawesome.css';
 import '@fortawesome/fontawesome-free/css/solid.css';
 import './styles/base.css';
 import './styles/vars.css';
+import 'floating-vue/dist/style.css';
 import FeaturePage from './components/FeaturePage.vue';
 import InstallPage from './components/InstallPage.vue';
+import FloatingVue from 'floating-vue';
 
 export default {
 	...Theme,
 	enhanceApp({ app }) {
+		app.use(FloatingVue);
 		app.component('FeaturePage', FeaturePage);
 		app.component('InstallPage', InstallPage);
 	},

+ 1 - 1
docs/_data/options.js

@@ -189,7 +189,7 @@ export const options = [
 	{
 		flag: 'interactive',
 		label: 'Interactive install',
-		description: 'Enable interactive install.',
+		description: 'Run the install interactively.',
 		default: 'yes',
 	},
 	{

+ 2 - 2
docs/install.md

@@ -5,7 +5,7 @@ title: Install
 
 <script setup>
   import PageHeader from "./.vitepress/theme/components/PageHeader.vue";
-  import InstallScriptGenerator from "./.vitepress/theme/components/InstallScriptGenerator.vue";
+  import InstallBuilder from "./.vitepress/theme/components/InstallBuilder.vue";
   import { options } from "./_data/options";
 </script>
 
@@ -13,5 +13,5 @@ title: Install
   <PageHeader>
     <template #title>Install</template>
   </PageHeader>
-  <InstallScriptGenerator :options="options"></InstallScriptGenerator>
+  <InstallBuilder :options="options"></InstallBuilder>
 </InstallPage>

+ 61 - 41
package-lock.json

@@ -14,6 +14,7 @@
 				"alpinejs": "3.13.1",
 				"chart.js": "4.4.0",
 				"check-password-strength": "2.0.7",
+				"floating-vue": "^2.0.0-beta.24",
 				"nanoid": "5.0.1",
 				"normalize.css": "8.0.1",
 				"xterm": "5.3.0",
@@ -335,7 +336,6 @@
 			"version": "7.22.6",
 			"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.6.tgz",
 			"integrity": "sha512-EIQu22vNkceq3LbjAq7knDf/UmtI2qbcNI8GRBlijez6TpQLvSodJPYfydQmNA5buwkxxxa/PVI44jjYZ+/cLw==",
-			"dev": true,
 			"bin": {
 				"parser": "bin/babel-parser.js"
 			},
@@ -885,6 +885,27 @@
 				"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
 			}
 		},
+		"node_modules/@floating-ui/core": {
+			"version": "1.5.0",
+			"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz",
+			"integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==",
+			"dependencies": {
+				"@floating-ui/utils": "^0.1.3"
+			}
+		},
+		"node_modules/@floating-ui/dom": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
+			"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
+			"dependencies": {
+				"@floating-ui/core": "^1.1.0"
+			}
+		},
+		"node_modules/@floating-ui/utils": {
+			"version": "0.1.6",
+			"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
+			"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
+		},
 		"node_modules/@fortawesome/fontawesome-free": {
 			"version": "6.4.2",
 			"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.2.tgz",
@@ -930,8 +951,7 @@
 		"node_modules/@jridgewell/sourcemap-codec": {
 			"version": "1.4.15",
 			"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-			"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
-			"dev": true
+			"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
 		},
 		"node_modules/@kurkle/color": {
 			"version": "0.3.2",
@@ -1043,7 +1063,6 @@
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
 			"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
-			"dev": true,
 			"dependencies": {
 				"@babel/parser": "^7.21.3",
 				"@vue/shared": "3.3.4",
@@ -1054,14 +1073,12 @@
 		"node_modules/@vue/compiler-core/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/compiler-dom": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
 			"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
-			"dev": true,
 			"dependencies": {
 				"@vue/compiler-core": "3.3.4",
 				"@vue/shared": "3.3.4"
@@ -1070,14 +1087,12 @@
 		"node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/compiler-sfc": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
 			"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
-			"dev": true,
 			"dependencies": {
 				"@babel/parser": "^7.20.15",
 				"@vue/compiler-core": "3.3.4",
@@ -1094,14 +1109,12 @@
 		"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/compiler-ssr": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
 			"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
-			"dev": true,
 			"dependencies": {
 				"@vue/compiler-dom": "3.3.4",
 				"@vue/shared": "3.3.4"
@@ -1110,8 +1123,7 @@
 		"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/devtools-api": {
 			"version": "6.5.0",
@@ -1131,7 +1143,6 @@
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
 			"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
-			"dev": true,
 			"dependencies": {
 				"@babel/parser": "^7.20.15",
 				"@vue/compiler-core": "3.3.4",
@@ -1143,14 +1154,12 @@
 		"node_modules/@vue/reactivity-transform/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/runtime-core": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
 			"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
-			"dev": true,
 			"dependencies": {
 				"@vue/reactivity": "3.3.4",
 				"@vue/shared": "3.3.4"
@@ -1160,7 +1169,6 @@
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
 			"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
-			"dev": true,
 			"dependencies": {
 				"@vue/shared": "3.3.4"
 			}
@@ -1168,14 +1176,12 @@
 		"node_modules/@vue/runtime-core/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/runtime-dom": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
 			"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
-			"dev": true,
 			"dependencies": {
 				"@vue/runtime-core": "3.3.4",
 				"@vue/shared": "3.3.4",
@@ -1185,14 +1191,12 @@
 		"node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/server-renderer": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
 			"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
-			"dev": true,
 			"dependencies": {
 				"@vue/compiler-ssr": "3.3.4",
 				"@vue/shared": "3.3.4"
@@ -1204,8 +1208,7 @@
 		"node_modules/@vue/server-renderer/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/@vue/shared": {
 			"version": "3.1.5",
@@ -1978,8 +1981,7 @@
 		"node_modules/csstype": {
 			"version": "3.1.2",
 			"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
-			"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
-			"dev": true
+			"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
 		},
 		"node_modules/debug": {
 			"version": "4.3.4",
@@ -2604,8 +2606,7 @@
 		"node_modules/estree-walker": {
 			"version": "2.0.2",
 			"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
-			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
-			"dev": true
+			"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
 		},
 		"node_modules/esutils": {
 			"version": "2.0.3",
@@ -2768,6 +2769,24 @@
 			"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
 			"dev": true
 		},
+		"node_modules/floating-vue": {
+			"version": "2.0.0-beta.24",
+			"resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-2.0.0-beta.24.tgz",
+			"integrity": "sha512-URSzP6YXaF4u1oZ9XGL8Sn8puuM7ivp5jkOUrpy5Q1mfo9BfGppJOn+ierTmsSUfJEeHBae8KT7r5DeI3vQIEw==",
+			"dependencies": {
+				"@floating-ui/dom": "~1.1.1",
+				"vue-resize": "^2.0.0-alpha.1"
+			},
+			"peerDependencies": {
+				"@nuxt/kit": "^3.2.0",
+				"vue": "^3.2.0"
+			},
+			"peerDependenciesMeta": {
+				"@nuxt/kit": {
+					"optional": true
+				}
+			}
+		},
 		"node_modules/focus-trap": {
 			"version": "7.5.2",
 			"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz",
@@ -4014,7 +4033,6 @@
 			"version": "0.30.1",
 			"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz",
 			"integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==",
-			"dev": true,
 			"dependencies": {
 				"@jridgewell/sourcemap-codec": "^1.4.15"
 			},
@@ -4725,8 +4743,7 @@
 		"node_modules/picocolors": {
 			"version": "1.0.0",
 			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
-			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
-			"dev": true
+			"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
 		},
 		"node_modules/picomatch": {
 			"version": "2.3.1",
@@ -4756,7 +4773,6 @@
 			"version": "8.4.31",
 			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
 			"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "opencollective",
@@ -4825,7 +4841,6 @@
 			"version": "3.3.6",
 			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
 			"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
-			"dev": true,
 			"funding": [
 				{
 					"type": "github",
@@ -5384,7 +5399,6 @@
 			"version": "1.0.2",
 			"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
 			"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
-			"dev": true,
 			"engines": {
 				"node": ">=0.10.0"
 			}
@@ -6517,7 +6531,6 @@
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
 			"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
-			"dev": true,
 			"dependencies": {
 				"@vue/compiler-dom": "3.3.4",
 				"@vue/compiler-sfc": "3.3.4",
@@ -6526,11 +6539,18 @@
 				"@vue/shared": "3.3.4"
 			}
 		},
+		"node_modules/vue-resize": {
+			"version": "2.0.0-alpha.1",
+			"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
+			"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
+			"peerDependencies": {
+				"vue": "^3.0.0"
+			}
+		},
 		"node_modules/vue/node_modules/@vue/shared": {
 			"version": "3.3.4",
 			"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
-			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==",
-			"dev": true
+			"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
 		},
 		"node_modules/which": {
 			"version": "2.0.2",

+ 1 - 0
package.json

@@ -22,6 +22,7 @@
 		"alpinejs": "3.13.1",
 		"chart.js": "4.4.0",
 		"check-password-strength": "2.0.7",
+		"floating-vue": "^2.0.0-beta.24",
 		"nanoid": "5.0.1",
 		"normalize.css": "8.0.1",
 		"xterm": "5.3.0",