@minch/bentolio 0.9.2 → 0.9.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Bentolio
2
2
 
3
- A minimal portfolio template built with Nuxt 4, Tailwind CSS 4, and GSAP.
3
+ A minimal portfolio template and Nuxt layer built with Nuxt 4, Tailwind CSS 4, and GSAP.
4
4
 
5
5
  All content and basic styling are configured via `app/app.config.ts`.
6
6
 
@@ -10,7 +10,7 @@ All content and basic styling are configured via `app/app.config.ts`.
10
10
 
11
11
  ## Features
12
12
 
13
- - **Nuxt 4**: Modern DX, serverless-friendly, and SSG by default
13
+ - **Nuxt Layer**: Modern DX, serverless-friendly, and SSG by default
14
14
  - **Tailwind CSS 4**: Zero-config utility classes with CSS-first authoring
15
15
  - **GSAP animations**: Smooth entrance animations and micro‑interactions
16
16
  - **Dark Mode**: Toggle via `@nuxtjs/color-mode` (class strategy)
@@ -19,26 +19,25 @@ All content and basic styling are configured via `app/app.config.ts`.
19
19
  - **Static generation**: Pre‑rendered pages for easy deployment to any static host
20
20
  - **OG Image**: Automatically generated OG image via `nuxt-og-image`
21
21
 
22
- ## Project Structure
22
+ ## Configure Your Portfolio
23
+
24
+ To use as a template, you can simply clone and update the files to your liking.
23
25
 
26
+ Alternatively, you can also use it as a Nuxt layer:
27
+
28
+ ```bash
29
+ npm i -D @minch/bentolio
24
30
  ```
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
31
+
32
+ ```ts
33
+ export default defineNuxtConfig({
34
+ extends: ["@minch/bentolio"]
35
+ });
37
36
  ```
38
37
 
39
- ## Configure Your Portfolio
38
+ Have a look at [.playground](/.playground/) to see how to set up as a layer.
40
39
 
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:
40
+ All page content lives in `app/app.config.ts` and is fully typed (see `app/utils/types.ts`). Edit the example values to your own:
42
41
 
43
42
  ```ts
44
43
  // app/app.config.ts (excerpt)
@@ -80,15 +79,17 @@ Supported social keys by default: `"X" | "LinkedIn" | "Bluesky" | "GitHub"`. To
80
79
  ### Assets
81
80
 
82
81
  - 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.
82
+ - Update `public/favicon.svg`, `public/og-image.png`, `public/logo.svg` and `public/logo.svg` if desired.
83
+ - OG image can be auto-generated using the `nuxt-og-image` module.
85
84
 
86
85
  ### Theming and Styles
87
86
 
88
- - Tailwind entry is `app/assets/css/main.css`.
87
+ - Tailwind entry is `app/assets/css/tailwind.css`.
89
88
  - Primary color is set via CSS theme variable:
90
89
 
91
90
  ```css
91
+ @import "@minch/bentolio/tailwind";
92
+
92
93
  /* Change `--color-primary` to customize your accent. */
93
94
  @theme {
94
95
  --color-primary: var(--color-lime-500);
@@ -98,7 +99,7 @@ Supported social keys by default: `"X" | "LinkedIn" | "Bluesky" | "GitHub"`. To
98
99
  ### Animations
99
100
 
100
101
  - 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
+ - Elements with the `animate-element` utility class will animate in on page load. The initial state and transitions are defined in `app/assets/css/tailwind.css`.
102
103
  - The waving hand emoji animation is controlled via the `#wave` id in CSS.
103
104
 
104
105
  ## Deployment
package/app/app.vue CHANGED
@@ -1,156 +1,93 @@
1
1
  <script setup lang="ts">
2
- import type { SocialMediaKey } from "~";
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";
3
7
 
4
- const { portfolio, style } = useAppConfig();
8
+ const { portfolio } = useAppConfig();
5
9
 
6
- const {
7
- name,
8
- subtitle,
9
- avatar,
10
- company,
11
- bio,
12
- cta: { contact, location, link },
13
- socials
14
- } = portfolio;
10
+ const { name, subtitle, avatar, bio, customOgImage } = portfolio;
15
11
 
16
- const { roundedItems } = style;
12
+ const { $gsap } = useNuxtApp();
17
13
 
18
- const topFourSocials = Object.fromEntries(
19
- Object.entries(socials).slice(0, 4)
20
- );
14
+ const ctx = ref<gsap.Context | null>(null);
21
15
 
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
- };
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
+ });
30
43
 
31
- defineOgImageComponent("SocialImage", {
32
- name: name,
33
- subtitle: subtitle,
34
- avatar: avatar
44
+ onUnmounted(() => {
45
+ ctx.value && ctx.value.revert();
35
46
  });
36
- </script>
37
47
 
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" />
48
+ useHead({
49
+ bodyAttrs: {
50
+ class: "animate-layout opacity-0"
51
+ }
52
+ });
56
53
 
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>
54
+ useSeoMeta({
55
+ title: `${name} · ${subtitle}`,
56
+ description: bio.text,
57
+ ogTitle: `${name} · ${subtitle}`,
58
+ ogDescription: bio.text,
59
+ ogImage: customOgImage,
60
+ twitterImage: customOgImage,
61
+ twitterCard: "summary_large_image"
62
+ });
63
+
64
+ if (!customOgImage) {
65
+ defineOgImageComponent("SocialImage", {
66
+ name: name,
67
+ subtitle: subtitle,
68
+ avatar: avatar
69
+ });
70
+ }
71
+ </script>
70
72
 
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>
73
+ <template>
74
+ <div class="space-y-5 sm:space-y-10">
75
+ <app-header />
89
76
 
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>
77
+ <main class="min-h-full">
78
+ <article
79
+ 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">
80
+ <!-- Hero -->
81
+ <app-hero />
108
82
 
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>
83
+ <!-- About -->
84
+ <app-about />
127
85
 
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>
86
+ <!-- Miscellany -->
87
+ <app-miscellany />
88
+ </article>
89
+ </main>
139
90
 
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>
91
+ <app-footer />
92
+ </div>
156
93
  </template>
@@ -0,0 +1,86 @@
1
+ @import "tailwindcss";
2
+ @custom-variant dark (&:where(.dark, .dark *));
3
+ @source "../../**/*";
4
+
5
+ @theme {
6
+ --color-primary: var(--color-lime-500);
7
+ }
8
+
9
+ @utility global-focus {
10
+ @apply outline-none ring-2 ring-offset-0 ring-primary/75;
11
+ }
12
+
13
+ @utility animate-element {
14
+ @apply transition-transform opacity-0;
15
+ }
16
+
17
+ * {
18
+ @apply box-border selection:bg-primary/50;
19
+ }
20
+
21
+ body {
22
+ font-family: "Space Grotesk", sans-serif;
23
+ word-wrap: break-word;
24
+ @apply w-full bg-white dark:bg-zinc-900;
25
+ }
26
+
27
+ #app {
28
+ @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;
29
+ }
30
+
31
+ /* Animated Wave Emoji */
32
+ #wave {
33
+ @apply inline-block ml-1 origin-[70%_70%] animate-[10s_ease_2s_infinite_wave] hover:animate-[1.5s_ease_hoverwave];
34
+ }
35
+
36
+ @keyframes wave {
37
+ 0% {
38
+ transform: rotate(0deg);
39
+ }
40
+ 2.5%,
41
+ 7.5% {
42
+ transform: rotate(14deg);
43
+ }
44
+ 5% {
45
+ transform: rotate(-8deg);
46
+ }
47
+ 10% {
48
+ transform: rotate(-4deg);
49
+ }
50
+ 12.5% {
51
+ transform: rotate(10deg);
52
+ }
53
+ 15% {
54
+ transform: rotate(0deg);
55
+ }
56
+ 100% {
57
+ transform: rotate(0deg);
58
+ }
59
+ }
60
+
61
+ @keyframes hoverwave {
62
+ 0% {
63
+ transform: rotate(0deg);
64
+ }
65
+ 10% {
66
+ transform: rotate(14deg);
67
+ }
68
+ 20% {
69
+ transform: rotate(-8deg);
70
+ }
71
+ 30% {
72
+ transform: rotate(14deg);
73
+ }
74
+ 40% {
75
+ transform: rotate(-4deg);
76
+ }
77
+ 50% {
78
+ transform: rotate(10deg);
79
+ }
80
+ 60% {
81
+ transform: rotate(0deg);
82
+ }
83
+ 100% {
84
+ transform: rotate(0deg);
85
+ }
86
+ }
@@ -0,0 +1,27 @@
1
+ <script lang="ts" setup>
2
+ const { portfolio, style } = useAppConfig();
3
+
4
+ const { bio } = portfolio;
5
+
6
+ const { roundedItems } = style;
7
+ </script>
8
+
9
+ <template>
10
+ <div class="grid-rows-1">
11
+ <p
12
+ v-if="bio.html"
13
+ :class="[
14
+ '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',
15
+ { 'rounded-lg': roundedItems }
16
+ ]"
17
+ v-html="bio.html"></p>
18
+ <p
19
+ v-else
20
+ :class="[
21
+ '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',
22
+ { 'rounded-lg': roundedItems }
23
+ ]">
24
+ {{ bio.text }}
25
+ </p>
26
+ </div>
27
+ </template>
@@ -0,0 +1,71 @@
1
+ <script lang="ts" setup>
2
+ import type { SocialMediaKey } from "~";
3
+
4
+ const { portfolio, style } = useAppConfig();
5
+
6
+ const { name, subtitle, avatar, company, socials } = portfolio;
7
+
8
+ const { roundedItems } = style;
9
+
10
+ const topFourSocials = Object.fromEntries(
11
+ Object.entries(socials).slice(0, 4)
12
+ );
13
+
14
+ const numSocials = Object.entries(topFourSocials).length;
15
+ const socialLinkClass = {
16
+ "col-span-full lg:col-span-4 lg:row-span-2": numSocials === 1,
17
+ "col-span-6 lg:col-span-4": numSocials === 2,
18
+ "col-span-4 last:col-span-4 lg:col-span-2 lg:last:col-span-4":
19
+ numSocials === 3,
20
+ "col-span-3 lg:col-span-2": numSocials === 4
21
+ };
22
+ </script>
23
+
24
+ <template>
25
+ <div class="grid-rows-2">
26
+ <!-- Intro -->
27
+ <div
28
+ :class="[
29
+ 'animate-element col-span-full lg:col-span-8 row-span-2',
30
+ { 'rounded-lg': roundedItems }
31
+ ]">
32
+ <img
33
+ v-if="avatar"
34
+ :src="avatar"
35
+ alt="My Profile Picture"
36
+ 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" />
37
+
38
+ <div class="space-y-4 py-6 sm:pb-8">
39
+ <h1
40
+ class="text-2xl sm:text-3xl lg:text-4xl text-center lg:text-left font-bold *:inline-block">
41
+ Hi, I'm {{ name }}
42
+ <span id="wave">👋</span>
43
+ </h1>
44
+ <h2
45
+ class="text-center lg:text-left font-bold *:inline-block text-zinc-500 dark:text-primary text-xl sm:text-2xl lg:text-3xl">
46
+ <span>{{ subtitle }}</span
47
+ ><span v-if="company">&nbsp;@&nbsp;{{ company }}</span>
48
+ </h2>
49
+ </div>
50
+ </div>
51
+
52
+ <!-- Social Media -->
53
+ <template v-for="(item, key) in topFourSocials" :key="key">
54
+ <social-media-link
55
+ v-if="item"
56
+ :to="item.profileUrl"
57
+ :name="key as SocialMediaKey"
58
+ :icon="item.socialMediaIcon"
59
+ :style="{
60
+ backgroundColor: item.socialMediaBgColor,
61
+ textColor: item.socialMediaTextColor,
62
+ borderColor: item.socialMediaBorderColor
63
+ }"
64
+ :class="[
65
+ 'animate-element',
66
+ socialLinkClass,
67
+ { 'rounded-lg': roundedItems }
68
+ ]" />
69
+ </template>
70
+ </div>
71
+ </template>
@@ -0,0 +1,56 @@
1
+ <script lang="ts" setup>
2
+ const { portfolio, style } = useAppConfig();
3
+
4
+ const {
5
+ cta: { contact, location, link }
6
+ } = portfolio;
7
+
8
+ const { roundedItems } = style;
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ class="grid-rows-1 grid-cols-1! sm:grid-cols-[repeat(auto-fit,minmax(100px,1fr))]!">
14
+ <!-- Link -->
15
+ <nuxt-link
16
+ v-if="link"
17
+ :to="link.url"
18
+ :class="[
19
+ 'animate-element flex flex-col items-center justify-center gap-y-2 focus-visible:global-focus hover:scale-105!',
20
+ { 'rounded-lg': roundedItems }
21
+ ]">
22
+ <Icon
23
+ name="ph:arrow-square-up-right-duotone"
24
+ class="text-3xl lg:text-4xl text-primary" />
25
+ <span class="text-center text-lg">
26
+ {{ link.label }}
27
+ </span>
28
+ </nuxt-link>
29
+
30
+ <!-- Location -->
31
+ <p
32
+ :class="[
33
+ 'animate-element flex flex-col items-center justify-center gap-y-2',
34
+ { 'rounded-lg': roundedItems }
35
+ ]">
36
+ <Icon
37
+ name="ph:map-pin-duotone"
38
+ class="text-3xl lg:text-4xl text-primary" />
39
+ <span class="text-center text-lg"> {{ location }} </span>
40
+ </p>
41
+
42
+ <!-- Contact -->
43
+ <p
44
+ :class="[
45
+ 'animate-element flex flex-col items-center justify-center gap-y-2',
46
+ { 'rounded-lg': roundedItems }
47
+ ]">
48
+ <Icon
49
+ name="ph:chat-circle-text-duotone"
50
+ class="text-3xl lg:text-4xl text-primary" />
51
+ <span class="text-center text-lg">
52
+ {{ contact }}
53
+ </span>
54
+ </p>
55
+ </div>
56
+ </template>
@@ -0,0 +1,32 @@
1
+ <script setup lang="ts">
2
+ const colorMode = useColorMode();
3
+ const { style } = useAppConfig();
4
+
5
+ const { roundedItems } = style;
6
+
7
+ const onClick = () =>
8
+ colorMode.value === "light"
9
+ ? (colorMode.preference = "dark")
10
+ : (colorMode.preference = "light");
11
+ </script>
12
+
13
+ <template>
14
+ <button
15
+ aria-label="Color Mode"
16
+ :class="[
17
+ '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',
18
+ { 'rounded-lg': roundedItems }
19
+ ]"
20
+ @click="onClick">
21
+ <ColorScheme placeholder="...">
22
+ <template v-if="colorMode.value === 'dark'">
23
+ <Icon name="ph:sun-duotone" class="w-5 h-5" />
24
+ <span class="sr-only">Dark Mode</span>
25
+ </template>
26
+ <template v-else>
27
+ <Icon name="ph:moon-duotone" class="w-5 h-5" />
28
+ <span class="sr-only">Light Mode</span>
29
+ </template>
30
+ </ColorScheme>
31
+ </button>
32
+ </template>
@@ -1,42 +1,43 @@
1
1
  export type SocialMediaKey = "X" | "LinkedIn" | "Bluesky" | "GitHub";
2
2
 
3
3
  export interface SocialMediaValue {
4
- socialMediaIcon: string;
5
- socialMediaBgColor: string;
6
- socialMediaBorderColor: string;
7
- socialMediaTextColor: string;
8
- profileUrl: string;
4
+ socialMediaIcon: string;
5
+ socialMediaBgColor: string;
6
+ socialMediaBorderColor: string;
7
+ socialMediaTextColor: string;
8
+ profileUrl: string;
9
9
  }
10
10
 
11
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
- };
12
+ name: string;
13
+ subtitle: string;
14
+ avatar?: string;
15
+ customOgImage?: string;
16
+ company?: string;
17
+ socials: Partial<Record<SocialMediaKey, SocialMediaValue>>;
18
+ bio: {
19
+ html?: string;
20
+ text: string;
21
+ };
22
+ cta: {
23
+ contact?: string;
24
+ location?: string;
25
+ link?: { url: string; label: string };
26
+ };
26
27
  }
27
28
 
28
29
  export interface Style {
29
- roundedItems: boolean;
30
+ roundedItems: boolean;
30
31
  }
31
32
 
32
33
  declare module "nuxt/schema" {
33
- interface AppConfigInput {
34
- portfolio: Portfolio;
35
- style?: Style;
36
- }
34
+ interface AppConfigInput {
35
+ portfolio: Portfolio;
36
+ style?: Style;
37
+ }
37
38
 
38
- interface AppConfig {
39
- portfolio: Portfolio;
40
- style?: Style;
41
- }
39
+ interface AppConfig {
40
+ portfolio: Portfolio;
41
+ style?: Style;
42
+ }
42
43
  }
package/eslint.config.mjs CHANGED
@@ -6,6 +6,7 @@ export default withNuxt(
6
6
  {
7
7
  rules: {
8
8
  "vue/html-self-closing": "off",
9
+ "vue/multi-word-component-names": "off"
9
10
  }
10
11
  }
11
12
  )
package/nuxt.config.ts CHANGED
@@ -19,6 +19,13 @@ export default defineNuxtConfig({
19
19
  transpile: ["gsap"]
20
20
  },
21
21
 
22
+ components: [
23
+ {
24
+ path: join(currentDir, "./app/components"),
25
+ pathPrefix: false
26
+ }
27
+ ],
28
+
22
29
  colorMode: { classSuffix: "" },
23
30
 
24
31
  compatibilityDate: "2025-05-15",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minch/bentolio",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "A minimal portfolio template and layer built with Nuxt 4, Tailwind CSS 4, and GSAP.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -52,10 +52,11 @@
52
52
  "vue-router": "^4.5.1"
53
53
  },
54
54
  "scripts": {
55
- "build": "nuxt build",
56
- "dev": "nuxt dev",
57
- "generate": "nuxt generate",
58
- "preview": "nuxt preview",
59
- "postinstall": "nuxt prepare"
55
+ "dev": "nuxt dev .playground",
56
+ "dev:prepare": "nuxt prepare .playground",
57
+ "build": "nuxt build .playground",
58
+ "generate": "nuxt generate .playground",
59
+ "preview": "nuxt preview .playground",
60
+ "lint": "eslint ."
60
61
  }
61
62
  }
package/app/app.config.ts DELETED
@@ -1,53 +0,0 @@
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
- });
@@ -1,85 +0,0 @@
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
- }
@@ -1,26 +0,0 @@
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>
@@ -1,73 +0,0 @@
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>
@@ -1,6 +0,0 @@
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>
package/public/logo.svg DELETED
@@ -1,6 +0,0 @@
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