@minch/bentolio 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,3 @@
1
+ # For OG Image Generator
2
+ NUXT_SITE_URL=https://bento.minch.dev
3
+ NUXT_SITE_NAME='Bentolio'
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dawit Urgessa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Bentolio
2
+
3
+ A minimal portfolio template built with Nuxt 4, Tailwind CSS 4, and GSAP.
4
+
5
+ All content and basic styling are configured via `app/app.config.ts`.
6
+
7
+ ![Screenshot](/public/screenshot.png)
8
+
9
+ > For the original Astro version (not actively updated), check the [`astro`](https://github.com/oneminch/Bentolio/tree/astro) branch.
10
+
11
+ ## Features
12
+
13
+ - **Nuxt 4**: Modern DX, serverless-friendly, and SSG by default
14
+ - **Tailwind CSS 4**: Zero-config utility classes with CSS-first authoring
15
+ - **GSAP animations**: Smooth entrance animations and micro‑interactions
16
+ - **Dark Mode**: Toggle via `@nuxtjs/color-mode` (class strategy)
17
+ - **Icons**: `@nuxt/icon` with Iconify collections (e.g., `ph`, `simple-icons`)
18
+ - **Type‑safe app config**: Autocompletion and validation for your site data
19
+ - **Static generation**: Pre‑rendered pages for easy deployment to any static host
20
+ - **OG Image**: Automatically generated OG image via `nuxt-og-image`
21
+
22
+ ## Project Structure
23
+
24
+ ```
25
+ app/
26
+ app.config.ts # Portfolio data and style options (edit me)
27
+ app.vue # Single-page portfolio content using app config
28
+ assets/css/main.css # Tailwind entry + custom theme/utilities
29
+ components/ # UI components (Header, Footer, Social link, Theme toggle)
30
+ layouts/default.vue # Layout + SEO meta + GSAP entrance animations
31
+ plugins/gsap.client.ts # Nuxt plugin providing `$gsap`
32
+ utils/types.ts # Types for the app config
33
+ public/
34
+ favicon.svg, logo.svg, profile.jpg, robots.txt
35
+ nuxt.config.ts # Nuxt configuration
36
+ package.json # Scripts and dependencies
37
+ ```
38
+
39
+ ## Configure Your Portfolio
40
+
41
+ All site data lives in `app/app.config.ts` and is fully typed (see `app/utils/types.ts`). Edit the example values to your own:
42
+
43
+ ```ts
44
+ // app/app.config.ts (excerpt)
45
+ export default defineAppConfig({
46
+ style: {
47
+ roundedItems: false
48
+ },
49
+ portfolio: {
50
+ name: "Your Name",
51
+ subtitle: "What you do",
52
+ // company: "[COMPANY]",
53
+ avatar: "/profile.jpg",
54
+ bio: {
55
+ // Prefer html for rich content; fallback to text if html is empty
56
+ html: "I'm a <span class='font-bold text-primary'>Full‑Stack</span> Dev...",
57
+ text: "I'm a Full‑Stack Dev..."
58
+ },
59
+ socials: {
60
+ GitHub: {
61
+ socialMediaIcon: "simple-icons:github",
62
+ profileUrl: "https://github.com/your-handle",
63
+ socialMediaBgColor: "bg-black!",
64
+ socialMediaBorderColor: "border-black!",
65
+ socialMediaTextColor: "text-white!"
66
+ }
67
+ // X, LinkedIn, Bluesky are also supported
68
+ },
69
+ cta: {
70
+ contact: "you@example.com",
71
+ location: "Your City",
72
+ link: { url: "https://your-site.com", label: "Portfolio" }
73
+ }
74
+ }
75
+ });
76
+ ```
77
+
78
+ Supported social keys by default: `"X" | "LinkedIn" | "Bluesky" | "GitHub"`. To add more networks, extend the `SocialMediaKey` union and the `socials` mapping in `app/utils/types.ts`. The layout is designed to only display 4 social links by default.
79
+
80
+ ### Assets
81
+
82
+ - Replace `public/profile.jpg` with your own avatar.
83
+ - Update `public/favicon.svg` and `public/logo.svg` if desired.
84
+ - OG image is auto-generated using the `nuxt-og-image` module.
85
+
86
+ ### Theming and Styles
87
+
88
+ - Tailwind entry is `app/assets/css/main.css`.
89
+ - Primary color is set via CSS theme variable:
90
+
91
+ ```css
92
+ /* Change `--color-primary` to customize your accent. */
93
+ @theme {
94
+ --color-primary: var(--color-lime-500);
95
+ }
96
+ ```
97
+
98
+ ### Animations
99
+
100
+ - GSAP is provided via a Nuxt plugin (`$gsap`). See `app/layouts/default.vue` for entrance animations.
101
+ - Elements with the `animate-element` utility class will animate in on page load. The initial state and transitions are defined in `app/assets/css/main.css`.
102
+ - The waving hand emoji animation is controlled via the `#wave` id in CSS.
103
+
104
+ ## Deployment
105
+
106
+ This template is configured for static generation via `routeRules` in `nuxt.config.ts`.
107
+
108
+ ## Credit
109
+
110
+ The design is inspired by [@TomIsLoading](https://twitter.com/TomIsLoading)'s Bento Grid from [Hover.dev](https://hover.dev).
@@ -0,0 +1,53 @@
1
+ export default defineAppConfig({
2
+ style: {
3
+ roundedItems: false,
4
+ },
5
+ portfolio: {
6
+ name: "Dawit",
7
+ subtitle: "I Craft Delightful Web Apps",
8
+ // company: "[COMPANY]",
9
+ avatar: "/profile.jpg",
10
+ bio: {
11
+ html: "I'm a Full-Stack Problem-Solver specializing in <span class='font-bold text-primary'>Vue</span>, <span class='font-bold text-primary'>Nuxt</span>, <span class='font-bold text-primary'>Tailwind CSS</span>, and <span class='font-bold text-primary'>Node</span>. I sometimes write articles on various software development topics. I'm also a hobbyist photographer.",
12
+ text: "I'm a Full-Stack Problem-Solver Specializing in Vue, Nuxt, Tailwind CSS and Node.js. I Sometimes Write Articles on Various Web Dev Topics. I'm Also a Hobbyist Photographer.",
13
+ },
14
+ socials: {
15
+ Bluesky: {
16
+ socialMediaIcon: "simple-icons:bluesky",
17
+ profileUrl: "https://bsky.app/profile/minch.dev",
18
+ socialMediaBgColor: "bg-blue-500!",
19
+ socialMediaBorderColor: "border-blue-500!",
20
+ socialMediaTextColor: "text-white!",
21
+ },
22
+ GitHub: {
23
+ socialMediaIcon: "simple-icons:github",
24
+ profileUrl: "https://x.com/oneminch",
25
+ socialMediaBgColor: "bg-black!",
26
+ socialMediaBorderColor: "border-black!",
27
+ socialMediaTextColor: "text-white!",
28
+ },
29
+ X: {
30
+ socialMediaIcon: "simple-icons:x",
31
+ profileUrl: "https://x.com/oneminch",
32
+ socialMediaBgColor: "bg-black!",
33
+ socialMediaBorderColor: "border-black!",
34
+ socialMediaTextColor: "text-white!",
35
+ },
36
+ LinkedIn: {
37
+ socialMediaIcon: "simple-icons:linkedin",
38
+ profileUrl: "https://linkedin.com/in/oneminch",
39
+ socialMediaBgColor: "bg-blue-600!",
40
+ socialMediaBorderColor: "border-blue-600!",
41
+ socialMediaTextColor: "text-white!",
42
+ },
43
+ },
44
+ cta: {
45
+ contact: "[firstname]@example.com",
46
+ location: "Planet Earth",
47
+ link: {
48
+ url: "https://github.com/oneminch/Bentolio",
49
+ label: "Source Code",
50
+ },
51
+ },
52
+ },
53
+ });
package/app/app.vue ADDED
@@ -0,0 +1,156 @@
1
+ <script setup lang="ts">
2
+ import type { SocialMediaKey } from "~";
3
+
4
+ const { portfolio, style } = useAppConfig();
5
+
6
+ const {
7
+ name,
8
+ subtitle,
9
+ avatar,
10
+ company,
11
+ bio,
12
+ cta: { contact, location, link },
13
+ socials
14
+ } = portfolio;
15
+
16
+ const { roundedItems } = style;
17
+
18
+ const topFourSocials = Object.fromEntries(
19
+ Object.entries(socials).slice(0, 4)
20
+ );
21
+
22
+ const numSocials = Object.entries(topFourSocials).length;
23
+ const socialLinkClass = {
24
+ "col-span-full lg:col-span-4 lg:row-span-2": numSocials === 1,
25
+ "col-span-6 lg:col-span-4": numSocials === 2,
26
+ "col-span-4 last:col-span-4 lg:col-span-2 lg:last:col-span-4":
27
+ numSocials === 3,
28
+ "col-span-3 lg:col-span-2": numSocials === 4
29
+ };
30
+
31
+ defineOgImageComponent("SocialImage", {
32
+ name: name,
33
+ subtitle: subtitle,
34
+ avatar: avatar
35
+ });
36
+ </script>
37
+
38
+ <template>
39
+ <NuxtRouteAnnouncer />
40
+ <NuxtLayout>
41
+ <article
42
+ class="flex flex-col gap-4 *:grid *:grid-cols-12 *:gap-4 *:w-full *:*:min-h-16 *:*:py-4 *:*:px-6 sm:*:*:py-6 sm:*:*:px-10 *:*:border *:*:border-zinc-200 dark:*:*:border-zinc-700 *:*:bg-zinc-50/50 dark:*:*:bg-zinc-800/25 *:text-zinc-950 dark:*:text-zinc-300">
43
+ <!-- Hero -->
44
+ <div class="grid-rows-2">
45
+ <!-- Intro -->
46
+ <div
47
+ :class="[
48
+ 'animate-element col-span-full lg:col-span-8 row-span-2',
49
+ { 'rounded-lg': roundedItems }
50
+ ]">
51
+ <img
52
+ v-if="avatar"
53
+ :src="avatar"
54
+ alt="My Profile Picture"
55
+ class="size-20 mb-4 mx-auto lg:mx-0 bg-transparent rounded-full ring ring-zinc-200 dark:ring-zinc-800 p-0.5" />
56
+
57
+ <div class="space-y-4 py-6 sm:pb-8">
58
+ <h1
59
+ class="text-2xl sm:text-3xl lg:text-4xl text-center lg:text-left font-bold *:inline-block">
60
+ Hi, I'm {{ name }}
61
+ <span id="wave">👋</span>
62
+ </h1>
63
+ <h2
64
+ class="text-center lg:text-left font-bold *:inline-block text-zinc-500 dark:text-primary text-xl sm:text-2xl lg:text-3xl">
65
+ <span>{{ subtitle }}</span
66
+ ><span v-if="company">&nbsp;@&nbsp;{{ company }}</span>
67
+ </h2>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Social Media -->
72
+ <social-media-link
73
+ v-for="(item, key) in topFourSocials"
74
+ :key="key"
75
+ :to="item.profileUrl"
76
+ :name="key as SocialMediaKey"
77
+ :icon="item.socialMediaIcon"
78
+ :style="{
79
+ backgroundColor: item.socialMediaBgColor,
80
+ textColor: item.socialMediaTextColor,
81
+ borderColor: item.socialMediaBorderColor
82
+ }"
83
+ :class="[
84
+ 'animate-element',
85
+ socialLinkClass,
86
+ { 'rounded-lg': roundedItems }
87
+ ]" />
88
+ </div>
89
+
90
+ <!-- About -->
91
+ <div class="grid-rows-1">
92
+ <p
93
+ v-if="bio.html"
94
+ :class="[
95
+ 'animate-element leading-relaxed col-span-full font-medium gap-4 py-6! sm:py-10! text-zinc-700 dark:text-zinc-300 text-lg sm:text-xl lg:text-2xl',
96
+ { 'rounded-lg': roundedItems }
97
+ ]"
98
+ v-html="bio.html"></p>
99
+ <p
100
+ v-else
101
+ :class="[
102
+ 'animate-element leading-relaxed col-span-full font-medium gap-4 py-6! sm:py-10! text-zinc-700 dark:text-zinc-300 text-lg sm:text-xl lg:text-2xl',
103
+ { 'rounded-lg': roundedItems }
104
+ ]">
105
+ {{ bio.text }}
106
+ </p>
107
+ </div>
108
+
109
+ <!-- Miscellany -->
110
+ <div
111
+ class="grid-rows-1 grid-cols-1! sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))]!">
112
+ <!-- Link -->
113
+ <nuxt-link
114
+ v-if="link"
115
+ :to="link.url"
116
+ :class="[
117
+ 'animate-element flex flex-col items-center justify-center gap-y-2 focus-visible:global-focus hover:scale-105!',
118
+ { 'rounded-lg': roundedItems }
119
+ ]">
120
+ <Icon
121
+ name="ph:arrow-square-up-right-duotone"
122
+ class="text-3xl lg:text-4xl text-primary" />
123
+ <span class="text-center text-lg">
124
+ {{ link.label }}
125
+ </span>
126
+ </nuxt-link>
127
+
128
+ <!-- Location -->
129
+ <p
130
+ :class="[
131
+ 'animate-element flex flex-col items-center justify-center gap-y-2',
132
+ { 'rounded-lg': roundedItems }
133
+ ]">
134
+ <Icon
135
+ name="ph:map-pin-duotone"
136
+ class="text-3xl lg:text-4xl text-primary" />
137
+ <span class="text-center text-lg"> {{ location }} </span>
138
+ </p>
139
+
140
+ <!-- Contact -->
141
+ <p
142
+ :class="[
143
+ 'animate-element flex flex-col items-center justify-center gap-y-2',
144
+ { 'rounded-lg': roundedItems }
145
+ ]">
146
+ <Icon
147
+ name="ph:chat-circle-text-duotone"
148
+ class="text-3xl lg:text-4xl text-primary" />
149
+ <span class="text-center text-lg">
150
+ {{ contact }}
151
+ </span>
152
+ </p>
153
+ </div>
154
+ </article>
155
+ </NuxtLayout>
156
+ </template>
@@ -0,0 +1,85 @@
1
+ @import "tailwindcss";
2
+ @custom-variant dark (&:where(.dark, .dark *));
3
+
4
+ @theme {
5
+ --color-primary: var(--color-lime-500);
6
+ }
7
+
8
+ @utility global-focus {
9
+ @apply outline-none ring-2 ring-offset-0 ring-primary/75;
10
+ }
11
+
12
+ @utility animate-element {
13
+ @apply transition-transform opacity-0;
14
+ }
15
+
16
+ * {
17
+ @apply box-border selection:bg-primary/50;
18
+ }
19
+
20
+ body {
21
+ font-family: "Space Grotesk", sans-serif;
22
+ word-wrap: break-word;
23
+ @apply w-full bg-white dark:bg-zinc-900;
24
+ }
25
+
26
+ #app {
27
+ @apply max-w-5xl min-w-xs w-full bg-white dark:bg-zinc-900 mx-auto break-words my-0 p-4 sm:p-6 md:p-12;
28
+ }
29
+
30
+ /* Animated Wave Emoji */
31
+ #wave {
32
+ @apply inline-block ml-1 origin-[70%_70%] animate-[10s_ease_2s_infinite_wave] hover:animate-[1.5s_ease_hoverwave];
33
+ }
34
+
35
+ @keyframes wave {
36
+ 0% {
37
+ transform: rotate(0deg);
38
+ }
39
+ 2.5%,
40
+ 7.5% {
41
+ transform: rotate(14deg);
42
+ }
43
+ 5% {
44
+ transform: rotate(-8deg);
45
+ }
46
+ 10% {
47
+ transform: rotate(-4deg);
48
+ }
49
+ 12.5% {
50
+ transform: rotate(10deg);
51
+ }
52
+ 15% {
53
+ transform: rotate(0deg);
54
+ }
55
+ 100% {
56
+ transform: rotate(0deg);
57
+ }
58
+ }
59
+
60
+ @keyframes hoverwave {
61
+ 0% {
62
+ transform: rotate(0deg);
63
+ }
64
+ 10% {
65
+ transform: rotate(14deg);
66
+ }
67
+ 20% {
68
+ transform: rotate(-8deg);
69
+ }
70
+ 30% {
71
+ transform: rotate(14deg);
72
+ }
73
+ 40% {
74
+ transform: rotate(-4deg);
75
+ }
76
+ 50% {
77
+ transform: rotate(10deg);
78
+ }
79
+ 60% {
80
+ transform: rotate(0deg);
81
+ }
82
+ 100% {
83
+ transform: rotate(0deg);
84
+ }
85
+ }
@@ -0,0 +1,5 @@
1
+ <template>
2
+ <footer class="text-center py-4 space-y-2 text-zinc-600 dark:text-zinc-400">
3
+ <p class="flex items-center">&copy; {{ new Date().getFullYear() }}</p>
4
+ </footer>
5
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ const {
3
+ portfolio: { name },
4
+ } = useAppConfig();
5
+ </script>
6
+
7
+ <template>
8
+ <header class="py-0 sm:py-4">
9
+ <nav class="flex items-center justify-between">
10
+ <h2 class="text-center">
11
+ <a
12
+ href="/"
13
+ class="flex items-center gap-x-4 p-1 font-semibold text-xl text-zinc-950 dark:text-zinc-300 focus-visible:global-focus"
14
+ >
15
+ <img
16
+ src="/logo.svg"
17
+ alt="Logo"
18
+ class="size-10 rounded-xl ring ring-zinc-200 dark:ring-zinc-800 p-0.5"
19
+ />
20
+ {{ name }}
21
+ </a>
22
+ </h2>
23
+
24
+ <theme-toggle />
25
+ </nav>
26
+ </header>
27
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import type { Portfolio } from "#imports";
3
+
4
+ withDefaults(defineProps<Pick<Portfolio, "name" | "subtitle" | "avatar">>(), {
5
+ name: "Full Name",
6
+ subtitle: "Whatever It Is I Do",
7
+ });
8
+ </script>
9
+
10
+ <template>
11
+ <div
12
+ :style="{
13
+ background:
14
+ '#18181b radial-gradient(circle at 1px 1px, #3f3f46 1px, transparent 0) repeat 0 0 / 20px 20px',
15
+ }"
16
+ class="flex h-full w-full items-center justify-center overflow-hidden p-16 text-zinc-200 bg-zinc-900"
17
+ >
18
+ <div class="flex flex-row items-center justify-center gap-16">
19
+ <div class="flex flex-col items-center justify-around gap-6 text-left">
20
+ <h1 class="text-8xl font-black">{{ name }}</h1>
21
+ <p class="text-4xl font-bold">{{ subtitle }}</p>
22
+ </div>
23
+
24
+ <!-- <div
25
+ v-if="avatar"
26
+ class="mx-auto w-44 h-44 shrink-0 rounded-full bg-cover bg-center p-1 ring ring-zinc-700"
27
+ :style="{ backgroundImage: `url(${avatar})` }"
28
+ /> -->
29
+ </div>
30
+ </div>
31
+ </template>
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ import type { SocialMediaKey, SocialMediaValue } from "~";
3
+
4
+ defineProps<{
5
+ name: SocialMediaKey;
6
+ icon: SocialMediaValue["socialMediaIcon"];
7
+ to: SocialMediaValue["profileUrl"];
8
+ style: {
9
+ backgroundColor: SocialMediaValue["socialMediaBgColor"];
10
+ borderColor: SocialMediaValue["socialMediaBorderColor"];
11
+ textColor: SocialMediaValue["socialMediaTextColor"];
12
+ };
13
+ }>();
14
+ </script>
15
+
16
+ <template>
17
+ <nuxt-link
18
+ :to="to"
19
+ :title="name"
20
+ :aria-label="name"
21
+ :class="[
22
+ 'flex items-center justify-center text-2xl sm:text-3xl p-0! hover:scale-105! focus-visible:global-focus',
23
+ style.backgroundColor,
24
+ style.textColor,
25
+ style.borderColor,
26
+ ]"
27
+ >
28
+ <Icon :name="icon" />
29
+ </nuxt-link>
30
+ </template>
@@ -0,0 +1,26 @@
1
+ <script setup lang="ts">
2
+ const colorMode = useColorMode();
3
+ const onClick = () =>
4
+ colorMode.value === "light"
5
+ ? (colorMode.preference = "dark")
6
+ : (colorMode.preference = "light");
7
+ </script>
8
+
9
+ <template>
10
+ <button
11
+ aria-label="Color Mode"
12
+ class="w-10 h-10 flex items-center justify-center text-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50/50 dark:bg-zinc-800/25 hover:bg-zinc-200 dark:hover:bg-zinc-700 *:text-zinc-950 dark:*:text-zinc-300 focus-visible:global-focus"
13
+ @click="onClick"
14
+ >
15
+ <ColorScheme placeholder="...">
16
+ <template v-if="colorMode.value === 'dark'">
17
+ <Icon name="ph:sun-duotone" class="w-5 h-5" />
18
+ <span class="sr-only">Dark Mode</span>
19
+ </template>
20
+ <template v-else>
21
+ <Icon name="ph:moon-duotone" class="w-5 h-5" />
22
+ <span class="sr-only">Light Mode</span>
23
+ </template>
24
+ </ColorScheme>
25
+ </button>
26
+ </template>
package/app/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./utils/types";
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ import "@fontsource/space-grotesk/400.css";
3
+ import "@fontsource/space-grotesk/500.css";
4
+ import "@fontsource/space-grotesk/600.css";
5
+ import "@fontsource/space-grotesk/700.css";
6
+ import gsap from "gsap";
7
+
8
+ const { portfolio } = useAppConfig();
9
+
10
+ const { name, subtitle, bio } = portfolio;
11
+
12
+ const { $gsap } = useNuxtApp();
13
+
14
+ const ctx = ref<gsap.Context | null>(null);
15
+
16
+ onMounted(() => {
17
+ ctx.value = $gsap.context(() => {
18
+ $gsap.fromTo(
19
+ ".animate-layout",
20
+ { opacity: 0 },
21
+ {
22
+ opacity: 1,
23
+ duration: 0.75,
24
+ delay: 0.5,
25
+ ease: "elastic.out(0.5, 0.3)",
26
+ }
27
+ );
28
+ $gsap.fromTo(
29
+ ".animate-element",
30
+ { scale: 0.75, opacity: 0, y: 20 },
31
+ {
32
+ scale: 1,
33
+ opacity: 1,
34
+ y: 0,
35
+ duration: 0.15,
36
+ delay: 0.9,
37
+ ease: "elastic.out(0.5, 0.3)",
38
+ stagger: 0.1,
39
+ }
40
+ );
41
+ });
42
+ });
43
+
44
+ onUnmounted(() => {
45
+ ctx.value && ctx.value.revert();
46
+ });
47
+
48
+ useHead({
49
+ bodyAttrs: {
50
+ class: "animate-layout opacity-0",
51
+ },
52
+ });
53
+
54
+ useSeoMeta({
55
+ title: `${name} · ${subtitle}`,
56
+ description: bio.text,
57
+ ogTitle: `${name} · ${subtitle}`,
58
+ ogDescription: bio.text,
59
+ twitterCard: "summary_large_image",
60
+ });
61
+ </script>
62
+
63
+ <template>
64
+ <div class="space-y-5 sm:space-y-10">
65
+ <app-header />
66
+
67
+ <main class="min-h-full">
68
+ <slot />
69
+ </main>
70
+
71
+ <app-footer />
72
+ </div>
73
+ </template>
@@ -0,0 +1,9 @@
1
+ import { gsap } from "gsap";
2
+
3
+ export default defineNuxtPlugin((nuxtApp) => {
4
+ return {
5
+ provide: {
6
+ gsap,
7
+ },
8
+ };
9
+ });
@@ -0,0 +1,42 @@
1
+ export type SocialMediaKey = "X" | "LinkedIn" | "Bluesky" | "GitHub";
2
+
3
+ export interface SocialMediaValue {
4
+ socialMediaIcon: string;
5
+ socialMediaBgColor: string;
6
+ socialMediaBorderColor: string;
7
+ socialMediaTextColor: string;
8
+ profileUrl: string;
9
+ }
10
+
11
+ export interface Portfolio {
12
+ name: string;
13
+ subtitle: string;
14
+ avatar?: string;
15
+ company?: string;
16
+ socials: Partial<Record<SocialMediaKey, SocialMediaValue>>;
17
+ bio: {
18
+ html?: string;
19
+ text: string;
20
+ };
21
+ cta: {
22
+ contact?: string;
23
+ location?: string;
24
+ link?: { url: string; label: string };
25
+ };
26
+ }
27
+
28
+ export interface Style {
29
+ roundedItems: boolean;
30
+ }
31
+
32
+ declare module "nuxt/schema" {
33
+ interface AppConfigInput {
34
+ portfolio: Portfolio;
35
+ style?: Style;
36
+ }
37
+
38
+ interface AppConfig {
39
+ portfolio: Portfolio;
40
+ style?: Style;
41
+ }
42
+ }
@@ -0,0 +1,11 @@
1
+ // @ts-check
2
+ import withNuxt from './.nuxt/eslint.config.mjs'
3
+
4
+ export default withNuxt(
5
+ // Your custom configs here
6
+ {
7
+ rules: {
8
+ "vue/html-self-closing": "off",
9
+ }
10
+ }
11
+ )
package/nuxt.config.ts ADDED
@@ -0,0 +1,51 @@
1
+ import tailwindcss from "@tailwindcss/vite";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ const currentDir = dirname(fileURLToPath(import.meta.url));
6
+
7
+ // https://nuxt.com/docs/api/configuration/nuxt-config
8
+ export default defineNuxtConfig({
9
+ app: {
10
+ rootAttrs: {
11
+ id: "app"
12
+ },
13
+ head: {
14
+ link: [{ rel: "icon", type: "image/svg+xml", href: "/favicon.svg" }]
15
+ }
16
+ },
17
+
18
+ build: {
19
+ transpile: ["gsap"]
20
+ },
21
+
22
+ colorMode: { classSuffix: "" },
23
+
24
+ compatibilityDate: "2025-05-15",
25
+
26
+ css: [join(currentDir, "./app/assets/css/tailwind.css")],
27
+
28
+ alias: {
29
+ "@minch/bentolio/tailwind": join(
30
+ currentDir,
31
+ "./app/assets/css/tailwind.css"
32
+ )
33
+ },
34
+
35
+ devtools: { enabled: true },
36
+
37
+ modules: [
38
+ "@nuxt/eslint",
39
+ "@nuxt/icon",
40
+ "@nuxtjs/color-mode",
41
+ "nuxt-og-image"
42
+ ],
43
+
44
+ routeRules: {
45
+ "/*": { prerender: true }
46
+ },
47
+
48
+ vite: {
49
+ plugins: [tailwindcss()]
50
+ }
51
+ });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@minch/bentolio",
3
+ "version": "0.9.1",
4
+ "description": "A minimal portfolio template and layer built with Nuxt 4, Tailwind CSS 4, and GSAP.",
5
+ "type": "module",
6
+ "repository": {
7
+ "url": "https://github.com/oneminch/bentolio"
8
+ },
9
+ "homepage": "https://github.com/oneminch/bentolio#readme",
10
+ "license": "MIT",
11
+ "author": {
12
+ "name": "Dawit",
13
+ "url": "https://bsky.app/profile/minch.dev"
14
+ },
15
+ "main": "./nuxt.config.ts",
16
+ "files": [
17
+ "nuxt.config.ts",
18
+ "app/**/*",
19
+ "public/**/*",
20
+ ".env.example",
21
+ "pnpm-workspace.yaml",
22
+ "eslint.config.mjs",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "keywords": [
27
+ "nuxt",
28
+ "nuxt-layer",
29
+ "tailwindcss",
30
+ "gsap"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "registry": "https://registry.npmjs.org"
35
+ },
36
+ "dependencies": {
37
+ "@fontsource/space-grotesk": "^5.2.8",
38
+ "@iconify-json/ph": "^1.2.2",
39
+ "@iconify-json/simple-icons": "^1.2.47",
40
+ "@nuxt/eslint": "1.8.0",
41
+ "@nuxt/icon": "2.0.0",
42
+ "@nuxtjs/color-mode": "^3.5.2",
43
+ "@tailwindcss/vite": "^4.1.11",
44
+ "@unhead/vue": "^2.0.14",
45
+ "eslint": "^9.33.0",
46
+ "gsap": "^3.13.0",
47
+ "nuxt": "^4.0.3",
48
+ "nuxt-og-image": "5.1.9",
49
+ "tailwindcss": "^4.1.11",
50
+ "unstorage": "^1.16.1",
51
+ "vue": "^3.5.18",
52
+ "vue-router": "^4.5.1"
53
+ },
54
+ "scripts": {
55
+ "build": "nuxt build",
56
+ "dev": "nuxt dev",
57
+ "generate": "nuxt generate",
58
+ "preview": "nuxt preview",
59
+ "postinstall": "nuxt prepare"
60
+ }
61
+ }
@@ -0,0 +1,5 @@
1
+ onlyBuiltDependencies:
2
+ - '@parcel/watcher'
3
+ - '@tailwindcss/oxide'
4
+ - esbuild
5
+ - unrs-resolver
@@ -0,0 +1,6 @@
1
+ <svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M181 0H75C33.5786 0 0 33.5786 0 75V181C0 222.421 33.5786 256 75 256H181C222.421 256 256 222.421 256 181V75C256 33.5786 222.421 0 181 0Z" fill="#020617"/>
3
+ <rect x="62" y="63" width="58" height="131" rx="20" fill="#D9D9D9"/>
4
+ <rect x="135" y="63" width="58" height="58" rx="20" fill="#D9D9D9"/>
5
+ <rect x="135" y="136" width="58" height="58" rx="20" fill="#D9D9D9"/>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M181 0H75C33.5786 0 0 33.5786 0 75V181C0 222.421 33.5786 256 75 256H181C222.421 256 256 222.421 256 181V75C256 33.5786 222.421 0 181 0Z" fill="#020617"/>
3
+ <rect x="62" y="63" width="58" height="131" rx="20" fill="#D9D9D9"/>
4
+ <rect x="135" y="63" width="58" height="58" rx="20" fill="#D9D9D9"/>
5
+ <rect x="135" y="136" width="58" height="58" rx="20" fill="#D9D9D9"/>
6
+ </svg>
Binary file
@@ -0,0 +1,2 @@
1
+ User-Agent: *
2
+ Disallow:
Binary file