lots of things

This commit is contained in:
Sam Hoffman
2026-01-18 18:30:22 -05:00
parent 5e71721f4d
commit 78a1be39eb
12 changed files with 568 additions and 28 deletions

View File

@@ -1,7 +1,12 @@
<template>
<v-app>
<Login v-if="!ircStore.connected" />
<Chat v-else />
<router-view />
<!-- <Login v-if="!ircStore.connected" /> -->
<!-- <Chat v-else /> -->
<!-- <v-dialog max-width="450px" v-model="showRegistration"> -->
<!-- <Register @success="showRegistration = false" /> -->
<!-- </v-dialog> -->
</v-app>
</template>
@@ -9,6 +14,10 @@
import { useIRCStore } from "@/stores/irc";
import Chat from "@/components/Chat.vue";
import Login from "@/components/Login.vue";
import Register from "@/components/Register.vue";
import { useAccountStore } from "./stores/accountStore";
const ircStore = useIRCStore();
const accountStore = useAccountStore();
const { showRegistration } = storeToRefs(accountStore);
</script>

View File

@@ -10,6 +10,7 @@ const accountStore = useAccountStore();
<template>
<div class="d-flex flex-row" style="height: 100vh">
<router-view />
<v-sheet border class="buffers">
<UserCard />
<v-divider />
@@ -27,16 +28,10 @@ const accountStore = useAccountStore();
:me="accountStore.account.nick"
/>
<v-sheet>
<!-- <v-text-field -->
<!-- variant="outlined" -->
<!-- :placeholder="`Message ${store.activeBufferName}`" -->
<!-- v-model="inputBuffer" -->
<!-- hide-details -->
<!-- class="ma-2" -->
<!-- @keydown.enter.exact.prevent="send" -->
<!-- /> -->
<InputBuffer @send="ircStore.sendActiveBuffer" />
<InputBuffer
@raw="(txt: string) => ircStore.client.raw(txt)"
@send="ircStore.sendActiveBuffer"
/>
</v-sheet>
</div>
<v-sheet class="user-list h-100" border>

View File

@@ -1,10 +1,16 @@
<script setup>
<script setup lang="ts">
import { ref } from "vue";
import { useIRCStore } from "@/stores/irc";
import { useBufferStore } from "@/stores/bufferStore";
const emit = defineEmits(["send"]);
import { useAccountStore } from "@/stores/accountStore";
import { useRouter } from "vue-router";
const emit = defineEmits(["send", "raw"]);
const store = useIRCStore();
const accountStore = useAccountStore();
const { showRegistration } = storeToRefs(accountStore);
const router = useRouter();
const bufferStore = useBufferStore();
const text = ref();
const menu = ref({
@@ -15,7 +21,7 @@ const menu = ref({
const menuList = ref({
density: "compact",
slim: true,
items: [],
items: [] as any[],
itemTitle: "title",
itemValue: "value",
selected: [],
@@ -30,18 +36,26 @@ const completionPos = ref(0);
function clickItem() {}
function send() {
if (!text.value) return;
emit("send", text.value);
if (text.value.slice(0, 2) === "//") {
emit("raw", text.value.substring(2));
} else if (text.value[0] === "/") {
router.push({ path: "/register" });
} else {
emit("send", text.value);
}
menu.value.open = false;
text.value = "";
}
function filterUsers(s) {
function filterUsers(s: string) {
if (store.activeBuffer) {
return store.activeBuffer.users.filter((u) => u.nick.startsWith(s));
}
}
function trigger(ev) {
function trigger(ev: KeyboardEvent) {
const input = ev.target;
const cursor = input.selectionStart;
cursorPos.value = cursor;
@@ -49,7 +63,18 @@ function trigger(ev) {
const textBefore = text.slice(0, cursor);
const mentionMatch = textBefore.match(/@(\w*)$/);
if (mentionMatch) {
if (text[0] === "/") {
menu.value.open = true;
menuList.value.items = [
{
title: "register",
value: "register",
cmd: () => {
showRegistration.value = true;
},
},
];
} else if (mentionMatch) {
menu.value.open = true;
menuList.value.items = filterUsers(mentionMatch[1]);
menuList.value.itemTitle = "nick";

View File

@@ -2,15 +2,21 @@
import { useAccountStore } from "@/stores/accountStore";
import { useIRCStore } from "@/stores/irc";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
const accountStore = useAccountStore();
const ircStore = useIRCStore();
const { account } = storeToRefs(accountStore);
const withAccount = ref(false);
const form = ref(false);
const connecting = ref(false);
const router = useRouter();
function login() {
ircStore.connect();
connecting.value = true;
router.push({ name: "Chat" });
}
function required(v: any) {
@@ -48,7 +54,13 @@ function required(v: any) {
<v-checkbox v-model="withAccount" label="Login with an account" />
</v-card-text>
<v-card-actions>
<v-btn type="submit" color="success" :disabled="!form">Connect</v-btn>
<v-btn
:loading="connecting"
type="submit"
color="success"
:disabled="!form"
>Connect</v-btn
>
</v-card-actions>
</v-form>
</v-card>

View File

@@ -51,6 +51,15 @@ watch(
<span class="message-time" v-if="!!msg.time">{{
formatTime(msg.time)
}}</span>
<v-chip
class="ml-2"
v-bind="props"
label
color="purple-lighten-2"
v-if="msg.kind === 'notice'"
size="x-small"
><v-icon class="mr-2">mdi-eye</v-icon>Only visible to you
</v-chip>
</v-list-item-title>
{{ msg.message }}
</v-list-item></template

View File

@@ -0,0 +1,88 @@
<script lang="ts" setup>
import { HookStatus, useIRCStore } from "@/stores/irc";
import { onBeforeMount, onBeforeUnmount } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const ircStore = useIRCStore();
const emit = defineEmits(["success"]);
const RegistrationSuccess = new RegExp(/Account created/);
function interceptNickServMessage(message: { nick: string; message: string }) {
if (message.nick !== "NickServ") {
return HookStatus.HOOK_OK;
}
registering.value = false;
if (message.message.match(RegistrationSuccess)) {
emit("success");
}
return HookStatus.HOOK_EAT;
}
onBeforeMount(() => {
ircStore.registerHook("message", interceptNickServMessage);
ircStore.registerHook("notice", interceptNickServMessage);
});
onBeforeUnmount(() => {
ircStore.unregisterHook("message", interceptNickServMessage);
ircStore.unregisterHook("notice", interceptNickServMessage);
});
const newAccount = ref({
email: "",
username: "",
password: "",
});
const form = ref(false);
const registering = ref(false);
function register() {
if (!form.value) return;
const { password, email } = newAccount.value;
registering.value = true;
ircStore.client.say("NickServ", `REGISTER ${password} ${email}`);
}
function cancel() {
router.back();
}
const show = ref(true);
</script>
<template>
<v-dialog v-model="show" max-width="400px" persistent>
<v-card title="Account Registration">
<v-form v-model="form" @submit.prevent="register">
<v-card-text>
<v-text-field
:rules="[(v) => !!v || 'Email required']"
v-model="newAccount.email"
label="Email"
role="email"
/>
<v-text-field
:rules="[(v) => !!v || 'Password required']"
v-model="newAccount.password"
label="Password"
role="password"
type="password"
/>
</v-card-text>
<v-card-actions>
<v-btn
:disabled="!form"
type="submit"
:loading="registering"
text="Register"
color="success"
/>
<v-btn @click="cancel">Cancel</v-btn>
</v-card-actions>
</v-form>
</v-card></v-dialog
>
</template>

View File

@@ -1,8 +1,9 @@
import vuetify from "./vuetify";
import pinia from "@/stores";
import router from "./router.ts";
import type { App } from "vue";
export function registerPlugins(app: App) {
app.use(vuetify).use(pinia);
app.use(vuetify).use(router).use(pinia);
}

View File

@@ -0,0 +1,37 @@
import { createMemoryHistory, createRouter } from "vue-router";
import Chat from "@/components/Chat.vue";
import { useIRCStore } from "@/stores/irc";
const routes = [
{
path: "/",
name: "Chat",
component: Chat,
children: [
{
path: "register",
name: "Register",
component: () => import("@/components/Register.vue"),
},
],
},
{
path: "/login",
name: "Login",
component: () => import("@/components/Login.vue"),
},
];
const router = createRouter({
history: createMemoryHistory(),
routes,
});
router.beforeEach(async (to, from) => {
if (!useIRCStore().connected && to.name !== "Login") {
return { name: "Login" };
}
return true;
});
export default router;

View File

@@ -3,6 +3,7 @@ import { ref } from "vue";
export const useAccountStore = defineStore("accountStore", () => {
const authenticated = ref(false);
const showRegistration = ref(false);
const account = ref({
nick: "",
account: "",
@@ -21,5 +22,13 @@ export const useAccountStore = defineStore("accountStore", () => {
account.value.nick = v;
}
return { account, authError, authenticated, setAuthenticated, setNick };
return {
account,
authError,
authenticated,
showRegistration,
setAuthenticated,
setNick,
showRegistration,
};
});

View File

@@ -1,15 +1,50 @@
import { defineStore, storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { Client } from "irc-framework";
import { useBufferStore } from "./bufferStore";
import { ref } from "vue";
import { useAccountStore } from "./accountStore";
export type HookFunction = (event: any) => HookStatus;
export enum HookStatus {
HOOK_OK,
HOOK_EAT,
}
export const useIRCStore = defineStore("ircStore", () => {
const bufferStore = useBufferStore();
const accountStore = useAccountStore();
const connected = ref(false);
const { authError } = storeToRefs(accountStore);
const hooks = {} as Record<string, HookFunction[]>;
function registerHook(event: string, f: HookFunction) {
if (hooks[event]) hooks[event].push(f);
else hooks[event] = [f];
}
function runHook(eventName: string, eventArgs: any): HookStatus {
if (!hooks[eventName]) return HookStatus.HOOK_OK;
let lastRetVal = HookStatus.HOOK_OK;
for (const hookFunction of hooks[eventName]) {
const retVal = hookFunction(eventArgs);
if (retVal === HookStatus.HOOK_EAT) return retVal;
lastRetVal = retVal;
}
return lastRetVal;
}
function unregisterHook(eventName: string, f: HookFunction) {
if (!hooks[eventName]) return;
const idx = hooks[eventName].findIndex((item) => item === f);
if (idx === -1) return;
hooks[eventName].splice(idx, 1);
}
const selfAvatar = ref("https://placekittens.com/128/128");
const bio = ref();
@@ -85,7 +120,6 @@ export const useIRCStore = defineStore("ircStore", () => {
nick: accountStore.account.nick,
};
console.log(connectParams);
client.connect(connectParams);
}
@@ -93,11 +127,12 @@ export const useIRCStore = defineStore("ircStore", () => {
if (!bufferStore.activeBuffer) {
return;
}
bufferStore.activeBuffer.channel.say(message);
if (bufferStore.activeBuffer.channel)
bufferStore.activeBuffer.channel.say(message);
else client.say(bufferStore.activeBuffer.name, message);
}
function isMe(target: string) {
console.log(client.user.nick);
return target === client.user.nick;
}
@@ -129,8 +164,10 @@ export const useIRCStore = defineStore("ircStore", () => {
},
);
const router = useRouter();
client.on("registered", function () {
connected.value = true;
router.push({ name: "Chat" });
client.list();
client.raw("METADATA * SUB avatar");
client.raw("METADATA * SUB bio");
@@ -207,14 +244,22 @@ export const useIRCStore = defineStore("ircStore", () => {
client.on("message", function (message: { nick: string; target: string }) {
let buffer;
const retVal = runHook("message", message);
if (retVal === HookStatus.HOOK_EAT) return;
if (message.nick === "HistServ") return;
if (isMe(message.target)) {
buffer = bufferStore.getBuffer(message.nick);
} else {
buffer = bufferStore.getBuffer(message.target);
}
if (!buffer) {
return;
buffer = bufferStore.addBuffer(message.nick, {
name: message.nick,
channel: null,
});
}
buffer.messages.push(message);
@@ -227,6 +272,14 @@ export const useIRCStore = defineStore("ircStore", () => {
}
});
client.on("notice", function (message) {
const retVal = runHook("notice", message);
if (retVal === HookStatus.HOOK_EAT) return;
if (bufferStore.activeBuffer) {
bufferStore.activeBuffer.messages.push({ ...message, kind: "notice" });
}
});
client.on("join", ({ nick, channel }: { nick: string; channel: string }) => {
if (isMe(nick)) {
bufferStore.addBuffer(channel, {
@@ -294,5 +347,7 @@ export const useIRCStore = defineStore("ircStore", () => {
setBio,
bio,
connected,
registerHook,
unregisterHook,
};
});

299
app/package-lock.json generated
View File

@@ -1,6 +1,301 @@
{
"name": "IrChad",
"name": "app",
"lockfileVersion": 3,
"requires": true,
"packages": {}
"packages": {
"": {
"dependencies": {
"vue-router": "^4.6.4"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.6"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@vue/compiler-core": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz",
"integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.26",
"entities": "^7.0.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz",
"integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.26",
"@vue/shared": "3.5.26"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz",
"integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.26",
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-ssr": "3.5.26",
"@vue/shared": "3.5.26",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz",
"integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/shared": "3.5.26"
}
},
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/@vue/reactivity": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz",
"integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.26"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz",
"integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.26",
"@vue/shared": "3.5.26"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz",
"integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.26",
"@vue/runtime-core": "3.5.26",
"@vue/shared": "3.5.26",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz",
"integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.26",
"@vue/shared": "3.5.26"
},
"peerDependencies": {
"vue": "3.5.26"
}
},
"node_modules/@vue/shared": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz",
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/entities": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz",
"integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"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==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vue": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26",
"@vue/runtime-dom": "3.5.26",
"@vue/server-renderer": "3.5.26",
"@vue/shared": "3.5.26"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
}
}
}

5
app/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"vue-router": "^4.6.4"
}
}