mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
Dashboard page restructured. Moved some elements to the other views.
This commit is contained in:
parent
1cf9029a63
commit
315d169cf9
10 changed files with 363 additions and 121 deletions
|
|
@ -35,18 +35,18 @@
|
|||
}
|
||||
|
||||
&.block__primary {
|
||||
@apply bg-sky-300/40;
|
||||
@apply bg-sky-300/20;
|
||||
|
||||
&::before {
|
||||
@apply from-sky-100/40;
|
||||
@apply from-sky-100/20;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__secondary {
|
||||
@apply bg-amber-300/40;
|
||||
@apply bg-amber-300/20;
|
||||
|
||||
&::before {
|
||||
@apply from-amber-100/40;
|
||||
@apply from-amber-100/20;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { onMounted, onUpdated, ref } from 'vue'
|
||||
import ButtonComp from './ButtonComp.vue'
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'
|
||||
|
||||
|
|
@ -35,6 +35,10 @@ onMounted(() => {
|
|||
if (props.open) toggleAccordion(props.open)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
if (props.open) toggleAccordion(props.open)
|
||||
})
|
||||
|
||||
function toggleAccordion(open = false) {
|
||||
if (open) {
|
||||
accordionOpen.value = true
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ button,
|
|||
tracking-wide
|
||||
font-normal
|
||||
transition-all
|
||||
cursor-pointer;
|
||||
cursor-pointer
|
||||
no-underline;
|
||||
|
||||
transition:
|
||||
border-color 0.1s ease-in-out,
|
||||
|
|
|
|||
86
fe/src/components/dashboard/RemoteView.vue
Normal file
86
fe/src/components/dashboard/RemoteView.vue
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div id="remote-dashboard">
|
||||
<div id="panels" class="dashboard-block mcrm-block block__light" v-if="server.handshake">
|
||||
<div class="icon__container">
|
||||
<IconLayoutGrid />
|
||||
</div>
|
||||
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
|
||||
<template v-if="server.panelCount == 0">
|
||||
<p><em>No panels found. </em></p>
|
||||
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Start using a panel!</p>
|
||||
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
<div id="server" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconServer />
|
||||
</div>
|
||||
<h4>Server</h4>
|
||||
<template v-if="server.handshake">
|
||||
<p>
|
||||
Linked with: <strong class="text-center">{{ server.ip }}</strong>
|
||||
</p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconServer /> View server</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
<em>Not linked</em>
|
||||
</p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link with server</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconLayoutGrid, IconLink, IconServer } from '@tabler/icons-vue'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
|
||||
const device = useDeviceStore()
|
||||
const panel = usePanelStore()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
handshake: '',
|
||||
panelCount: 0,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const serverIp = await device.serverGetIP()
|
||||
server.ip = serverIp
|
||||
|
||||
if (device.key()) server.handshake = true
|
||||
|
||||
device.$subscribe(() => {
|
||||
if (device.key()) server.handshake = true
|
||||
})
|
||||
|
||||
const panelCount = await panel.getList(true)
|
||||
server.panelCount = panelCount
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
#remote-dashboard {
|
||||
@apply grid
|
||||
pt-8
|
||||
gap-4
|
||||
md:w-fit
|
||||
h-fit
|
||||
content-start;
|
||||
|
||||
&.not__linked #server {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
fe/src/components/dashboard/ServerView.vue
Normal file
133
fe/src/components/dashboard/ServerView.vue
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<div
|
||||
id="server-dashboard"
|
||||
:class="`${server.remoteCount == 0 ? 'no__devices' : 'devices__found'} ${server.macroCount == 0 ? 'no__macros' : 'macros__found'}`"
|
||||
>
|
||||
<div id="devices" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconDevices />
|
||||
</div>
|
||||
<h4>{{ server.remoteCount }} {{ server.remoteCount != 1 ? 'Devices' : 'Device' }}</h4>
|
||||
<template v-if="server.remoteCount == 0">
|
||||
<p><em>No devices found.</em></p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link a device</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Unlink a device or add new devices.</p>
|
||||
<ButtonComp variant="primary" href="/devices"><IconDevices /> View devices</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
<div id="macros" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconKeyboard />
|
||||
</div>
|
||||
<h4>{{ server.macroCount }} {{ server.macroCount != 1 ? 'Macros' : 'Macro' }}</h4>
|
||||
<template v-if="server.macroCount == 0">
|
||||
<p><em>No macros found.</em></p>
|
||||
<ButtonComp variant="secondary" href="/macros"> <IconLayoutGrid /> Create macro</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Edit and view your macros.</p>
|
||||
<ButtonComp variant="secondary" href="/macros"><IconKeyboard /> View macros</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="panels" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconLayoutGrid />
|
||||
</div>
|
||||
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
|
||||
<template v-if="server.panelCount == 0">
|
||||
<p><em>No panels found. </em></p>
|
||||
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Link macros to panels or view a panel.</p>
|
||||
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { IconDevices, IconKeyboard, IconLayoutGrid, IconLink } from '@tabler/icons-vue'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { GetMacroList } from '@/services/MacroService'
|
||||
|
||||
const device = useDeviceStore()
|
||||
const panel = usePanelStore()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
port: '',
|
||||
fullPath: '',
|
||||
remoteCount: 0,
|
||||
macroCount: 0,
|
||||
panelCount: 0,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const serverIP = await device.serverGetIP()
|
||||
server.ip = serverIP
|
||||
// server.port = window.__CONFIG__.MCRM__PORT
|
||||
// server.fullPath = `http://${server.ip}:${server.port}`
|
||||
|
||||
const remoteCount = await device.serverGetRemotes(true)
|
||||
server.remoteCount = remoteCount
|
||||
|
||||
const macroCount = await GetMacroList(true)
|
||||
server.macroCount = macroCount
|
||||
|
||||
const panelCount = await panel.getList(true)
|
||||
server.panelCount = panelCount
|
||||
|
||||
console.log(server)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
#server-dashboard {
|
||||
@apply grid
|
||||
grid-cols-1
|
||||
grid-rows-3
|
||||
md:grid-cols-3
|
||||
md:grid-rows-1
|
||||
gap-4
|
||||
w-fit
|
||||
h-fit
|
||||
pt-8;
|
||||
|
||||
&.no__devices #devices {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
|
||||
&.no__macros.devices__found #devices {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
|
||||
&.devices__found #devices {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
|
||||
&.no__devices.no__macros #macros {
|
||||
@apply row-start-2 md:col-start-2;
|
||||
}
|
||||
|
||||
&.no__macros #macros {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
|
||||
&.macros__found #macros {
|
||||
@apply row-start-2 md:col-start-2;
|
||||
}
|
||||
|
||||
&.no__devices.macros__found #macros {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,12 +7,14 @@
|
|||
|
||||
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
|
||||
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
|
||||
<span class="flex gap-4" v-if="Object.keys(remote.devices).length > 0">
|
||||
<span class="flex gap-4">
|
||||
<IconDevices />{{ Object.keys(remote.devices).length }}
|
||||
{{ Object.keys(remote.devices).length > 1 ? 'Devices' : 'Device' }}
|
||||
{{ Object.keys(remote.devices).length == 1 ? 'Device' : 'Devices' }}
|
||||
</span>
|
||||
|
||||
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
|
||||
<ButtonComp v-if="!remote.poll" variant="primary" @click="device.serverGetRemotes()"
|
||||
><IconReload
|
||||
/></ButtonComp>
|
||||
</h4>
|
||||
<template v-if="Object.keys(remote.devices).length > 0">
|
||||
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
|
||||
|
|
@ -51,28 +53,34 @@
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- <template v-else>
|
||||
<div class="grid w-full gap-4">
|
||||
<em class="text-slate-300">No remote devices</em>
|
||||
</div>
|
||||
</template>
|
||||
</template> -->
|
||||
|
||||
<AccordionComp
|
||||
class="w-full mt-8 border-t border-t-white/50"
|
||||
title="How to connect a device?"
|
||||
:open="Object.keys(remote.devices).length == 0"
|
||||
>
|
||||
<div class="grid py-4">
|
||||
<ul class="space-y-1">
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
To connect a device, open <strong>http://{{ server.ip }}:{{ server.port }}</strong> in
|
||||
a browser on the device.
|
||||
Scan the QR code with the remote device.
|
||||
<div class="grid gap-4 py-4 pl-6">
|
||||
<canvas ref="serverQr"></canvas>
|
||||
<p>
|
||||
Or manually type the IP address: <br />
|
||||
<strong>{{ server.ip }}/devices</strong>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>Open the menu, and click on <strong>Server.</strong></li>
|
||||
<li>
|
||||
The device will automatically request access, if you see "Access requested" on the
|
||||
device.
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="!remote.poll">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
Click the
|
||||
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
|
||||
|
|
@ -85,14 +93,14 @@
|
|||
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
|
||||
<IconLink class="size-4" /> Link device
|
||||
</span>
|
||||
to generate a one-time-pin to link the device.
|
||||
A one-time-pin will be shown in a dialog.
|
||||
</div>
|
||||
</li>
|
||||
<li>Enter the pin on the remote device.</li>
|
||||
<li>
|
||||
Enter the pin that is shown on this server in the dialog that will appear on the
|
||||
Congratulations! You have linked a device! You can now start using panels on that
|
||||
device.
|
||||
</li>
|
||||
<li>Congratulations! You have linked a device! (Hopefully)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AccordionComp>
|
||||
|
|
@ -110,7 +118,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
||||
import AlertComp from '../base/AlertComp.vue'
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import {
|
||||
|
|
@ -128,16 +136,18 @@ import DialogComp from '../base/DialogComp.vue'
|
|||
import axios from 'axios'
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
import AccordionComp from '../base/AccordionComp.vue'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const device = useDeviceStore()
|
||||
|
||||
const pinDialog = ref()
|
||||
const serverQr = ref()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
port: '',
|
||||
})
|
||||
const remote = reactive({ devices: [], pinlink: false })
|
||||
|
||||
const remote = reactive({ devices: [], pinlink: false, poll: false })
|
||||
|
||||
onMounted(async () => {
|
||||
device.serverGetRemotes()
|
||||
|
|
@ -146,11 +156,32 @@ onMounted(async () => {
|
|||
if (state.remote !== remote.devices) remote.devices = device.remote
|
||||
})
|
||||
|
||||
getIp()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
getIp()
|
||||
|
||||
if (Object.keys(remote.devices).length == 0 && !remote.poll) {
|
||||
remote.poll = setInterval(() => {
|
||||
device.serverGetRemotes()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (Object.keys(remote.devices).length > 0 && remote.poll) {
|
||||
clearInterval(remote.poll)
|
||||
remote.poll = false
|
||||
}
|
||||
})
|
||||
|
||||
async function getIp() {
|
||||
const serverIP = await device.serverGetIP()
|
||||
server.ip = serverIP
|
||||
|
||||
server.port = window.__CONFIG__.MCRM__PORT
|
||||
QRCode.toCanvas(serverQr.value, `${server.ip}/devices`, (error) => {
|
||||
if (error) console.log('QRCode error: ', error)
|
||||
})
|
||||
}
|
||||
|
||||
async function startLink(deviceUuid) {
|
||||
const pin = await device.serverStartLink(deviceUuid)
|
||||
|
|
|
|||
|
|
@ -2,9 +2,13 @@ import axios from 'axios'
|
|||
import { appUrl, isLocal } from './ApiService'
|
||||
import { AuthCall } from './EncryptService'
|
||||
|
||||
export const GetMacroList = async () => {
|
||||
export const GetMacroList = async (count = false) => {
|
||||
const request = await axios.post(appUrl() + '/macro/list')
|
||||
return sortMacroList(request.data)
|
||||
|
||||
if (!request.data) return 0
|
||||
|
||||
if (!count) return sortMacroList(request.data)
|
||||
else return request.data.length
|
||||
}
|
||||
|
||||
const sortMacroList = (list) => {
|
||||
|
|
|
|||
|
|
@ -52,16 +52,19 @@ export const useDeviceStore = defineStore('device', () => {
|
|||
|
||||
const serverGetIP = async () => {
|
||||
const request = await axios.post(appUrl() + '/device/server/ip')
|
||||
return request.data
|
||||
return `http://${request.data}:${window.__CONFIG__.MCRM__PORT}`
|
||||
}
|
||||
|
||||
// Server application
|
||||
const serverGetRemotes = async (remoteUuid) => {
|
||||
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
|
||||
if (data.data.devices) {
|
||||
remote.value = data.data.devices
|
||||
}
|
||||
})
|
||||
const serverGetRemotes = async (count = false) => {
|
||||
const request = await axios.post(appUrl() + '/device/list')
|
||||
|
||||
if (!request.data.devices) return false
|
||||
|
||||
remote.value = request.data.devices
|
||||
|
||||
if (!count) return remote.value
|
||||
else return Object.keys(remote.value).length
|
||||
}
|
||||
|
||||
const serverStartLink = async (deviceUuid) => {
|
||||
|
|
|
|||
|
|
@ -36,15 +36,20 @@ export const usePanelStore = defineStore('panel', () => {
|
|||
return current.value
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
if (list.value.length > 0) return list.value
|
||||
const getList = async (count = false) => {
|
||||
if (list.value.length > 0 && !count) return list.value
|
||||
else if (list.value.length > 0 && count) return list.value.length
|
||||
|
||||
const data = AuthCall()
|
||||
|
||||
const resp = await axios.post(appUrl() + '/panel/list', data)
|
||||
list.value = resp.data
|
||||
|
||||
return list.value
|
||||
if (!resp.data && !count) return false
|
||||
else if (!resp.data && count) return 0
|
||||
|
||||
if (!count) return list.value
|
||||
else return list.value.length
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,97 +1,72 @@
|
|||
<template>
|
||||
<div id="dashboard" class="panel">
|
||||
<h1 class="panel__title">Dashboard</h1>
|
||||
|
||||
<div class="panel__content">
|
||||
<div class="grid gap-1 opacity-50 h-fit">
|
||||
<div class="panel__title">
|
||||
<h1>Dashboard</h1>
|
||||
<div>
|
||||
<em v-if="isLocal()">This is the server dashboard.</em>
|
||||
<em v-else>This is the remote dashboard.</em>
|
||||
</div>
|
||||
<div v-if="isLocal()">
|
||||
<h4>Start: Authenticate a device</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Open <strong>{{ server.ip }}:{{ server.port }}</strong> in a browser on your phone.
|
||||
<div class="p-4 qr-container">
|
||||
<canvas ref="serverQr"></canvas>
|
||||
</div>
|
||||
</li>
|
||||
<li>Navigate to the Server page. An authentication request will start automatically.</li>
|
||||
<li>
|
||||
Open the
|
||||
<RouterLink to="/devices"> Devices</RouterLink>
|
||||
page in this window.
|
||||
</li>
|
||||
<li>
|
||||
<div class="inline-flex items-center gap-2">
|
||||
The device will appear, if not click the
|
||||
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> button.
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h4>Using a panel</h4>
|
||||
<ul>
|
||||
<li>Once authenticated you can access the Test Panel on your device.</li>
|
||||
<li>Open the Panels page on your device and click the Test Panel.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4>Start: Authenticate this device</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Open
|
||||
<RouterLink to="/devices"> Server</RouterLink>
|
||||
to start.
|
||||
</li>
|
||||
<li>An authentication request will start automatically.</li>
|
||||
</ul>
|
||||
<h4>Using a panel</h4>
|
||||
<ul>
|
||||
<li>
|
||||
Once authenticated you can access the
|
||||
<RouterLink to="/panel/view/test_panel"> Test Panel</RouterLink>
|
||||
on your device.
|
||||
</li>
|
||||
<li>
|
||||
Open the
|
||||
<RouterLink to="/panels">Panels page</RouterLink>
|
||||
you can edit the panel.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panel__content">
|
||||
<ServerView v-if="isLocal()" />
|
||||
<RemoteView v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { IconReload } from '@tabler/icons-vue'
|
||||
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const device = useDeviceStore()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
port: '',
|
||||
fullPath: '',
|
||||
})
|
||||
|
||||
const serverQr = ref()
|
||||
|
||||
onMounted(async () => {
|
||||
const serverIP = await device.serverGetIP()
|
||||
server.ip = serverIP
|
||||
server.port = window.__CONFIG__.MCRM__PORT
|
||||
server.fullPath = `http://${server.ip}:${server.port}`
|
||||
|
||||
QRCode.toCanvas(serverQr.value, server.fullPath, (error) => {
|
||||
console.log(error)
|
||||
})
|
||||
})
|
||||
import ServerView from '@/components/dashboard/ServerView.vue'
|
||||
import RemoteView from '@/components/dashboard/RemoteView.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.dashboard-block {
|
||||
@apply md:!row-start-1
|
||||
grid
|
||||
justify-items-center
|
||||
gap-4;
|
||||
|
||||
&#devices .icon__container,
|
||||
&#server .icon__container {
|
||||
@apply bg-sky-300/30
|
||||
text-sky-400
|
||||
border-sky-300/60;
|
||||
}
|
||||
|
||||
&#macros .icon__container {
|
||||
@apply bg-amber-300/30
|
||||
text-amber-400
|
||||
border-amber-300/60;
|
||||
}
|
||||
|
||||
&#panels .icon__container {
|
||||
@apply bg-rose-300/30
|
||||
text-rose-400
|
||||
border-rose-300/60;
|
||||
}
|
||||
|
||||
.icon__container {
|
||||
@apply flex
|
||||
justify-center
|
||||
items-center
|
||||
size-16
|
||||
aspect-square
|
||||
rounded-full
|
||||
border;
|
||||
|
||||
svg {
|
||||
@apply size-8;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@apply opacity-50
|
||||
w-42
|
||||
text-center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue