mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-23 16:26:15 -04:00
feat(app): tools management base
This commit is contained in:
parent
202896fa95
commit
b22173681c
29 changed files with 1372 additions and 45 deletions
|
@ -5,7 +5,7 @@
|
|||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--background: 0 0% 98%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
|
@ -14,7 +14,7 @@
|
|||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary: 150 76% 38%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
|
@ -36,10 +36,10 @@
|
|||
}
|
||||
|
||||
.dark {
|
||||
--background:0 0% 9%;
|
||||
--background:240 5% 6%;
|
||||
--foreground:0 0% 98%;
|
||||
|
||||
--card: 0 0% 7%;
|
||||
--card: 240 5% 8%;
|
||||
--card-foreground:0 0% 98%;
|
||||
|
||||
--popover:240 10% 3.9%;
|
||||
|
@ -74,4 +74,4 @@
|
|||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,5 @@
|
|||
export default defineI18nConfig(() => ({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
welcome: 'Welcome',
|
||||
},
|
||||
fr: {
|
||||
welcome: 'Bienvenue',
|
||||
},
|
||||
},
|
||||
fallbackLocale: 'en',
|
||||
}));
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import toolsModule from './src/modules/tools/modules/tools.modules';
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
|
@ -14,9 +16,18 @@ export default defineNuxtConfig({
|
|||
'@nuxt/icon',
|
||||
'@vueuse/nuxt',
|
||||
'@nuxtjs/color-mode',
|
||||
toolsModule, // Must be imported before i18n
|
||||
'@nuxtjs/i18n',
|
||||
'@nuxtjs/seo',
|
||||
'@pinia/nuxt',
|
||||
],
|
||||
|
||||
site: {
|
||||
url: 'https://it-tools.tech',
|
||||
name: 'IT Tools',
|
||||
description: 'The open-source collection of handy online tools to help developers in their daily life.',
|
||||
},
|
||||
|
||||
fonts: {
|
||||
provider: 'bunny',
|
||||
defaults: {
|
||||
|
@ -35,7 +46,11 @@ export default defineNuxtConfig({
|
|||
i18n: {
|
||||
strategy: 'prefix',
|
||||
vueI18n: './i18n.config.ts',
|
||||
locales: ['en', 'fr'],
|
||||
defaultLocale: 'en',
|
||||
langDir: './src/locales',
|
||||
locales: [
|
||||
{ code: 'en', file: 'en.yaml', name: 'English' },
|
||||
{ code: 'fr', file: 'fr.yaml', name: 'Français' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@9.12.2",
|
||||
|
||||
"scripts": {
|
||||
"dev": "nuxt dev",
|
||||
"build": "nuxt build",
|
||||
|
@ -19,12 +18,16 @@
|
|||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@corentinth/chisels": "^1.1.0",
|
||||
"@nuxt/fonts": "^0.10.2",
|
||||
"@nuxt/icon": "^1.5.6",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/i18n": "^8.5.5",
|
||||
"@nuxtjs/seo": "2.0.0-rc.23",
|
||||
"@pinia/nuxt": "^0.5.5",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-vue-next": "^0.453.0",
|
||||
"nuxt": "^3.13.2",
|
||||
"radix-vue": "^1.9.7",
|
||||
|
@ -37,6 +40,7 @@
|
|||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^3.8.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"@vueuse/nuxt": "^11.1.0",
|
||||
"eslint": "^9.13.0",
|
||||
|
|
4
packages/app/public/humans.txt
Normal file
4
packages/app/public/humans.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* TEAM */
|
||||
Developer: Corentin Thomasset
|
||||
Site: https://corentin.tech
|
||||
Twitter: @cthmsst
|
|
@ -1 +0,0 @@
|
|||
|
31
packages/app/src/locales/en.yaml
Normal file
31
packages/app/src/locales/en.yaml
Normal file
|
@ -0,0 +1,31 @@
|
|||
app:
|
||||
title: IT-Tools
|
||||
description: The open-source collection of handy online tools to help developers in their daily life.
|
||||
home:
|
||||
all-the-tools: All the tools
|
||||
search-tools: Search for a tool
|
||||
open-source: Open Source
|
||||
free: Free
|
||||
self-hostable: Self-hostable
|
||||
open-tool: Open tool
|
||||
footer:
|
||||
resources:
|
||||
title: Resources
|
||||
all-tools: All the tools
|
||||
github: GitHub repository
|
||||
support: Support IT-Tools
|
||||
license: License
|
||||
support:
|
||||
title: Support
|
||||
report-bug: Report a bug
|
||||
request-feature: Request a feature
|
||||
contribute: Contribute to the project
|
||||
contact: Contact me
|
||||
friends:
|
||||
title: Friends
|
||||
tools:
|
||||
token-generator:
|
||||
title: Token Generator
|
||||
description: >-
|
||||
Generate random string with the characters you want, uppercase, lowercase
|
||||
letters, numbers and/or symbols.
|
29
packages/app/src/locales/fr.yaml
Normal file
29
packages/app/src/locales/fr.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
app:
|
||||
title: IT-Tools
|
||||
description: La collection open-source d'outils en ligne pour aider les devs dans leur vie quotidienne.
|
||||
home:
|
||||
all-the-tools: Tous les outils
|
||||
search-tools: Rechercher un outil
|
||||
open-source: Open Source
|
||||
free: Gratuit
|
||||
self-hostable: Self-hostable
|
||||
open-tool: Ouvrir l'outil
|
||||
footer:
|
||||
resources:
|
||||
title: Ressources
|
||||
all-tools: Tous les outils
|
||||
github: Dépôt GitHub
|
||||
support: Soutenir IT-Tools
|
||||
license: Licence
|
||||
support:
|
||||
title: Support
|
||||
report-bug: Signaler un bug
|
||||
request-feature: Demander une fonctionnalité
|
||||
contribute: Contribuer au projet
|
||||
contact: Me contacter
|
||||
friends:
|
||||
title: Ami·e·s
|
||||
tools:
|
||||
token-generator:
|
||||
title: Générateur de token
|
||||
description: Générer des chaines de caractères aléatoires, contrôlez les caractères que vous voulez, lettres majuscules, minuscules, chiffres et/ou symboles.
|
|
@ -1,29 +1,32 @@
|
|||
<script setup>
|
||||
const localePath = useLocalePath();
|
||||
const { t } = useI18n();
|
||||
|
||||
const sections = computed(() => [
|
||||
{
|
||||
title: 'Lorem',
|
||||
title: t('footer.resources.title'),
|
||||
items: [
|
||||
{ label: 'Foo', to: '/foo' },
|
||||
{ label: 'Bar', to: '/bar' },
|
||||
{ label: 'Baz', to: '/baz' },
|
||||
{ label: t('footer.resources.all-tools'), to: localePath('/tools') },
|
||||
{ label: t('footer.resources.github'), href: 'https://github.com/CorentinTh/it-tools' },
|
||||
{ label: t('footer.resources.support'), href: 'https://buymeacoffee.com/cthmsst' },
|
||||
{ label: 'Humans.txt', href: '/humans.txt' },
|
||||
{ label: t('footer.resources.license'), href: 'https://github.com/CorentinTh/it-tools/blob/main/LICENSE' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Ipsum',
|
||||
title: t('footer.support.title'),
|
||||
items: [
|
||||
{ label: 'Foo', to: '/foo' },
|
||||
{ label: 'Bar', to: '/bar' },
|
||||
{ label: 'Baz', to: '/baz' },
|
||||
{ label: t('footer.support.report-bug'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
|
||||
{ label: t('footer.support.request-feature'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
|
||||
{ label: t('footer.support.contribute'), href: 'https://github.com/CorentinTh/it-tools/blob/main/CONTRIBUTING.md' },
|
||||
{ label: t('footer.support.contact'), href: 'https://github.com/CorentinTh/it-tools/issues/new/choose' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Dolor',
|
||||
title: t('footer.friends.title'),
|
||||
items: [
|
||||
{ label: 'Foo', to: '/foo' },
|
||||
{ label: 'Bar', to: '/bar' },
|
||||
{ label: 'Baz', to: '/baz' },
|
||||
{ label: 'Jugly.io', href: 'https://jugly.io' },
|
||||
{ label: 'Enclosed.cc', href: 'https://enclosed.cc' },
|
||||
],
|
||||
},
|
||||
|
||||
|
@ -49,7 +52,7 @@ const socialLinks = [
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="bg-card border-t border-border">
|
||||
<footer class="light:bg-muted/50 dark:bg-black/20 mt-12">
|
||||
<div class="py-12 px-6 max-w-screen-xl mx-auto ">
|
||||
<div class="flex items-start justify-between flex-col md:flex-row gap-12">
|
||||
<div>
|
||||
|
|
|
@ -6,7 +6,7 @@ const colorMode = useColorMode();
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full border-b">
|
||||
<div class="w-full border-b bg-card">
|
||||
<div class="max-w-screen-xl mx-auto flex items-center justify-between py-2 px-6">
|
||||
<NuxtLink variant="link" class="text-xl font-semibold border-b border-transparent hover:no-underline h-auto px-1 rounded-none !transition-border-color-250" :as="Button" :to="localePath('/')" aria-label="Home">
|
||||
<span class="font-bold text-foreground">IT</span>
|
||||
|
|
|
@ -5,7 +5,7 @@ const localePath = useLocalePath();
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex justify-center text-center">
|
||||
<div class="flex justify-center text-center mt-24">
|
||||
<div>
|
||||
<h1 class="text-3xl font-light text-muted-foreground">
|
||||
404
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<script setup>
|
||||
import { Badge } from '@/src/modules/ui/components/badge';
|
||||
import { Button } from '@/src/modules/ui/components/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/src/modules/ui/components/dropdown-menu';
|
||||
import { Button, buttonVariants } from '@/src/modules/ui/components/button';
|
||||
import { cn } from '../../shared/style/cn';
|
||||
import { useToolsStore } from '../../tools/tools.store';
|
||||
import { CardContent } from '../../ui/components/card';
|
||||
import Card from '../../ui/components/card/Card.vue';
|
||||
|
||||
const { tools } = useToolsStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -10,16 +15,13 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||
<div class="max-w-xl">
|
||||
<div class="flex gap-2">
|
||||
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
|
||||
<!-- {{ $t('landing.hero.badges.open-source') }} -->
|
||||
Open Source
|
||||
{{ $t('home.open-source') }}
|
||||
</Badge>
|
||||
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
|
||||
<!-- {{ $t('landing.hero.badges.free') }} -->
|
||||
Free
|
||||
{{ $t('home.free') }}
|
||||
</Badge>
|
||||
<Badge class="text-primary bg-primary/10 hover:bg-primary/10">
|
||||
<!-- {{ $t('landing.hero.badges.self-hostable') }} -->
|
||||
Self-hostable
|
||||
{{ $t('home.self-hostable') }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
|
@ -29,21 +31,18 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||
</h1>
|
||||
|
||||
<p class="text-xl text-gray-400 mb-4">
|
||||
<!-- {{ $t('app.description') }} -->
|
||||
The open-source collection of handy online tools to help developers in their daily life.
|
||||
{{ $t('app.description') }}
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<Button>
|
||||
<!-- {{ $t('landing.hero.all-the-tools') }} -->
|
||||
All the tools
|
||||
{{ $t('home.all-the-tools') }}
|
||||
<Icon name="i-tabler-arrow-right" class="ml-2 size-4" />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline">
|
||||
<!-- {{ $t('landing.hero.search-tools') }} -->
|
||||
<Icon name="i-tabler-search" class="mr-2 size-4" />
|
||||
Search tools
|
||||
{{ $t('home.search-tools') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -54,4 +53,22 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
|||
</div>
|
||||
</div>
|
||||
</grid-background>
|
||||
|
||||
<div class="max-w-screen-xl mx-auto px-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-12">
|
||||
<NuxtLink v-for="tool in tools" :key="tool.key" :to="tool.path">
|
||||
<Card class="p-6 h-full cursor-pointer hover:shadow-lg transition hover:translate-y-[-2px]">
|
||||
<Icon :name="tool.icon" class="size-12 text-muted-foreground/60" />
|
||||
|
||||
<div class="font-semibold text-base">
|
||||
{{ tool.title }}
|
||||
</div>
|
||||
|
||||
<p class="text-muted-foreground mt-2">
|
||||
{{ tool.description }}
|
||||
</p>
|
||||
</Card>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
// @vitest-environment nuxt
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { useRefreshableState } from './useRefreshableState';
|
||||
|
||||
describe('useRefreshableState composables', () => {
|
||||
describe('useRefreshableState', () => {
|
||||
test('the tuple provided by useRefreshableState contain the state that is the result of the provided function and a refresh function', () => {
|
||||
let index = 0;
|
||||
|
||||
const [state, refresh] = useRefreshableState('key', () => ++index);
|
||||
|
||||
expect(state.value).to.equal(1);
|
||||
expect(index).to.equal(1);
|
||||
|
||||
refresh();
|
||||
|
||||
expect(state.value).to.equal(2);
|
||||
expect(index).to.equal(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import { get } from '@vueuse/core';
|
||||
|
||||
export function useRefreshableState<T>(key: string, getState: () => T | Ref<T>) {
|
||||
const state = useState(key, getState);
|
||||
|
||||
const refresh = () => {
|
||||
const value = getState();
|
||||
state.value = get(value);
|
||||
};
|
||||
|
||||
return [state, refresh] as const;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { sample as sampleImpl, times } from 'lodash-es';
|
||||
|
||||
export function createToken({
|
||||
withUppercase = true,
|
||||
withLowercase = true,
|
||||
withNumbers = true,
|
||||
withSymbols = false,
|
||||
length = 64,
|
||||
alphabet,
|
||||
sample = sampleImpl,
|
||||
}: {
|
||||
withUppercase?: boolean;
|
||||
withLowercase?: boolean;
|
||||
withNumbers?: boolean;
|
||||
withSymbols?: boolean;
|
||||
length?: number;
|
||||
alphabet?: string;
|
||||
sample?: (str: string) => string;
|
||||
}) {
|
||||
const allAlphabet = alphabet ?? [
|
||||
withUppercase ? 'ABCDEFGHIJKLMOPQRSTUVWXYZ' : '',
|
||||
withLowercase ? 'abcdefghijklmopqrstuvwxyz' : '',
|
||||
withNumbers ? '0123456789' : '',
|
||||
withSymbols ? '.,;:!?./-"\'#{([-|\\@)]=}*+' : '',
|
||||
].join('');
|
||||
|
||||
return times(length, () => sample(allAlphabet)).join('');
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { defineTool } from '../../tools.models';
|
||||
|
||||
export const tokenGeneratorTool = defineTool({
|
||||
slug: 'token-generator',
|
||||
entryFile: './token-generator.vue',
|
||||
icon: 'i-tabler-key',
|
||||
createdAt: new Date('2024-02-13'),
|
||||
currentDirUrl: import.meta.url,
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
<script setup lang="ts">
|
||||
import { useRefreshableState } from '~/src/modules/shared/composables/useRefreshableState';
|
||||
import { Button } from '~/src/modules/ui/components/button';
|
||||
import { createToken } from './token-generator.models';
|
||||
|
||||
const withUppercase = ref(true);
|
||||
const withLowercase = ref(true);
|
||||
const withNumbers = ref(true);
|
||||
const withSymbols = ref(false);
|
||||
const length = ref(64);
|
||||
|
||||
const [token, refreshToken] = useRefreshableState(
|
||||
'token-generator:token',
|
||||
() => createToken({
|
||||
withUppercase: withUppercase.value,
|
||||
withLowercase: withLowercase.value,
|
||||
withNumbers: withNumbers.value,
|
||||
withSymbols: withSymbols.value,
|
||||
length: length.value,
|
||||
}),
|
||||
);
|
||||
|
||||
watch([withUppercase, withLowercase, withNumbers, withSymbols, length], refreshToken);
|
||||
|
||||
// const { copy: copyToken } = useCopy({ source: token, notificationText: 'Token copied to clipboard' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-screen-md mx-auto p-6">
|
||||
<div>{{ token }}</div>
|
||||
|
||||
<Button class="mt-4" @click="refreshToken">
|
||||
Generate new token
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
21
packages/app/src/modules/tools/modules/tools.modules.ts
Normal file
21
packages/app/src/modules/tools/modules/tools.modules.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { defineNuxtModule, extendPages } from '@nuxt/kit';
|
||||
import { toolDefinitions } from '../tools.registry';
|
||||
|
||||
export default defineNuxtModule({
|
||||
meta: {
|
||||
name: 'tools',
|
||||
},
|
||||
setup() {
|
||||
extendPages((pages) => {
|
||||
pages.push(...toolDefinitions.map((tool) => {
|
||||
return {
|
||||
path: `/${tool.slug}`,
|
||||
file: tool.entryFile,
|
||||
meta: {
|
||||
toolKey: tool.key,
|
||||
},
|
||||
};
|
||||
}));
|
||||
});
|
||||
},
|
||||
});
|
18
packages/app/src/modules/tools/tools.models.ts
Normal file
18
packages/app/src/modules/tools/tools.models.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export function defineTool(toolDefinition: {
|
||||
slug: string;
|
||||
entryFile: string;
|
||||
currentDirUrl: string;
|
||||
icon: string;
|
||||
createdAt: Date;
|
||||
}) {
|
||||
const entryFile = new URL(toolDefinition.entryFile, toolDefinition.currentDirUrl).pathname;
|
||||
const baseGithubUrlPath = entryFile.match(/(\/tools\/.*)$/)?.[1];
|
||||
const entryFileGithubUrl = `https://github.com/CorentinTh/crucials-tools/blob/main${baseGithubUrlPath}`;
|
||||
|
||||
return {
|
||||
...toolDefinition,
|
||||
key: toolDefinition.slug,
|
||||
entryFile,
|
||||
entryFileGithubUrl,
|
||||
};
|
||||
}
|
5
packages/app/src/modules/tools/tools.registry.ts
Normal file
5
packages/app/src/modules/tools/tools.registry.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { tokenGeneratorTool } from './definitions/token-generator/token-generator.tool';
|
||||
|
||||
export const toolDefinitions = [
|
||||
tokenGeneratorTool,
|
||||
];
|
36
packages/app/src/modules/tools/tools.store.ts
Normal file
36
packages/app/src/modules/tools/tools.store.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { joinUrlPaths } from '@corentinth/chisels';
|
||||
import { toolDefinitions } from './tools.registry';
|
||||
|
||||
export const useToolsStore = defineStore('tools', () => {
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const localizedTools = computed(() => {
|
||||
return toolDefinitions.map((tool) => {
|
||||
const { key, slug } = tool;
|
||||
|
||||
return {
|
||||
...tool,
|
||||
title: t(`tools.${key}.title`),
|
||||
description: t(`tools.${key}.description`),
|
||||
path: `/${joinUrlPaths(locale.value, slug)}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
tools: localizedTools,
|
||||
getToolByKey({ key }: { key: unknown }) {
|
||||
if (typeof key !== 'string') {
|
||||
throw new TypeError('Invalid key');
|
||||
}
|
||||
|
||||
const tool = localizedTools.value.find(tool => tool.key === key);
|
||||
|
||||
if (!tool) {
|
||||
throw new Error('Tool not found');
|
||||
}
|
||||
|
||||
return tool;
|
||||
},
|
||||
};
|
||||
});
|
21
packages/app/src/modules/ui/components/card/Card.vue
Normal file
21
packages/app/src/modules/ui/components/card/Card.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'rounded-xl border bg-card text-card-foreground shadow',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
14
packages/app/src/modules/ui/components/card/CardContent.vue
Normal file
14
packages/app/src/modules/ui/components/card/CardContent.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p :class="cn('text-sm text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
14
packages/app/src/modules/ui/components/card/CardFooter.vue
Normal file
14
packages/app/src/modules/ui/components/card/CardFooter.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex items-center p-6 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
14
packages/app/src/modules/ui/components/card/CardHeader.vue
Normal file
14
packages/app/src/modules/ui/components/card/CardHeader.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
18
packages/app/src/modules/ui/components/card/CardTitle.vue
Normal file
18
packages/app/src/modules/ui/components/card/CardTitle.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/src/modules/shared/style/cn'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h3
|
||||
:class="
|
||||
cn('font-semibold leading-none tracking-tight', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
</template>
|
6
packages/app/src/modules/ui/components/card/index.ts
Normal file
6
packages/app/src/modules/ui/components/card/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export { default as Card } from './Card.vue'
|
||||
export { default as CardContent } from './CardContent.vue'
|
||||
export { default as CardDescription } from './CardDescription.vue'
|
||||
export { default as CardFooter } from './CardFooter.vue'
|
||||
export { default as CardHeader } from './CardHeader.vue'
|
||||
export { default as CardTitle } from './CardTitle.vue'
|
Loading…
Add table
Add a link
Reference in a new issue