create-fate 1.0.4 → 1.3.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.
- package/README.md +3 -0
- package/bin/create-fate.mjs +594 -9
- package/package.json +2 -2
- package/templates/fate/README.md +3 -0
- package/templates/fate/_shared/vue-app/package.json +54 -0
- package/templates/fate/_shared/vue-app/pages/index.vue +57 -0
- package/templates/fate/_shared/vue-app/pages/layout.vue +32 -0
- package/templates/fate/_shared/vue-app/pages/post/[id].vue +24 -0
- package/templates/fate/_shared/vue-app/src/App.css +63 -0
- package/templates/fate/_shared/vue-app/src/fateViews.ts +14 -0
- package/templates/fate/_shared/vue-app/src/lib/cx.ts +6 -0
- package/templates/fate/_shared/vue-app/src/lib/env.ts +3 -0
- package/templates/fate/_shared/vue-app/src/ui/Button.vue +67 -0
- package/templates/fate/_shared/vue-app/src/ui/Card.vue +16 -0
- package/templates/fate/_shared/vue-app/src/ui/Error.vue +9 -0
- package/templates/fate/_shared/vue-app/src/ui/ErrorBoundary.vue +22 -0
- package/templates/fate/_shared/vue-app/src/ui/Header.vue +35 -0
- package/templates/fate/_shared/vue-app/src/ui/Link.vue +15 -0
- package/templates/fate/_shared/vue-app/src/ui/PostCard.vue +29 -0
- package/templates/fate/_shared/vue-app/src/ui/Section.vue +19 -0
- package/templates/fate/_shared/vue-app/src/ui/Thinking.vue +11 -0
- package/templates/fate/_shared/vue-app/src/ui/UserCard.vue +23 -0
- package/templates/fate/_shared/vue-app/src/vue-shims.d.ts +6 -0
- package/templates/fate/_shared/vue-app/tsconfig.json +22 -0
- package/templates/fate/_shared/vue-app/vite.config.ts +58 -0
- package/templates/fate/_shared/vue-app/void.json +5 -0
- package/templates/fate/cloudflare/.github/workflows/push.yml +27 -0
- package/templates/fate/cloudflare/.vite-hooks/pre-commit +1 -0
- package/templates/fate/cloudflare/.vscode/extensions.json +12 -0
- package/templates/fate/cloudflare/.vscode/settings.json +25 -0
- package/templates/fate/cloudflare/AGENTS.md +55 -0
- package/templates/fate/cloudflare/README.md +126 -0
- package/templates/fate/cloudflare/_gitignore +20 -0
- package/templates/fate/cloudflare/client/package.json +56 -0
- package/templates/fate/cloudflare/client/pages/index.tsx +96 -0
- package/templates/fate/cloudflare/client/pages/layout.tsx +82 -0
- package/templates/fate/cloudflare/client/pages/login.tsx +107 -0
- package/templates/fate/cloudflare/client/pages/post/[id].tsx +22 -0
- package/templates/fate/cloudflare/client/pages/search.tsx +99 -0
- package/templates/fate/cloudflare/client/public/icon.svg +1 -0
- package/templates/fate/cloudflare/client/src/App.css +153 -0
- package/templates/fate/cloudflare/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/cloudflare/client/src/lib/cx.tsx +6 -0
- package/templates/fate/cloudflare/client/src/lib/env.tsx +5 -0
- package/templates/fate/cloudflare/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/cloudflare/client/src/ui/Button.tsx +108 -0
- package/templates/fate/cloudflare/client/src/ui/Card.tsx +32 -0
- package/templates/fate/cloudflare/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/cloudflare/client/src/ui/Error.tsx +19 -0
- package/templates/fate/cloudflare/client/src/ui/H2.tsx +15 -0
- package/templates/fate/cloudflare/client/src/ui/H3.tsx +20 -0
- package/templates/fate/cloudflare/client/src/ui/Header.tsx +98 -0
- package/templates/fate/cloudflare/client/src/ui/Input.tsx +17 -0
- package/templates/fate/cloudflare/client/src/ui/Link.tsx +11 -0
- package/templates/fate/cloudflare/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/cloudflare/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/cloudflare/client/src/ui/Section.tsx +23 -0
- package/templates/fate/cloudflare/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/cloudflare/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/cloudflare/client/translations/de_DE.json +689 -0
- package/templates/fate/cloudflare/client/translations/ja_JP.json +689 -0
- package/templates/fate/cloudflare/client/vite.config.ts +45 -0
- package/templates/fate/cloudflare/client/void.json +29 -0
- package/templates/fate/cloudflare/package.json +46 -0
- package/templates/fate/cloudflare/pnpm-workspace.yaml +39 -0
- package/templates/fate/cloudflare/server/.env.example +1 -0
- package/templates/fate/cloudflare/server/db/db.ts +30 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508073357_initial_schema.sql +133 -0
- package/templates/fate/cloudflare/server/db/migrations/20260508120500_seed_cloudflare_demo.sql +170 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/20260508073357_snapshot.json +868 -0
- package/templates/fate/cloudflare/server/db/migrations/meta/_journal.json +20 -0
- package/templates/fate/cloudflare/server/db/queries.ts +211 -0
- package/templates/fate/cloudflare/server/db/schema.ts +332 -0
- package/templates/fate/cloudflare/server/package.json +34 -0
- package/templates/fate/cloudflare/server/src/fate/commentPermissions.ts +17 -0
- package/templates/fate/cloudflare/server/src/fate/context.ts +15 -0
- package/templates/fate/cloudflare/server/src/fate/live.ts +7 -0
- package/templates/fate/cloudflare/server/src/fate/server.ts +411 -0
- package/templates/fate/cloudflare/server/src/fate/views.ts +164 -0
- package/templates/fate/cloudflare/server/src/index.ts +80 -0
- package/templates/fate/cloudflare/server/src/lib/auth.ts +66 -0
- package/templates/fate/cloudflare/server/src/lib/origins.ts +17 -0
- package/templates/fate/cloudflare/server/src/router.ts +2 -0
- package/templates/fate/cloudflare/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/cloudflare/server/tsconfig.json +10 -0
- package/templates/fate/cloudflare/server/wrangler.jsonc +29 -0
- package/templates/fate/cloudflare/tsconfig.json +33 -0
- package/templates/fate/cloudflare/vite.config.ts +72 -0
- package/templates/fate/drizzle/README.md +2 -8
- package/templates/fate/drizzle/client/pages/layout.tsx +2 -2
- package/templates/fate/drizzle/client/src/ui/PostCard.tsx +38 -15
- package/templates/fate/drizzle/pnpm-workspace.yaml +9 -3
- package/templates/fate/drizzle/server/vite.config.ts +1 -1
- package/templates/fate/graphql/.github/workflows/push.yml +27 -0
- package/templates/fate/graphql/.vite-hooks/pre-commit +1 -0
- package/templates/fate/graphql/.vscode/extensions.json +12 -0
- package/templates/fate/graphql/.vscode/settings.json +25 -0
- package/templates/fate/graphql/AGENTS.md +55 -0
- package/templates/fate/graphql/README.md +121 -0
- package/templates/fate/graphql/_gitignore +23 -0
- package/templates/fate/graphql/client/package.json +57 -0
- package/templates/fate/graphql/client/pages/index.tsx +96 -0
- package/templates/fate/graphql/client/pages/layout.tsx +83 -0
- package/templates/fate/graphql/client/pages/login.tsx +107 -0
- package/templates/fate/graphql/client/pages/post/[id].tsx +22 -0
- package/templates/fate/graphql/client/pages/search.tsx +99 -0
- package/templates/fate/graphql/client/public/icon.svg +1 -0
- package/templates/fate/graphql/client/src/App.css +153 -0
- package/templates/fate/graphql/client/src/lib/AvailableLanguages.tsx +5 -0
- package/templates/fate/graphql/client/src/lib/cx.tsx +6 -0
- package/templates/fate/graphql/client/src/lib/env.tsx +5 -0
- package/templates/fate/graphql/client/src/ui/Badge.tsx +31 -0
- package/templates/fate/graphql/client/src/ui/Button.tsx +108 -0
- package/templates/fate/graphql/client/src/ui/Card.tsx +32 -0
- package/templates/fate/graphql/client/src/ui/CommentCard.tsx +75 -0
- package/templates/fate/graphql/client/src/ui/Error.tsx +19 -0
- package/templates/fate/graphql/client/src/ui/H2.tsx +15 -0
- package/templates/fate/graphql/client/src/ui/H3.tsx +20 -0
- package/templates/fate/graphql/client/src/ui/Header.tsx +98 -0
- package/templates/fate/graphql/client/src/ui/Input.tsx +17 -0
- package/templates/fate/graphql/client/src/ui/Link.tsx +11 -0
- package/templates/fate/graphql/client/src/ui/LocaleSwitcher.tsx +52 -0
- package/templates/fate/graphql/client/src/ui/PostCard.tsx +303 -0
- package/templates/fate/graphql/client/src/ui/Section.tsx +23 -0
- package/templates/fate/graphql/client/src/ui/UserCard.tsx +131 -0
- package/templates/fate/graphql/client/src/user/AuthClient.tsx +6 -0
- package/templates/fate/graphql/client/translations/de_DE.json +689 -0
- package/templates/fate/graphql/client/translations/ja_JP.json +689 -0
- package/templates/fate/graphql/client/vite.config.ts +40 -0
- package/templates/fate/graphql/client/void.json +29 -0
- package/templates/fate/graphql/docker-compose.yml +11 -0
- package/templates/fate/graphql/package.json +47 -0
- package/templates/fate/graphql/pnpm-workspace.yaml +45 -0
- package/templates/fate/graphql/server/.env.example +5 -0
- package/templates/fate/graphql/server/README.md +27 -0
- package/templates/fate/graphql/server/package.json +64 -0
- package/templates/fate/graphql/server/prisma.config.ts +17 -0
- package/templates/fate/graphql/server/scripts/generate-graphql.tsx +40 -0
- package/templates/fate/graphql/server/src/__tests__/index.test.tsx +5 -0
- package/templates/fate/graphql/server/src/app.tsx +47 -0
- package/templates/fate/graphql/server/src/graphql/builder.tsx +121 -0
- package/templates/fate/graphql/server/src/graphql/context.tsx +22 -0
- package/templates/fate/graphql/server/src/graphql/fate.ts +85 -0
- package/templates/fate/graphql/server/src/graphql/lib/authDirectives.tsx +93 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeGlobalID.tsx +12 -0
- package/templates/fate/graphql/server/src/graphql/lib/decodeIDOrThrow.tsx +10 -0
- package/templates/fate/graphql/server/src/graphql/lib/encodeGlobalID.tsx +5 -0
- package/templates/fate/graphql/server/src/graphql/live.tsx +241 -0
- package/templates/fate/graphql/server/src/graphql/mutations.tsx +265 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Category.tsx +35 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Comment.tsx +46 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Event.tsx +71 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Post.tsx +45 -0
- package/templates/fate/graphql/server/src/graphql/nodes/Tag.tsx +11 -0
- package/templates/fate/graphql/server/src/graphql/nodes/User.tsx +28 -0
- package/templates/fate/graphql/server/src/graphql/schema.tsx +16 -0
- package/templates/fate/graphql/server/src/graphql/views.ts +193 -0
- package/templates/fate/graphql/server/src/index.tsx +45 -0
- package/templates/fate/graphql/server/src/lib/auth.tsx +31 -0
- package/templates/fate/graphql/server/src/lib/env.tsx +3 -0
- package/templates/fate/graphql/server/src/lib/origins.ts +18 -0
- package/templates/fate/graphql/server/src/prisma/prisma.tsx +9 -0
- package/templates/fate/graphql/server/src/prisma/schema.prisma +181 -0
- package/templates/fate/graphql/server/src/prisma/seed.tsx +161 -0
- package/templates/fate/graphql/server/src/user/SessionUser.tsx +17 -0
- package/templates/fate/graphql/server/src/user/isAdmin.tsx +3 -0
- package/templates/fate/graphql/server/tsconfig.json +33 -0
- package/templates/fate/graphql/server/vite.config.ts +55 -0
- package/templates/fate/graphql/tsconfig.json +33 -0
- package/templates/fate/graphql/vite.config.ts +80 -0
- package/templates/fate/graphql-client/.env.example +3 -0
- package/templates/fate/graphql-client/AGENTS.md +3 -0
- package/templates/fate/graphql-client/README.md +69 -0
- package/templates/fate/graphql-client/_gitignore +14 -0
- package/templates/fate/graphql-client/package.json +62 -0
- package/templates/fate/graphql-client/pages/index.tsx +69 -0
- package/templates/fate/graphql-client/pages/layout.tsx +41 -0
- package/templates/fate/graphql-client/pnpm-workspace.yaml +38 -0
- package/templates/fate/graphql-client/public/icon.svg +8 -0
- package/templates/fate/graphql-client/src/App.css +28 -0
- package/templates/fate/graphql-client/src/fate/graphql.ts +52 -0
- package/templates/fate/graphql-client/tsconfig.json +30 -0
- package/templates/fate/graphql-client/vite.config.ts +87 -0
- package/templates/fate/graphql-client/void.json +29 -0
- package/templates/fate/http/README.md +2 -8
- package/templates/fate/http/client/pages/layout.tsx +2 -2
- package/templates/fate/http/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/http/pnpm-workspace.yaml +9 -3
- package/templates/fate/http/server/vite.config.ts +1 -1
- package/templates/fate/prisma/README.md +2 -8
- package/templates/fate/prisma/client/pages/layout.tsx +2 -2
- package/templates/fate/prisma/client/src/ui/PostCard.tsx +37 -15
- package/templates/fate/prisma/pnpm-workspace.yaml +9 -3
- package/templates/fate/prisma/server/vite.config.ts +1 -1
- package/templates/fate/prisma/vite.config.ts +1 -1
- package/templates/fate/void/package.json +10 -2
- package/templates/fate/void/pnpm-workspace.yaml +12 -4
- package/templates/fate/void/src/ui/CategoryCard.tsx +3 -7
- package/templates/fate/void/src/ui/PostCard.tsx +38 -15
- package/templates/fate/void/vite.config.ts +5 -0
- package/templates/fate/drizzle/public/fate-logo-dark.svg +0 -4
- package/templates/fate/drizzle/public/fate-logo.svg +0 -4
- package/templates/fate/http/public/fate-logo-dark.svg +0 -4
- package/templates/fate/http/public/fate-logo.svg +0 -4
- package/templates/fate/prisma/public/fate-logo-dark.svg +0 -4
- package/templates/fate/prisma/public/fate-logo.svg +0 -4
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Comment, Post } from '@app/server/src/router.ts';
|
|
2
|
+
import Stack, { VStack } from '@nkzw/stack';
|
|
3
|
+
import { fbs } from 'fbtee';
|
|
4
|
+
import { Suspense, useDeferredValue, useState } from 'react';
|
|
5
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
6
|
+
import { useRequest, useView, view, ViewRef } from 'react-fate';
|
|
7
|
+
import cx from '../src/lib/cx.tsx';
|
|
8
|
+
import Card from '../src/ui/Card.tsx';
|
|
9
|
+
import CommentCard, { CommentView } from '../src/ui/CommentCard.tsx';
|
|
10
|
+
import Error from '../src/ui/Error.tsx';
|
|
11
|
+
import Input from '../src/ui/Input.tsx';
|
|
12
|
+
import Section from '../src/ui/Section.tsx';
|
|
13
|
+
|
|
14
|
+
const CommentPostView = view<Post>()({
|
|
15
|
+
commentCount: true,
|
|
16
|
+
id: true,
|
|
17
|
+
title: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const CommentSearchView = view<Comment>()({
|
|
21
|
+
...CommentView,
|
|
22
|
+
id: true,
|
|
23
|
+
post: CommentPostView,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const isDevelopment = import.meta.env.DEV;
|
|
27
|
+
|
|
28
|
+
const CommentResult = ({ comment: commentRef }: { comment: ViewRef<'Comment'> }) => {
|
|
29
|
+
const comment = useView(CommentSearchView, commentRef);
|
|
30
|
+
const post = useView(CommentPostView, comment.post);
|
|
31
|
+
|
|
32
|
+
return <CommentCard comment={comment} link post={post} />;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const SearchResults = ({ isStale, query }: { isStale: boolean; query: string }) => {
|
|
36
|
+
const { commentSearch } = useRequest({
|
|
37
|
+
commentSearch: { args: { query }, list: CommentSearchView },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (commentSearch.length === 0) {
|
|
41
|
+
return (
|
|
42
|
+
<p>
|
|
43
|
+
<fbt desc="No matches text">
|
|
44
|
+
No matches for{' '}
|
|
45
|
+
<i>
|
|
46
|
+
"<fbt:param name="query">{query}</fbt:param>"
|
|
47
|
+
</i>
|
|
48
|
+
</fbt>
|
|
49
|
+
</p>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<VStack className={cx(isStale && 'opacity-50')} gap={12}>
|
|
55
|
+
{commentSearch.map((comment) => (
|
|
56
|
+
<CommentResult comment={comment} key={comment.id} />
|
|
57
|
+
))}
|
|
58
|
+
</VStack>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default function SearchPage() {
|
|
63
|
+
const [query, setQuery] = useState('');
|
|
64
|
+
const deferredQuery = useDeferredValue(query);
|
|
65
|
+
const isStale = query !== deferredQuery;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Section>
|
|
69
|
+
<Card>
|
|
70
|
+
<Stack alignCenter between gap={16}>
|
|
71
|
+
<Input
|
|
72
|
+
className="w-64"
|
|
73
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
74
|
+
placeholder={fbs('Search comments...', 'Search input placeholder')}
|
|
75
|
+
ref={(ref) => ref?.focus()}
|
|
76
|
+
value={query}
|
|
77
|
+
/>
|
|
78
|
+
{isDevelopment ? (
|
|
79
|
+
<div className="text-xs text-muted-foreground">
|
|
80
|
+
<fbt desc="Slowdown label explanation">500ms artificial slowdown</fbt>
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
</Stack>
|
|
84
|
+
|
|
85
|
+
<ErrorBoundary FallbackComponent={Error}>
|
|
86
|
+
<Suspense
|
|
87
|
+
fallback={
|
|
88
|
+
<h2>
|
|
89
|
+
<fbt desc="Text for thinking/loading screen">Thinking…</fbt>
|
|
90
|
+
</h2>
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{query.trim().length > 0 ? <SearchResults isStale={isStale} query={query} /> : null}
|
|
94
|
+
</Suspense>
|
|
95
|
+
</ErrorBoundary>
|
|
96
|
+
</Card>
|
|
97
|
+
</Section>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMax" viewBox="0 0 160 160"><path d="M108.301 17.465h.272c1.797-.005 3.563.177 5.331.496l.349.063c5.588 1.02 10.731 3.447 15.201 6.933l.347.267c1.579 1.26 3.083 2.684 4.386 4.227l.161.186c4.018 4.719 6.924 10.686 7.767 16.852l.033.239c.262 1.848.351 3.688.352 5.551l.001.307c-.001 1.623-.092 3.188-.386 4.788l-.05.292c-1.003 5.874-3.476 11.447-7.286 16.034l-.29.358c-1.422 1.721-3.011 3.382-4.797 4.729l-.444.354c-3.046 2.405-6.436 4.147-10.084 5.442l-.497.186c-1.766.627-3.615.974-5.465 1.266l-.364.058c-1.591.238-3.185.286-4.791.281h-.499l-1.339-.002h-1.408c-.886-.001-1.772-.001-2.656-.003-3.085-.001-6.163-.005-9.247-.008v-12.78l3.606-.006 2.282-.007c1.205-.006 2.407-.01 3.611-.011.878 0 1.754-.003 2.63-.007.462-.002.928-.005 1.389-.002 6.302.01 12.003-1.72 16.65-6.117l.291-.272c3.951-3.807 6.097-9.302 6.241-14.736.04-3.078-.476-6.048-1.68-8.892l-.104-.244c-2.382-5.559-6.98-9.439-12.504-11.675-2.204-.816-4.522-1.16-6.863-1.134-.301.004-.606.004-.909.001-1.677.003-3.304.127-4.924.591l-.331.098c-2.625.758-5.093 1.928-7.254 3.607l-.323.249c-1.046.817-2.031 1.682-2.873 2.71l-.154.179c-2.354 2.782-3.9 5.988-4.727 9.529l-.054.229c-.391 1.8-.423 3.633-.42 5.471l-.001.74c.002 1.4.001 2.801-.004 4.201l-.004 3.779c0 1.824-.001 3.649-.003 5.473l-.006 2.813-.005 6.076-.001.267-.008 8.614c-.001 2.023-.004 4.045-.006 6.066v.259l-.004 2.55-.006 5.46c0 1.331-.001 2.658-.003 3.985-.002.72-.002 1.438-.002 2.156 0 .658 0 1.317-.002 1.978v.708c.004 2.098-.07 4.214-.545 6.263l-.077.359-.161.716c-.071.302-.136.605-.199.909-.55 2.538-1.6 5.067-2.832 7.348l-.214.42c-.965 1.921-2.22 3.714-3.574 5.38l-.152.183c-.78.973-1.639 1.866-2.519 2.751l-.308.313a25.81 25.81 0 0 1-2.226 1.955l-.22.179c-3.698 2.953-7.975 5.044-12.538 6.262-.212.056-.422.115-.631.176-8.366 2.287-17.8.738-25.278-3.512-3.639-2.122-7.171-4.945-9.721-8.307-.121-.15-.241-.298-.356-.448a36.247 36.247 0 0 1-2.605-3.814l-.144-.234c-.889-1.466-1.562-3.034-2.222-4.617l-.11-.265c-3.253-7.791-2.911-17.367.184-25.133.865-2.069 1.869-4.089 3.123-5.951l.172-.261a31.91 31.91 0 0 1 2.547-3.29l.229-.263c.13-.152.266-.304.396-.452l.425-.493c1.216-1.389 2.593-2.615 4.068-3.723.208-.157.414-.315.62-.476 1.184-.901 2.437-1.665 3.729-2.4l.272-.159c1.586-.893 3.235-1.573 4.934-2.21l.253-.097c4.232-1.567 8.615-1.836 13.079-1.82 1.359 0 2.732.002 4.093.007 1.209.005 2.417.006 3.621.006 1.899.002 3.794.007 5.688.012v12.78l-3.612.013-2.282.011c-1.201.005-2.401.012-3.61.016-.876.003-1.751.005-2.629.013-.463.002-.924.005-1.387.006-4.322.005-8.193.592-12.033 2.662l-.217.114c-1.458.773-2.697 1.731-3.908 2.849-.206.185-.413.362-.63.535-.435.376-.794.788-1.16 1.232l-.153.18a21.18 21.18 0 0 0-4.474 8.651l-.094.342c-.42 1.682-.54 3.395-.536 5.127v.272c.019 5.672 2.392 10.671 6.206 14.769l.215.237c3.296 3.453 8.483 5.917 13.247 6.046.448.008.896.01 1.345.01.276 0 .553.003.835.005 4.917.019 9.864-1.73 13.577-4.995l.303-.267c.299-.263.592-.527.881-.799l.262-.247c.787-.756 1.451-1.599 2.104-2.474l.151-.201c1.929-2.606 3.117-5.723 3.635-8.909l.059-.362c.194-1.296.21-2.573.207-3.883l.003-.74v-2.02c0-.73 0-1.459.005-2.188v-3.782c0-1.827.004-3.654.007-5.481 0-.939 0-1.877.004-2.816l.003-6.082v-.268c.005-2.876.008-5.75.008-8.627l.013-8.883c.004-1.823.004-3.646.004-5.469 0-1.33.004-2.659.004-3.989.003-.721.003-1.442.003-2.159v-1.98c.005-.237.005-.473 0-.711-.011-8.523 3.072-17.023 8.941-23.268.244-.263.483-.531.717-.805.416-.48.861-.918 1.324-1.352l.252-.232a26.259 26.259 0 0 1 2.184-1.812l.46-.349c1.39-1.058 2.837-1.951 4.375-2.775l.318-.172c5.005-2.666 10.5-3.877 16.15-3.889Z" style="transform-box:fill-box;transform-origin:50% 50%" transform="rotate(-31 0 0)"/></svg>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
body {
|
|
4
|
+
font-family:
|
|
5
|
+
system-ui,
|
|
6
|
+
-apple-system,
|
|
7
|
+
BlinkMacSystemFont,
|
|
8
|
+
'Segoe UI',
|
|
9
|
+
'Open Sans',
|
|
10
|
+
'Helvetica Neue',
|
|
11
|
+
sans-serif;
|
|
12
|
+
|
|
13
|
+
background-color: hsl(var(--background));
|
|
14
|
+
color: hsl(var(--foreground));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@theme {
|
|
18
|
+
--color-accent: hsl(var(--accent));
|
|
19
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
20
|
+
|
|
21
|
+
--color-background: hsl(var(--background));
|
|
22
|
+
--color-foreground: hsl(var(--foreground));
|
|
23
|
+
--color-border: hsl(var(--border));
|
|
24
|
+
--color-ring: hsl(var(--ring));
|
|
25
|
+
|
|
26
|
+
--color-card: hsl(var(--card));
|
|
27
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
28
|
+
|
|
29
|
+
--color-muted: hsl(var(--muted));
|
|
30
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
31
|
+
|
|
32
|
+
--color-secondary: hsl(var(--secondary));
|
|
33
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
34
|
+
|
|
35
|
+
--color-destructive: hsl(var(--destructive));
|
|
36
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@layer utilities {
|
|
40
|
+
.text-balance {
|
|
41
|
+
text-wrap: balance;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@layer base {
|
|
46
|
+
:root {
|
|
47
|
+
--background: 0 0% 100%;
|
|
48
|
+
--foreground: 0 0% 3.9%;
|
|
49
|
+
--card: 0 0% 100%;
|
|
50
|
+
--card-foreground: 0 0% 3.9%;
|
|
51
|
+
--popover: 0 0% 100%;
|
|
52
|
+
--popover-foreground: 0 0% 3.9%;
|
|
53
|
+
--primary: 0 0% 9%;
|
|
54
|
+
--primary-foreground: 0 0% 98%;
|
|
55
|
+
--secondary: 0 0% 96.1%;
|
|
56
|
+
--secondary-foreground: 0 0% 9%;
|
|
57
|
+
--muted: 0 0% 96.1%;
|
|
58
|
+
--muted-foreground: 0 0 40%;
|
|
59
|
+
--accent: 0 0% 96.1%;
|
|
60
|
+
--accent-foreground: 0 0% 9%;
|
|
61
|
+
--destructive: 0 84.2% 60.2%;
|
|
62
|
+
--destructive-foreground: 0 0% 98%;
|
|
63
|
+
--border: 0 0% 89.8%;
|
|
64
|
+
--input: 0 0% 89.8%;
|
|
65
|
+
--ring: 0 0% 3.9%;
|
|
66
|
+
--chart-1: 12 76% 61%;
|
|
67
|
+
--chart-2: 173 58% 39%;
|
|
68
|
+
--chart-3: 197 37% 24%;
|
|
69
|
+
--chart-4: 43 74% 66%;
|
|
70
|
+
--chart-5: 27 87% 67%;
|
|
71
|
+
--radius: 0.5rem;
|
|
72
|
+
--sidebar-background: 0 0% 98%;
|
|
73
|
+
--sidebar-foreground: 240 5.3% 26.1%;
|
|
74
|
+
--sidebar-primary: 240 5.9% 10%;
|
|
75
|
+
--sidebar-primary-foreground: 0 0% 98%;
|
|
76
|
+
--sidebar-accent: 240 4.8% 95.9%;
|
|
77
|
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
78
|
+
--sidebar-border: 220 13% 91%;
|
|
79
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
80
|
+
}
|
|
81
|
+
@media (prefers-color-scheme: dark) {
|
|
82
|
+
:root {
|
|
83
|
+
--background: 0 0% 3.9%;
|
|
84
|
+
--foreground: 0 0% 98%;
|
|
85
|
+
--card: 0 0% 3.9%;
|
|
86
|
+
--card-foreground: 0 0% 98%;
|
|
87
|
+
--popover: 0 0% 3.9%;
|
|
88
|
+
--popover-foreground: 0 0% 98%;
|
|
89
|
+
--primary: 0 0% 98%;
|
|
90
|
+
--primary-foreground: 0 0% 9%;
|
|
91
|
+
--secondary: 0 0% 14.9%;
|
|
92
|
+
--secondary-foreground: 0 0% 98%;
|
|
93
|
+
--muted: 0 0% 14.9%;
|
|
94
|
+
--muted-foreground: 0 0% 63.9%;
|
|
95
|
+
--accent: 0 0% 14.9%;
|
|
96
|
+
--accent-foreground: 0 0% 98%;
|
|
97
|
+
--destructive: 0 62.8% 30.6%;
|
|
98
|
+
--destructive-foreground: 0 0% 98%;
|
|
99
|
+
--border: 0 0% 14.9%;
|
|
100
|
+
--input: 0 0% 14.9%;
|
|
101
|
+
--ring: 0 0% 83.1%;
|
|
102
|
+
--chart-1: 220 70% 50%;
|
|
103
|
+
--chart-2: 160 60% 45%;
|
|
104
|
+
--chart-3: 30 80% 55%;
|
|
105
|
+
--chart-4: 280 65% 60%;
|
|
106
|
+
--chart-5: 340 75% 55%;
|
|
107
|
+
--sidebar-background: 240 5.9% 10%;
|
|
108
|
+
--sidebar-foreground: 240 4.8% 95.9%;
|
|
109
|
+
--sidebar-primary: 224.3 76.3% 48%;
|
|
110
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
111
|
+
--sidebar-accent: 240 3.7% 15.9%;
|
|
112
|
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
113
|
+
--sidebar-border: 240 3.7% 15.9%;
|
|
114
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
* {
|
|
119
|
+
@apply border-border;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
textarea {
|
|
124
|
+
resize: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.squircle {
|
|
128
|
+
border-radius: 32px;
|
|
129
|
+
corner-shape: squircle;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
::backdrop {
|
|
133
|
+
backdrop-filter: blur(4px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
[popover] {
|
|
137
|
+
transition:
|
|
138
|
+
opacity 150ms ease-in-out,
|
|
139
|
+
display 150ms ease-out allow-discrete,
|
|
140
|
+
overlay 150ms ease-out allow-discrete;
|
|
141
|
+
opacity: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
[popover]:popover-open {
|
|
145
|
+
opacity: 1;
|
|
146
|
+
display: block;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@starting-style {
|
|
150
|
+
[popover]:popover-open {
|
|
151
|
+
opacity: 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import { HTMLAttributes } from 'react';
|
|
3
|
+
import cx from '../lib/cx.tsx';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center squircle border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
7
|
+
{
|
|
8
|
+
defaultVariants: {
|
|
9
|
+
variant: 'default',
|
|
10
|
+
},
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
|
14
|
+
destructive:
|
|
15
|
+
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
|
16
|
+
outline: 'text-foreground',
|
|
17
|
+
secondary:
|
|
18
|
+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export interface BadgeProps
|
|
25
|
+
extends HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
|
|
26
|
+
|
|
27
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
28
|
+
return <div className={cx(badgeVariants({ variant }), className)} {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { type ButtonHTMLAttributes, type ReactNode, useOptimistic, useTransition } from 'react';
|
|
4
|
+
import { useFormStatus } from 'react-dom';
|
|
5
|
+
import cx from '../lib/cx.tsx';
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap squircle text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
9
|
+
{
|
|
10
|
+
defaultVariants: {
|
|
11
|
+
size: 'default',
|
|
12
|
+
variant: 'default',
|
|
13
|
+
},
|
|
14
|
+
variants: {
|
|
15
|
+
size: {
|
|
16
|
+
default: 'h-10 px-3 py-2 active:pt-[11px] active:pb-[9px]',
|
|
17
|
+
icon: 'h-10 w-10',
|
|
18
|
+
lg: 'h-11 squircle px-6 active:pt-[11px] active:pb-[9px]',
|
|
19
|
+
sm: 'h-9 squircle px-2 active:pt-[11px] active:pb-[9px]',
|
|
20
|
+
},
|
|
21
|
+
variant: {
|
|
22
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
23
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
24
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
25
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
26
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
27
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const Button = ({
|
|
34
|
+
action,
|
|
35
|
+
asChild = false,
|
|
36
|
+
className,
|
|
37
|
+
disabled,
|
|
38
|
+
onClick: initialOnClick,
|
|
39
|
+
size,
|
|
40
|
+
variant,
|
|
41
|
+
...props
|
|
42
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> &
|
|
43
|
+
VariantProps<typeof buttonVariants> & {
|
|
44
|
+
action?: () => void;
|
|
45
|
+
asChild?: boolean;
|
|
46
|
+
}) => {
|
|
47
|
+
const Component = asChild ? Slot : 'button';
|
|
48
|
+
|
|
49
|
+
const [isPending, startTransition] = useTransition();
|
|
50
|
+
|
|
51
|
+
const onClick = initialOnClick || (action ? () => startTransition(action) : undefined);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Component
|
|
55
|
+
className={cx(buttonVariants({ className, size, variant }))}
|
|
56
|
+
disabled={disabled !== undefined ? disabled : isPending}
|
|
57
|
+
onClick={onClick}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const AsyncButton = ({
|
|
64
|
+
action,
|
|
65
|
+
asChild = false,
|
|
66
|
+
children,
|
|
67
|
+
className,
|
|
68
|
+
disabled,
|
|
69
|
+
pendingPlaceholder = '...',
|
|
70
|
+
size,
|
|
71
|
+
variant,
|
|
72
|
+
...props
|
|
73
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> &
|
|
74
|
+
VariantProps<typeof buttonVariants> & {
|
|
75
|
+
action?: () => Promise<unknown> | void;
|
|
76
|
+
asChild?: boolean;
|
|
77
|
+
pendingPlaceholder?: ReactNode;
|
|
78
|
+
}) => {
|
|
79
|
+
const Component = asChild ? Slot : 'button';
|
|
80
|
+
|
|
81
|
+
const [optimisticIsPending, setOptimisticIsPending] = useOptimistic(false);
|
|
82
|
+
const [transitionIsPending, startTransition] = useTransition();
|
|
83
|
+
const { pending: formIsPending } = useFormStatus();
|
|
84
|
+
|
|
85
|
+
const isPending = transitionIsPending || optimisticIsPending || formIsPending;
|
|
86
|
+
|
|
87
|
+
const onClick = action
|
|
88
|
+
? () => {
|
|
89
|
+
startTransition(async () => {
|
|
90
|
+
setOptimisticIsPending(true);
|
|
91
|
+
await action();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
: undefined;
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Component
|
|
98
|
+
className={cx(buttonVariants({ className, size, variant }))}
|
|
99
|
+
disabled={disabled || isPending || undefined}
|
|
100
|
+
onClick={onClick}
|
|
101
|
+
{...props}
|
|
102
|
+
>
|
|
103
|
+
{isPending ? pendingPlaceholder : children}
|
|
104
|
+
</Component>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { AsyncButton, Button, buttonVariants };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { VStack } from '@nkzw/stack';
|
|
2
|
+
import { HTMLAttributes } from 'react';
|
|
3
|
+
import cx from '../lib/cx.tsx';
|
|
4
|
+
|
|
5
|
+
export default function Card({
|
|
6
|
+
children,
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
} & HTMLAttributes<HTMLDivElement>) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cx(
|
|
16
|
+
'relative overflow-hidden squircle border border-gray-200/70 bg-white/85 shadow-[0_16px_70px_-32px_rgba(15,23,42,0.45)] dark:border-neutral-800 dark:bg-neutral-900/80 transition duration-200 ease-out hover:shadow-[0_24px_80px_-38px_rgba(15,23,42,0.55)]',
|
|
17
|
+
className,
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<div className="pointer-events-none absolute inset-0 bg-linear-to-br from-blue-500/10 via-transparent to-indigo-500/15 opacity-30 transition duration-200" />
|
|
22
|
+
<VStack
|
|
23
|
+
className="relative z-10 flex h-full w-full"
|
|
24
|
+
gap={16}
|
|
25
|
+
horizontalPadding={20}
|
|
26
|
+
verticalPadding={16}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</VStack>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Comment } from '@app/server/src/router.ts';
|
|
2
|
+
import Stack from '@nkzw/stack';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { useView, view, ViewRef } from 'react-fate';
|
|
5
|
+
import { useFateClient } from 'react-fate';
|
|
6
|
+
import { Button } from './Button.tsx';
|
|
7
|
+
import Link from './Link.tsx';
|
|
8
|
+
|
|
9
|
+
export const CommentView = view<Comment>()({
|
|
10
|
+
author: {
|
|
11
|
+
id: true,
|
|
12
|
+
name: true,
|
|
13
|
+
username: true,
|
|
14
|
+
},
|
|
15
|
+
content: true,
|
|
16
|
+
id: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const CommentViewWithPostCount = view<Comment>()({
|
|
20
|
+
...CommentView,
|
|
21
|
+
post: { commentCount: true },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default function CommentCard({
|
|
25
|
+
comment: commentRef,
|
|
26
|
+
link,
|
|
27
|
+
post,
|
|
28
|
+
}: {
|
|
29
|
+
comment: ViewRef<'Comment'>;
|
|
30
|
+
link?: boolean;
|
|
31
|
+
post: { commentCount: number; id: string; title: string };
|
|
32
|
+
}) {
|
|
33
|
+
const fate = useFateClient();
|
|
34
|
+
const comment = useView(CommentView, commentRef);
|
|
35
|
+
const { author } = comment;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className="group squircle border border-gray-200/80 bg-gray-100/50 p-3 text-sm dark:border-neutral-800 dark:bg-neutral-900/40"
|
|
40
|
+
key={comment.id}
|
|
41
|
+
>
|
|
42
|
+
<Stack between gap={16}>
|
|
43
|
+
<p className="font-medium text-gray-900 dark:text-gray-200">
|
|
44
|
+
{author?.name ?? 'Anonymous'}
|
|
45
|
+
</p>
|
|
46
|
+
<Button
|
|
47
|
+
className="opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
|
|
48
|
+
onClick={async () => {
|
|
49
|
+
await fate.mutations.comment.delete({
|
|
50
|
+
delete: true,
|
|
51
|
+
input: { id: comment.id },
|
|
52
|
+
optimistic: {
|
|
53
|
+
post: { commentCount: post.commentCount - 1, id: post.id },
|
|
54
|
+
},
|
|
55
|
+
view: view<Comment>()({
|
|
56
|
+
id: true,
|
|
57
|
+
post: { commentCount: true },
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
}}
|
|
61
|
+
size="sm"
|
|
62
|
+
variant="ghost"
|
|
63
|
+
>
|
|
64
|
+
<X size={14} />
|
|
65
|
+
</Button>
|
|
66
|
+
</Stack>
|
|
67
|
+
<p className="text-foreground/80">{comment.content}</p>
|
|
68
|
+
{link && (
|
|
69
|
+
<Link className="text-blue-500 underline hover:no-underline" to={`/post/${post.id}`}>
|
|
70
|
+
{post.title}
|
|
71
|
+
</Link>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { VStack } from '@nkzw/stack';
|
|
2
|
+
|
|
3
|
+
export default function Error({ error: initialError }: { error: unknown }) {
|
|
4
|
+
const error = initialError instanceof Error ? (initialError as Error) : null;
|
|
5
|
+
return (
|
|
6
|
+
<VStack gap={12}>
|
|
7
|
+
<h3 className="text-xl font-semibold text-red-700">Error</h3>
|
|
8
|
+
<code>{error ? error.stack || `fate Error: ${error.message}` : String(initialError)}</code>
|
|
9
|
+
<div>
|
|
10
|
+
<a
|
|
11
|
+
className="cursor-pointer underline decoration-gray-300 hover:no-underline"
|
|
12
|
+
onClick={() => window.location.reload()}
|
|
13
|
+
>
|
|
14
|
+
<fbt desc="Try again button">Try again</fbt>
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
17
|
+
</VStack>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import cx from '../lib/cx.tsx';
|
|
2
|
+
|
|
3
|
+
export default function H2({
|
|
4
|
+
children,
|
|
5
|
+
className,
|
|
6
|
+
}: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<h2 className={cx('text-2xl font-semibold text-gray-900 dark:text-gray-50', className)}>
|
|
12
|
+
{children}
|
|
13
|
+
</h2>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import cx from '../lib/cx.tsx';
|
|
2
|
+
|
|
3
|
+
export default function H2({
|
|
4
|
+
children,
|
|
5
|
+
className,
|
|
6
|
+
}: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}) {
|
|
10
|
+
return (
|
|
11
|
+
<h3
|
|
12
|
+
className={cx(
|
|
13
|
+
'bg-linear-to-r from-slate-900 to-slate-600 bg-clip-text text-lg font-semibold text-transparent transition dark:from-white dark:to-slate-200 uppercase tracking-widest duration-150 opacity-100 hover:opacity-70 active:translate-y-[1.5px]',
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</h3>
|
|
19
|
+
);
|
|
20
|
+
}
|