create-absolutejs 0.6.1 → 0.8.0

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.
Files changed (78) hide show
  1. package/LICENSE +24 -24
  2. package/README.md +179 -179
  3. package/dist/commands/formatProject.js +3 -2
  4. package/dist/commands/initializeGit.js +1 -1
  5. package/dist/commands/installDependencies.js +4 -3
  6. package/dist/data.js +22 -21
  7. package/dist/generators/configurations/generateDrizzleConfig.js +15 -15
  8. package/dist/generators/configurations/generatePackageJson.js +41 -62
  9. package/dist/generators/configurations/generatePrettierrc.js +9 -9
  10. package/dist/generators/db/dockerInitTemplates.d.ts +4 -0
  11. package/dist/generators/db/dockerInitTemplates.js +83 -79
  12. package/dist/generators/db/generateDatabaseTypes.js +6 -6
  13. package/dist/generators/db/generateDockerContainer.js +53 -16
  14. package/dist/generators/db/generateDrizzleSchema.js +17 -17
  15. package/dist/generators/db/generateSqliteSchema.js +8 -8
  16. package/dist/generators/db/handlerTemplates.d.ts +1 -1
  17. package/dist/generators/db/handlerTemplates.js +260 -260
  18. package/dist/generators/db/scaffoldDatabase.d.ts +3 -1
  19. package/dist/generators/db/scaffoldDatabase.js +4 -2
  20. package/dist/generators/db/scaffoldDocker.d.ts +3 -1
  21. package/dist/generators/db/scaffoldDocker.js +21 -11
  22. package/dist/generators/html/generateHTMLPage.js +60 -60
  23. package/dist/generators/htmx/generateHTMXPage.js +86 -86
  24. package/dist/generators/project/generateAbsoluteAuthConfig.d.ts +1 -1
  25. package/dist/generators/project/generateAbsoluteAuthConfig.js +100 -89
  26. package/dist/generators/project/generateDBBlock.js +9 -9
  27. package/dist/generators/project/generateImportsBlock.js +4 -1
  28. package/dist/generators/project/generateMarkupCSS.js +145 -145
  29. package/dist/generators/project/generateRoutesBlock.d.ts +3 -2
  30. package/dist/generators/project/generateRoutesBlock.js +37 -36
  31. package/dist/generators/project/generateServer.js +22 -18
  32. package/dist/generators/project/scaffoldBackend.js +2 -1
  33. package/dist/generators/project/scaffoldFrontends.d.ts +2 -2
  34. package/dist/generators/project/scaffoldFrontends.js +5 -2
  35. package/dist/generators/react/generateReactComponents.js +95 -95
  36. package/dist/generators/svelte/generateSveltePage.js +210 -210
  37. package/dist/generators/vue/generateVuePage.js +261 -261
  38. package/dist/index.js +11 -2
  39. package/dist/messages.js +43 -43
  40. package/dist/questions/projectName.js +1 -1
  41. package/dist/scaffold.d.ts +3 -1
  42. package/dist/scaffold.js +10 -5
  43. package/dist/templates/README.md +35 -35
  44. package/dist/templates/assets/svg/google-logo.svg +7 -7
  45. package/dist/templates/assets/svg/htmx-logo-black.svg +9 -9
  46. package/dist/templates/assets/svg/htmx-logo-white.svg +9 -9
  47. package/dist/templates/assets/svg/vue-logo.svg +4 -4
  48. package/dist/templates/configurations/.prettierignore +3 -3
  49. package/dist/templates/configurations/.prettierrc.json +9 -9
  50. package/dist/templates/configurations/drizzle.config.ts +13 -13
  51. package/dist/templates/configurations/eslint.config.mjs +243 -243
  52. package/dist/templates/configurations/tsconfig.example.json +98 -98
  53. package/dist/templates/constants.ts +2 -2
  54. package/dist/templates/db/docker-compose.db.yml +15 -15
  55. package/dist/templates/git/gitignore +51 -51
  56. package/dist/templates/html/scripts/typescript-example.ts +21 -21
  57. package/dist/templates/react/components/App.tsx +52 -52
  58. package/dist/templates/react/components/Head.tsx +34 -34
  59. package/dist/templates/react/components/OAuthLink.tsx +39 -39
  60. package/dist/templates/react/components/ProfilePicture.tsx +56 -56
  61. package/dist/templates/styles/colors.ts +11 -11
  62. package/dist/templates/styles/reset.css +84 -84
  63. package/dist/templates/svelte/components/Counter.svelte +19 -19
  64. package/dist/templates/svelte/composables/counter.svelte.ts +14 -14
  65. package/dist/templates/tailwind/postcss.config.ts +8 -8
  66. package/dist/templates/tailwind/tailwind.config.ts +7 -7
  67. package/dist/templates/tailwind/tailwind.css +1 -1
  68. package/dist/templates/vue/components/CountButton.vue +39 -39
  69. package/dist/templates/vue/composables/useCount.ts +14 -14
  70. package/dist/utils/checkDockerInstalled.d.ts +9 -1
  71. package/dist/utils/checkDockerInstalled.js +137 -39
  72. package/dist/utils/checkSqliteInstalled.js +13 -13
  73. package/dist/utils/commandMaps.d.ts +1 -1
  74. package/dist/utils/commandMaps.js +4 -4
  75. package/dist/versions.d.ts +50 -0
  76. package/dist/versions.js +62 -0
  77. package/package.json +22 -21
  78. package/dist/templates/styles/tailwind.css +0 -1
@@ -1,40 +1,40 @@
1
1
  import { formatNavLink } from '../../utils/formatNavLink';
2
2
  export const generateDropdownComponent = (frontends) => {
3
3
  const navLinks = frontends.map(formatNavLink).join('\n\t\t\t');
4
- return `import { useState } from 'react';
5
-
6
- export const Dropdown = () => {
7
- const [isOpen, setIsOpen] = useState(false);
8
-
9
- return (
10
- <details
11
- onPointerEnter={() => setIsOpen(true)}
12
- onPointerLeave={() => setIsOpen(false)}
13
- open={isOpen}
14
- >
15
- <summary>Pages</summary>
16
- <nav>
17
- ${navLinks}
18
- </nav>
19
- </details>
20
- );
21
- };
4
+ return `import { useState } from 'react';
5
+
6
+ export const Dropdown = () => {
7
+ const [isOpen, setIsOpen] = useState(false);
8
+
9
+ return (
10
+ <details
11
+ onPointerEnter={() => setIsOpen(true)}
12
+ onPointerLeave={() => setIsOpen(false)}
13
+ open={isOpen}
14
+ >
15
+ <summary>Pages</summary>
16
+ <nav>
17
+ ${navLinks}
18
+ </nav>
19
+ </details>
20
+ );
21
+ };
22
22
  `;
23
23
  };
24
- export const generateSignInComponent = (absProviders) => `import { useState } from 'react';
25
- import { OAuthLink } from './OAuthLink';
26
-
27
- export const SignIn = () => {
28
- const [isOpen, setIsOpen] = useState(false);
29
-
30
- return (
31
- <details
32
- onPointerEnter={() => setIsOpen(true)}
33
- onPointerLeave={() => setIsOpen(false)}
34
- open={isOpen}
35
- >
36
- <summary>Sign In</summary>
37
- <nav>
24
+ export const generateSignInComponent = (absProviders) => `import { useState } from 'react';
25
+ import { OAuthLink } from './OAuthLink';
26
+
27
+ export const SignIn = () => {
28
+ const [isOpen, setIsOpen] = useState(false);
29
+
30
+ return (
31
+ <details
32
+ onPointerEnter={() => setIsOpen(true)}
33
+ onPointerLeave={() => setIsOpen(false)}
34
+ open={isOpen}
35
+ >
36
+ <summary>Sign In</summary>
37
+ <nav>
38
38
  ${absProviders && absProviders.length > 0
39
39
  ? absProviders
40
40
  .map((provider) => {
@@ -44,83 +44,83 @@ export const SignIn = () => {
44
44
  return `<OAuthLink provider="${provider}" logoUrl="${logoUrl}" name="${name}" mode="in" />`;
45
45
  })
46
46
  .join('\n\t\t\t')
47
- : `<p>No OAuth providers configured</p>`}
48
- </nav>
49
- </details>
50
- );
51
- }
47
+ : `<p>No OAuth providers configured</p>`}
48
+ </nav>
49
+ </details>
50
+ );
51
+ }
52
52
  `;
53
53
  export const generateReactExamplePage = (authOption) => {
54
54
  const useBlockReturn = authOption === 'abs';
55
- const propsType = `
56
- type ReactExampleProps = {
57
- initialCount: number;
58
- cssPath: string;
59
- ${authOption === 'abs' ? 'user: User | null;\n\tproviderConfiguration: ProviderConfiguration | undefined;' : ''}
60
- };
55
+ const propsType = `
56
+ type ReactExampleProps = {
57
+ initialCount: number;
58
+ cssPath: string;
59
+ ${authOption === 'abs' ? 'user: User | null;\n\tproviderConfiguration: ProviderConfiguration | undefined;' : ''}
60
+ };
61
61
  `;
62
62
  const fnSignature = authOption === 'abs'
63
63
  ? `export const ReactExample = ({ initialCount, cssPath, user, providerConfiguration }: ReactExampleProps) => {`
64
64
  : `export const ReactExample = ({ initialCount, cssPath }: ReactExampleProps) => (`;
65
- const extractProps = ` const userImage =
66
- user?.metadata && providerConfiguration?.picture
67
- ? extractPropFromIdentity(
68
- user.metadata,
69
- providerConfiguration.picture,
70
- 'string'
71
- )
72
- : undefined;
73
-
74
- const givenName =
75
- user?.metadata && providerConfiguration?.givenName
76
- ? extractPropFromIdentity(
77
- user.metadata,
78
- providerConfiguration.givenName,
79
- 'string'
80
- )
81
- : undefined;
82
-
83
- const familyName =
84
- user?.metadata && providerConfiguration?.familyName
85
- ? extractPropFromIdentity(
86
- user.metadata,
87
- providerConfiguration.familyName,
88
- 'string'
89
- )
65
+ const extractProps = ` const userImage =
66
+ user?.metadata && providerConfiguration?.picture
67
+ ? extractPropFromIdentity(
68
+ user.metadata,
69
+ providerConfiguration.picture,
70
+ 'string'
71
+ )
72
+ : undefined;
73
+
74
+ const givenName =
75
+ user?.metadata && providerConfiguration?.givenName
76
+ ? extractPropFromIdentity(
77
+ user.metadata,
78
+ providerConfiguration.givenName,
79
+ 'string'
80
+ )
81
+ : undefined;
82
+
83
+ const familyName =
84
+ user?.metadata && providerConfiguration?.familyName
85
+ ? extractPropFromIdentity(
86
+ user.metadata,
87
+ providerConfiguration.familyName,
88
+ 'string'
89
+ )
90
90
  : undefined;`;
91
91
  const userButtonsBlock = authOption === 'abs'
92
92
  ? `{user ? <ProfilePicture userImage={userImage} givenName={givenName} familyName={familyName} /> : <SignIn />}`
93
93
  : ``;
94
94
  const rightGroup = authOption === 'abs'
95
- ? `<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
96
- <Dropdown />
97
- ${userButtonsBlock}
95
+ ? `<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
96
+ <Dropdown />
97
+ ${userButtonsBlock}
98
98
  </div>`
99
99
  : `<Dropdown />`;
100
100
  const closing = authOption === 'abs' ? `};` : `);`;
101
- return `
102
- ${authOption === 'abs' ? `import { User } from '../../../types/databaseTypes';\nimport { extractPropFromIdentity, ProviderConfiguration } from '@absolutejs/auth';` : ''}
103
- import { App } from '../components/App';
104
- import { Dropdown } from '../components/Dropdown';
105
- import { Head } from '../components/Head';
106
- ${authOption === 'abs' ? `import { ProfilePicture } from '../components/ProfilePicture';\nimport { SignIn } from '../components/SignIn';` : ''}
107
-
108
- ${propsType}
109
-
110
- ${fnSignature}
111
- ${authOption === 'abs' ? extractProps : ''}
112
- ${useBlockReturn ? 'return (' : ''}
113
- <html>
114
- <Head cssPath={cssPath} />
115
- <body>
116
- <header>
117
- <a href="/">AbsoluteJS</a>
118
- ${rightGroup}
119
- </header>
120
- <App initialCount={initialCount} />
121
- </body>
122
- </html>
123
- ${useBlockReturn ? ');' : ''}
124
- ${closing}
101
+ return `
102
+ ${authOption === 'abs' ? `import { User } from '../../../types/databaseTypes';\nimport { extractPropFromIdentity, ProviderConfiguration } from '@absolutejs/auth';` : ''}
103
+ import { App } from '../components/App';
104
+ import { Dropdown } from '../components/Dropdown';
105
+ import { Head } from '../components/Head';
106
+ ${authOption === 'abs' ? `import { ProfilePicture } from '../components/ProfilePicture';\nimport { SignIn } from '../components/SignIn';` : ''}
107
+
108
+ ${propsType}
109
+
110
+ ${fnSignature}
111
+ ${authOption === 'abs' ? extractProps : ''}
112
+ ${useBlockReturn ? 'return (' : ''}
113
+ <html>
114
+ <Head cssPath={cssPath} />
115
+ <body>
116
+ <header>
117
+ <a href="/">AbsoluteJS</a>
118
+ ${rightGroup}
119
+ </header>
120
+ <App initialCount={initialCount} />
121
+ </body>
122
+ </html>
123
+ ${useBlockReturn ? ');' : ''}
124
+ ${closing}
125
125
  `;
126
126
  };
@@ -1,215 +1,215 @@
1
1
  import { formatNavLink } from '../../utils/formatNavLink';
2
2
  export const generateSveltePage = (frontends) => {
3
3
  const navLinks = frontends.map(formatNavLink).join('\n\t\t\t');
4
- return `<script lang="ts">
5
- type SvelteExampleProps = {
6
- initialCount: number;
7
- cssPath: string;
8
- };
9
- import Counter from '../components/Counter.svelte';
10
-
11
- let { initialCount, cssPath }: SvelteExampleProps = $props();
12
- let isOpen = $state(false);
13
- </script>
14
-
15
- <svelte:head>
16
- <meta charset="utf-8" />
17
- <title>AbsoluteJS + Svelte</title>
18
- <meta name="description" content="AbsoluteJS Svelte Example" />
19
- <meta name="viewport" content="width=device-width, initial-scale=1" />
20
- <link rel="icon" href="/assets/ico/favicon.ico" />
21
- <link rel="preconnect" href="https://fonts.googleapis.com" />
22
- <link
23
- rel="preconnect"
24
- href="https://fonts.gstatic.com"
25
- crossOrigin="anonymous"
26
- />
27
- <link
28
- href={\`https://fonts.googleapis.com/css2?family=Poppins:wght@100..900&display=swap\`}
29
- rel="stylesheet"
30
- />
31
- <link rel="stylesheet" href={cssPath} type="text/css" />
32
- </svelte:head>
33
-
34
- <header>
35
- <a href="/">AbsoluteJS</a>
36
- <details
37
- open={isOpen}
38
- onpointerenter={() => (isOpen = true)}
39
- onpointerleave={() => (isOpen = false)}
40
- >
41
- <summary>Pages</summary>
42
- <nav>
43
- ${navLinks}
44
- </nav>
45
- </details>
46
- </header>
47
-
48
- <main>
49
- <nav>
50
- <a href="https://absolutejs.com" target="_blank">
51
- <img
52
- class="logo"
53
- src="/assets/png/absolutejs-temp.png"
54
- alt="AbsoluteJS Logo"
55
- />
56
- </a>
57
- <a href="https://svelte.dev" target="_blank">
58
- <img
59
- class="logo svelte"
60
- src="/assets/svg/svelte-logo.svg"
61
- alt="Svelte Logo"
62
- />
63
- </a>
64
- </nav>
65
- <h1>AbsoluteJS + Svelte</h1>
66
- <Counter {initialCount} />
67
- <p>
68
- Edit <code>example/svelte/pages/SvelteExample.svelte</code> then save and
69
- refresh to update the page.
70
- </p>
71
- <p style="color: #777">( Hot Module Reloading is coming soon )</p>
72
- <p style="margin-top: 2rem;">
73
- Explore the other pages to see how AbsoluteJS seamlessly unifies
74
- multiple frameworks on a single server.
75
- </p>
76
- <p style="color: #777; font-size: 1rem; margin-top: 2rem;">
77
- Click on the AbsoluteJS and Svelte logos to learn more.
78
- </p>
79
- </main>
80
-
81
- <style>
82
- header {
83
- align-items: center;
84
- background-color: #1a1a1a;
85
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
86
- display: flex;
87
- justify-content: space-between;
88
- padding: 2rem;
89
- text-align: center;
90
- }
91
-
92
- header a {
93
- position: relative;
94
- color: #5fbeeb;
95
- text-decoration: none;
96
- }
97
-
98
- header a::after {
99
- content: '';
100
- position: absolute;
101
- left: 0;
102
- bottom: 0;
103
- width: 100%;
104
- height: 2px;
105
- background: linear-gradient(
106
- 90deg,
107
- #5fbeeb 0%,
108
- #35d5a2 50%,
109
- #ff4b91 100%
110
- );
111
- transform: scaleX(0);
112
- transform-origin: left;
113
- transition: transform 0.25s ease-in-out;
114
- }
115
-
116
- header a:hover::after {
117
- transform: scaleX(1);
118
- }
119
-
120
- h1 {
121
- font-size: 2.5rem;
122
- margin-top: 2rem;
123
- }
124
-
125
- .logo {
126
- height: 8rem;
127
- width: 8rem;
128
- will-change: filter;
129
- transition: filter 300ms;
130
- }
131
-
132
- .logo:hover {
133
- filter: drop-shadow(0 0 2rem #5fbeeb);
134
- }
135
-
136
- .logo.svelte:hover {
137
- filter: drop-shadow(0 0 2rem #ff3e00);
138
- }
139
-
140
- nav {
141
- display: flex;
142
- gap: 4rem;
143
- justify-content: center;
144
- }
145
-
146
- header details {
147
- position: relative;
148
- }
149
-
150
- header details summary {
151
- list-style: none;
152
- appearance: none;
153
- -webkit-appearance: none;
154
- cursor: pointer;
155
- user-select: none;
156
- color: #5fbeeb;
157
- font-size: 1.5rem;
158
- font-weight: 500;
159
- padding: 0.5rem 1rem;
160
- }
161
-
162
- header summary::after {
163
- content: '▼';
164
- display: inline-block;
165
- margin-left: 0.5rem;
166
- font-size: 0.75rem;
167
- transition: transform 0.3s ease;
168
- }
169
-
170
- header details[open] summary::after {
171
- transform: rotate(180deg);
172
- }
173
-
174
- header details nav {
175
- position: absolute;
176
- top: 100%;
177
- right: -0.5rem;
178
- display: flex;
179
- flex-direction: column;
180
- gap: 0.75rem;
181
- background: rgba(185, 185, 185, 0.1);
182
- backdrop-filter: blur(4px);
183
- border: 1px solid #5fbeeb;
184
- border-radius: 1rem;
185
- padding: 1rem 1.5rem;
186
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
187
- opacity: 0;
188
- transform: translateY(-8px);
189
- pointer-events: none;
190
- transition:
191
- opacity 0.3s ease,
192
- transform 0.3s ease;
193
- z-index: 1000;
194
- }
195
-
196
- header details[open] nav {
197
- opacity: 1;
198
- transform: translateY(0);
199
- pointer-events: auto;
200
- }
201
-
202
- header details nav a {
203
- font-size: 1.1rem;
204
- padding: 0.25rem 0;
205
- white-space: nowrap;
206
- }
207
-
208
- @media (prefers-color-scheme: light) {
209
- header {
210
- background-color: #ffffff;
211
- }
212
- }
213
- </style>
4
+ return `<script lang="ts">
5
+ type SvelteExampleProps = {
6
+ initialCount: number;
7
+ cssPath: string;
8
+ };
9
+ import Counter from '../components/Counter.svelte';
10
+
11
+ let { initialCount, cssPath }: SvelteExampleProps = $props();
12
+ let isOpen = $state(false);
13
+ </script>
14
+
15
+ <svelte:head>
16
+ <meta charset="utf-8" />
17
+ <title>AbsoluteJS + Svelte</title>
18
+ <meta name="description" content="AbsoluteJS Svelte Example" />
19
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
20
+ <link rel="icon" href="/assets/ico/favicon.ico" />
21
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
22
+ <link
23
+ rel="preconnect"
24
+ href="https://fonts.gstatic.com"
25
+ crossOrigin="anonymous"
26
+ />
27
+ <link
28
+ href={\`https://fonts.googleapis.com/css2?family=Poppins:wght@100..900&display=swap\`}
29
+ rel="stylesheet"
30
+ />
31
+ <link rel="stylesheet" href={cssPath} type="text/css" />
32
+ </svelte:head>
33
+
34
+ <header>
35
+ <a href="/">AbsoluteJS</a>
36
+ <details
37
+ open={isOpen}
38
+ onpointerenter={() => (isOpen = true)}
39
+ onpointerleave={() => (isOpen = false)}
40
+ >
41
+ <summary>Pages</summary>
42
+ <nav>
43
+ ${navLinks}
44
+ </nav>
45
+ </details>
46
+ </header>
47
+
48
+ <main>
49
+ <nav>
50
+ <a href="https://absolutejs.com" target="_blank">
51
+ <img
52
+ class="logo"
53
+ src="/assets/png/absolutejs-temp.png"
54
+ alt="AbsoluteJS Logo"
55
+ />
56
+ </a>
57
+ <a href="https://svelte.dev" target="_blank">
58
+ <img
59
+ class="logo svelte"
60
+ src="/assets/svg/svelte-logo.svg"
61
+ alt="Svelte Logo"
62
+ />
63
+ </a>
64
+ </nav>
65
+ <h1>AbsoluteJS + Svelte</h1>
66
+ <Counter {initialCount} />
67
+ <p>
68
+ Edit <code>example/svelte/pages/SvelteExample.svelte</code> then save and
69
+ refresh to update the page.
70
+ </p>
71
+ <p style="color: #777">( Hot Module Reloading is coming soon )</p>
72
+ <p style="margin-top: 2rem;">
73
+ Explore the other pages to see how AbsoluteJS seamlessly unifies
74
+ multiple frameworks on a single server.
75
+ </p>
76
+ <p style="color: #777; font-size: 1rem; margin-top: 2rem;">
77
+ Click on the AbsoluteJS and Svelte logos to learn more.
78
+ </p>
79
+ </main>
80
+
81
+ <style>
82
+ header {
83
+ align-items: center;
84
+ background-color: #1a1a1a;
85
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
86
+ display: flex;
87
+ justify-content: space-between;
88
+ padding: 2rem;
89
+ text-align: center;
90
+ }
91
+
92
+ header a {
93
+ position: relative;
94
+ color: #5fbeeb;
95
+ text-decoration: none;
96
+ }
97
+
98
+ header a::after {
99
+ content: '';
100
+ position: absolute;
101
+ left: 0;
102
+ bottom: 0;
103
+ width: 100%;
104
+ height: 2px;
105
+ background: linear-gradient(
106
+ 90deg,
107
+ #5fbeeb 0%,
108
+ #35d5a2 50%,
109
+ #ff4b91 100%
110
+ );
111
+ transform: scaleX(0);
112
+ transform-origin: left;
113
+ transition: transform 0.25s ease-in-out;
114
+ }
115
+
116
+ header a:hover::after {
117
+ transform: scaleX(1);
118
+ }
119
+
120
+ h1 {
121
+ font-size: 2.5rem;
122
+ margin-top: 2rem;
123
+ }
124
+
125
+ .logo {
126
+ height: 8rem;
127
+ width: 8rem;
128
+ will-change: filter;
129
+ transition: filter 300ms;
130
+ }
131
+
132
+ .logo:hover {
133
+ filter: drop-shadow(0 0 2rem #5fbeeb);
134
+ }
135
+
136
+ .logo.svelte:hover {
137
+ filter: drop-shadow(0 0 2rem #ff3e00);
138
+ }
139
+
140
+ nav {
141
+ display: flex;
142
+ gap: 4rem;
143
+ justify-content: center;
144
+ }
145
+
146
+ header details {
147
+ position: relative;
148
+ }
149
+
150
+ header details summary {
151
+ list-style: none;
152
+ appearance: none;
153
+ -webkit-appearance: none;
154
+ cursor: pointer;
155
+ user-select: none;
156
+ color: #5fbeeb;
157
+ font-size: 1.5rem;
158
+ font-weight: 500;
159
+ padding: 0.5rem 1rem;
160
+ }
161
+
162
+ header summary::after {
163
+ content: '▼';
164
+ display: inline-block;
165
+ margin-left: 0.5rem;
166
+ font-size: 0.75rem;
167
+ transition: transform 0.3s ease;
168
+ }
169
+
170
+ header details[open] summary::after {
171
+ transform: rotate(180deg);
172
+ }
173
+
174
+ header details nav {
175
+ position: absolute;
176
+ top: 100%;
177
+ right: -0.5rem;
178
+ display: flex;
179
+ flex-direction: column;
180
+ gap: 0.75rem;
181
+ background: rgba(185, 185, 185, 0.1);
182
+ backdrop-filter: blur(4px);
183
+ border: 1px solid #5fbeeb;
184
+ border-radius: 1rem;
185
+ padding: 1rem 1.5rem;
186
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
187
+ opacity: 0;
188
+ transform: translateY(-8px);
189
+ pointer-events: none;
190
+ transition:
191
+ opacity 0.3s ease,
192
+ transform 0.3s ease;
193
+ z-index: 1000;
194
+ }
195
+
196
+ header details[open] nav {
197
+ opacity: 1;
198
+ transform: translateY(0);
199
+ pointer-events: auto;
200
+ }
201
+
202
+ header details nav a {
203
+ font-size: 1.1rem;
204
+ padding: 0.25rem 0;
205
+ white-space: nowrap;
206
+ }
207
+
208
+ @media (prefers-color-scheme: light) {
209
+ header {
210
+ background-color: #ffffff;
211
+ }
212
+ }
213
+ </style>
214
214
  `;
215
215
  };