@weft-js/create 0.1.1 → 0.1.3

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 (127) hide show
  1. package/README.md +393 -19
  2. package/bin/create-weft.mjs +62 -14
  3. package/package.json +26 -16
  4. package/scripts/tarballs.mjs +56 -6
  5. package/src/data.mjs +90 -18
  6. package/src/index.mjs +210 -51
  7. package/src/stack/pages.mjs +151 -0
  8. package/src/stack/schema.mjs +195 -0
  9. package/src/stack.mjs +102 -313
  10. package/src/templates.mjs +174 -0
  11. package/src/wizard.mjs +65 -16
  12. package/templates/admin/files/drizzle/.schema-hash +1 -0
  13. package/templates/admin/files/drizzle/0000_admin.sql +136 -0
  14. package/templates/admin/files/drizzle/meta/0000_snapshot.json +771 -0
  15. package/templates/admin/files/drizzle/meta/_journal.json +13 -0
  16. package/templates/admin/files/public/favicon.svg +1 -0
  17. package/templates/admin/files/src/auth.server.ts +24 -0
  18. package/templates/admin/files/src/components/AuthPage.tsx +33 -0
  19. package/templates/admin/files/src/components/Failed.tsx +19 -0
  20. package/templates/admin/files/src/components/Logo.tsx +16 -0
  21. package/templates/admin/files/src/components/OrderDetail.tsx +93 -0
  22. package/templates/admin/files/src/components/OrdersFilters.tsx +44 -0
  23. package/templates/admin/files/src/components/OrdersTable.tsx +144 -0
  24. package/templates/admin/files/src/components/Overview.tsx +113 -0
  25. package/templates/admin/files/src/components/StatusBadge.tsx +12 -0
  26. package/templates/admin/files/src/db/schema.ts +68 -0
  27. package/templates/admin/files/src/lib/orders.ts +60 -0
  28. package/templates/admin/files/src/routes/404.tsx +20 -0
  29. package/templates/admin/files/src/routes/_error.tsx +20 -0
  30. package/templates/admin/files/src/routes/_layout.tsx +6 -0
  31. package/templates/admin/files/src/routes/admin/_layout.tsx +89 -0
  32. package/templates/admin/files/src/routes/admin/index.tsx +45 -0
  33. package/templates/admin/files/src/routes/admin/nested/_layout.tsx +35 -0
  34. package/templates/admin/files/src/routes/admin/nested/index.tsx +77 -0
  35. package/templates/admin/files/src/routes/admin/orders/[id].tsx +24 -0
  36. package/templates/admin/files/src/routes/admin/orders/index.tsx +28 -0
  37. package/templates/admin/files/src/routes/admin/per-row.tsx +81 -0
  38. package/templates/admin/files/src/routes/admin/slow.tsx +74 -0
  39. package/templates/admin/files/src/routes/index.tsx +13 -0
  40. package/templates/admin/files/src/routes/login.tsx +53 -0
  41. package/templates/admin/files/src/routes/signup.tsx +60 -0
  42. package/templates/admin/files/src/server/orders.ts +193 -0
  43. package/templates/admin/files/src/server/reports.ts +92 -0
  44. package/templates/admin/files/src/server/session.ts +42 -0
  45. package/templates/admin/files/src/theme.css +43 -0
  46. package/templates/admin/files/weft.config.ts +31 -0
  47. package/templates/admin/template.json +22 -0
  48. package/templates/catalog/files/drizzle/.schema-hash +1 -0
  49. package/templates/catalog/files/drizzle/0000_catalog.sql +47 -0
  50. package/templates/catalog/files/drizzle/meta/0000_snapshot.json +263 -0
  51. package/templates/catalog/files/drizzle/meta/_journal.json +13 -0
  52. package/templates/catalog/files/public/favicon.svg +1 -0
  53. package/templates/catalog/files/seed.sql +13 -0
  54. package/templates/catalog/files/src/components/AddToCart.tsx +22 -0
  55. package/templates/catalog/files/src/components/Aisles.tsx +31 -0
  56. package/templates/catalog/files/src/components/CartLines.tsx +96 -0
  57. package/templates/catalog/files/src/components/CartLink.tsx +29 -0
  58. package/templates/catalog/files/src/components/Failed.tsx +19 -0
  59. package/templates/catalog/files/src/components/Featured.tsx +19 -0
  60. package/templates/catalog/files/src/components/Filters.tsx +40 -0
  61. package/templates/catalog/files/src/components/Logo.tsx +16 -0
  62. package/templates/catalog/files/src/components/ProductArt.tsx +42 -0
  63. package/templates/catalog/files/src/components/ProductCard.tsx +42 -0
  64. package/templates/catalog/files/src/components/ProductList.tsx +65 -0
  65. package/templates/catalog/files/src/components/Related.tsx +19 -0
  66. package/templates/catalog/files/src/components/SiteFooter.tsx +9 -0
  67. package/templates/catalog/files/src/components/SiteHeader.tsx +16 -0
  68. package/templates/catalog/files/src/db/schema.ts +53 -0
  69. package/templates/catalog/files/src/lib/catalog.ts +35 -0
  70. package/templates/catalog/files/src/routes/404.tsx +18 -0
  71. package/templates/catalog/files/src/routes/_error.tsx +18 -0
  72. package/templates/catalog/files/src/routes/_layout.tsx +14 -0
  73. package/templates/catalog/files/src/routes/c/[slug].tsx +53 -0
  74. package/templates/catalog/files/src/routes/cart.tsx +29 -0
  75. package/templates/catalog/files/src/routes/index.tsx +84 -0
  76. package/templates/catalog/files/src/routes/p/[slug].tsx +67 -0
  77. package/templates/catalog/files/src/server/cart.ts +125 -0
  78. package/templates/catalog/files/src/server/catalog.ts +107 -0
  79. package/templates/catalog/files/src/theme.css +159 -0
  80. package/templates/catalog/files/weft.config.ts +30 -0
  81. package/templates/catalog/template.json +20 -0
  82. package/templates/landing/files/.weft/plugins.lock.json +42 -0
  83. package/templates/landing/files/assets/fonts/LICENSE-figtree.txt +93 -0
  84. package/templates/landing/files/assets/fonts/figtree.woff2 +0 -0
  85. package/templates/landing/files/assets/hero.webp +0 -0
  86. package/templates/landing/files/drizzle/.schema-hash +1 -0
  87. package/templates/landing/files/drizzle/0000_landing.sql +6 -0
  88. package/templates/landing/files/drizzle/meta/0000_snapshot.json +60 -0
  89. package/templates/landing/files/drizzle/meta/_journal.json +13 -0
  90. package/templates/landing/files/public/favicon.svg +1 -0
  91. package/templates/landing/files/src/components/SiteFooter.tsx +24 -0
  92. package/templates/landing/files/src/components/SiteHeader.tsx +23 -0
  93. package/templates/landing/files/src/components/WaitlistForm.tsx +36 -0
  94. package/templates/landing/files/src/db/schema.ts +7 -0
  95. package/templates/landing/files/src/routes/404.tsx +24 -0
  96. package/templates/landing/files/src/routes/_error.tsx +24 -0
  97. package/templates/landing/files/src/routes/_layout.tsx +14 -0
  98. package/templates/landing/files/src/routes/index.tsx +130 -0
  99. package/templates/landing/files/src/routes/joined.tsx +30 -0
  100. package/templates/landing/files/src/routes/pricing.tsx +131 -0
  101. package/templates/landing/files/src/routes/privacy.tsx +56 -0
  102. package/templates/landing/files/src/server/waitlist.ts +13 -0
  103. package/templates/landing/files/src/theme.css +108 -0
  104. package/templates/landing/files/weft.config.ts +44 -0
  105. package/templates/landing/template.json +17 -0
  106. package/templates/todo/files/drizzle/.schema-hash +1 -0
  107. package/templates/todo/files/drizzle/0000_todo.sql +82 -0
  108. package/templates/todo/files/drizzle/meta/0000_snapshot.json +462 -0
  109. package/templates/todo/files/drizzle/meta/_journal.json +13 -0
  110. package/templates/todo/files/public/favicon.svg +1 -0
  111. package/templates/todo/files/src/components/AuthPage.tsx +33 -0
  112. package/templates/todo/files/src/components/Logo.tsx +14 -0
  113. package/templates/todo/files/src/components/NewTodo.tsx +27 -0
  114. package/templates/todo/files/src/components/TodoList.tsx +101 -0
  115. package/templates/todo/files/src/db/schema.ts +18 -0
  116. package/templates/todo/files/src/lib/views.ts +13 -0
  117. package/templates/todo/files/src/routes/404.tsx +20 -0
  118. package/templates/todo/files/src/routes/_error.tsx +20 -0
  119. package/templates/todo/files/src/routes/_layout.tsx +6 -0
  120. package/templates/todo/files/src/routes/index.tsx +13 -0
  121. package/templates/todo/files/src/routes/login.tsx +53 -0
  122. package/templates/todo/files/src/routes/signup.tsx +57 -0
  123. package/templates/todo/files/src/routes/todos.tsx +87 -0
  124. package/templates/todo/files/src/server/todos.ts +98 -0
  125. package/templates/todo/files/src/theme.css +71 -0
  126. package/templates/todo/files/weft.config.ts +30 -0
  127. package/templates/todo/template.json +18 -0
@@ -0,0 +1,24 @@
1
+ import { Link, type Meta } from '@weft-js/start'
2
+ import { Center, Heading, Stack, Text } from '@weft-js/warp'
3
+
4
+ export const meta: Meta = { title: 'Something went wrong' }
5
+
6
+ export default function ServerError() {
7
+ return (
8
+ <section class="page-head">
9
+ <Center max="narrow" gutter>
10
+ <Stack gap="group" align="center">
11
+ <Heading level={1} role="display">
12
+ Something went wrong on our side
13
+ </Heading>
14
+ <Text class="lede" subtle>
15
+ It isn't you. Give it a minute and try again.
16
+ </Text>
17
+ <Link href="/" class="cta">
18
+ Back to the home page
19
+ </Link>
20
+ </Stack>
21
+ </Center>
22
+ </section>
23
+ )
24
+ }
@@ -0,0 +1,14 @@
1
+ import '../theme.css'
2
+ import type { RouteProps } from '@weft-js/start'
3
+ import { SiteFooter } from '#/components/SiteFooter'
4
+ import { SiteHeader } from '#/components/SiteHeader'
5
+
6
+ export default function Root(props: RouteProps) {
7
+ return (
8
+ <div class="site">
9
+ <SiteHeader />
10
+ <main id="content">{props.children}</main>
11
+ <SiteFooter />
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,130 @@
1
+ import { Link, type Meta } from '@weft-js/start'
2
+ import { Card, Center, Grid, Heading, Split, Stack, Text } from '@weft-js/warp'
3
+ import { Media } from '@weft-js/core/media'
4
+ import hero from '../../assets/hero.webp?media'
5
+ import { WaitlistForm } from '#/components/WaitlistForm'
6
+
7
+ export const meta: Meta = {
8
+ title: { absolute: 'Emberline: hear what customers keep asking for' },
9
+ description: 'Emberline turns customer calls into a ranked list of what to build next.',
10
+ }
11
+ export const prerender = true
12
+
13
+ const features = [
14
+ {
15
+ title: 'Every call, heard',
16
+ body: "Emberline reads your sales and support calls and pulls out each request, in the customer's own words.",
17
+ },
18
+ {
19
+ title: 'Ranked, not guessed',
20
+ body: 'Requests are grouped and weighted by how often they come up and which accounts asked.',
21
+ },
22
+ {
23
+ title: 'Proof beside every card',
24
+ body: 'Each item links back to the quotes behind it, so the roadmap argument ends quickly.',
25
+ },
26
+ ]
27
+
28
+ const steps = [
29
+ {
30
+ title: 'Connect your calls',
31
+ body: 'Point Emberline at your call recorder and help desk. Setup takes a few minutes.',
32
+ },
33
+ {
34
+ title: 'Read the signals',
35
+ body: 'Requests arrive grouped into Now, Next and Later, with a count beside each one.',
36
+ },
37
+ {
38
+ title: 'Close the loop',
39
+ body: 'When a feature ships, tell every customer who asked for it, with one digest.',
40
+ },
41
+ ]
42
+
43
+ function heroArt() {
44
+ return (
45
+ <Media
46
+ src={hero}
47
+ priority
48
+ sizes="(max-width: 599px) 100vw, min(46vw, 696px)"
49
+ class="hero-art"
50
+ alt="The Emberline board: customer requests grouped into Now, Next and Later, with a quote from a renewal call"
51
+ />
52
+ )
53
+ }
54
+
55
+ export default function Home() {
56
+ return (
57
+ <Stack gap="none">
58
+ <section class="hero">
59
+ <Center max="wide" gutter>
60
+ <Split gap="section" side="end" width="half">
61
+ <Stack gap="group" class="hero-copy" id="join">
62
+ <Text role="eyebrow" tone="accent">
63
+ Early access opens this winter
64
+ </Text>
65
+ <Heading level={1} role="display">
66
+ Hear what customers keep asking for
67
+ </Heading>
68
+ <Text class="lede" subtle>
69
+ Emberline listens to your customer calls and turns them into a ranked list of what
70
+ to build next.
71
+ </Text>
72
+ <WaitlistForm />
73
+ </Stack>
74
+ {heroArt()}
75
+ </Split>
76
+ </Center>
77
+ </section>
78
+
79
+ <section class="band">
80
+ <Center max="wide" gutter>
81
+ <Stack gap="section">
82
+ <Heading level={2} role="title" class="band-head">
83
+ Stop losing what customers tell you
84
+ </Heading>
85
+ <Grid gap="group" min="sm" cols={3}>
86
+ {features.map((feature) => (
87
+ <Card key={feature.title} title={feature.title}>
88
+ <Text subtle>{feature.body}</Text>
89
+ </Card>
90
+ ))}
91
+ </Grid>
92
+ </Stack>
93
+ </Center>
94
+ </section>
95
+
96
+ <section class="band band-sunken">
97
+ <Center max="wide" gutter>
98
+ <Stack gap="section">
99
+ <Heading level={2} role="title">
100
+ How it works
101
+ </Heading>
102
+ <Grid as="ol" gap="group" min="sm" cols={3} class="steps">
103
+ {steps.map((step) => (
104
+ <li key={step.title}>
105
+ <Stack gap="related">
106
+ <Heading level={3}>{step.title}</Heading>
107
+ <Text subtle>{step.body}</Text>
108
+ </Stack>
109
+ </li>
110
+ ))}
111
+ </Grid>
112
+ </Stack>
113
+ </Center>
114
+ </section>
115
+
116
+ <section class="closing">
117
+ <Center max="narrow" gutter>
118
+ <Stack gap="group" align="center">
119
+ <Heading level={2} role="title">
120
+ Your customers already told you what to build.
121
+ </Heading>
122
+ <Link href="#join" class="cta">
123
+ Get early access
124
+ </Link>
125
+ </Stack>
126
+ </Center>
127
+ </section>
128
+ </Stack>
129
+ )
130
+ }
@@ -0,0 +1,30 @@
1
+ import { Link, type Meta } from '@weft-js/start'
2
+ import { Center, Heading, Stack, Text } from '@weft-js/warp'
3
+
4
+ export const meta: Meta = {
5
+ title: "You're on the list",
6
+ description: 'We will write when your place in the Emberline queue comes up.',
7
+ extra: [{ name: 'robots', content: 'noindex' }],
8
+ }
9
+ export const prerender = true
10
+
11
+ export default function Joined() {
12
+ return (
13
+ <section class="page-head">
14
+ <Center max="narrow" gutter>
15
+ <Stack gap="group" align="center">
16
+ <Heading level={1} role="display">
17
+ You're on the list
18
+ </Heading>
19
+ <Text class="lede" subtle>
20
+ We'll write when your place in line comes up. Everyone hears from us in the order they
21
+ joined.
22
+ </Text>
23
+ <Link href="/pricing" class="cta" data-emphasis="outline">
24
+ See pricing
25
+ </Link>
26
+ </Stack>
27
+ </Center>
28
+ </section>
29
+ )
30
+ }
@@ -0,0 +1,131 @@
1
+ import { CheckIcon } from '@weft-js/warp-icons'
2
+ import { Link, type Meta } from '@weft-js/start'
3
+ import { Accordion, Badge, Card, Center, Grid, Heading, Stack, Text } from '@weft-js/warp'
4
+
5
+ export const meta: Meta = {
6
+ title: 'Pricing',
7
+ description: 'Free for one person. Simple per-seat pricing for teams.',
8
+ }
9
+ export const prerender = true
10
+
11
+ const plans = [
12
+ {
13
+ name: 'Starter',
14
+ price: '$0',
15
+ unit: 'forever',
16
+ blurb: 'For a founder reading their own calls.',
17
+ items: ['50 calls a month', 'Signals board', 'CSV export'],
18
+ cta: 'Join for free',
19
+ },
20
+ {
21
+ name: 'Team',
22
+ price: '$24',
23
+ unit: 'per seat, per month',
24
+ blurb: 'For product teams that share a roadmap.',
25
+ items: ['Unlimited calls', 'CRM and Slack digests', 'Team spaces', 'Priority support'],
26
+ cta: 'Get early access',
27
+ featured: true,
28
+ },
29
+ {
30
+ name: 'Enterprise',
31
+ price: 'Talk to us',
32
+ unit: 'billed yearly',
33
+ blurb: 'For companies with records to keep.',
34
+ items: ['Everything in Team', 'Single sign-on', 'Audit log', 'Data residency'],
35
+ cta: 'Get in line',
36
+ },
37
+ ]
38
+
39
+ const questions = [
40
+ {
41
+ value: 'open',
42
+ title: 'When does it open?',
43
+ content:
44
+ 'The first teams start this winter. Everyone on the waitlist hears from us in the order they joined.',
45
+ },
46
+ {
47
+ value: 'price',
48
+ title: 'Is the early price locked in?',
49
+ content:
50
+ 'Yes. Teams that join during early access keep their per-seat price for as long as they stay.',
51
+ },
52
+ {
53
+ value: 'data',
54
+ title: 'What happens to my data if I leave?',
55
+ content: "It's yours. Export every signal and quote as CSV at any time, on any plan.",
56
+ },
57
+ ]
58
+
59
+ export default function Pricing() {
60
+ return (
61
+ <Stack gap="none">
62
+ <section class="page-head">
63
+ <Center gutter>
64
+ <Stack gap="related" align="center">
65
+ <Heading level={1} role="display">
66
+ Simple pricing that grows with your team
67
+ </Heading>
68
+ <Text class="lede" subtle>
69
+ Free for one person. Pay per seat when the whole team comes along.
70
+ </Text>
71
+ </Stack>
72
+ </Center>
73
+ </section>
74
+
75
+ <section class="band band-tight">
76
+ <Center max="wide" gutter>
77
+ <Grid gap="group" min="sm" cols={3}>
78
+ {plans.map((plan) => (
79
+ <Card key={plan.name} class="plan" data-featured={plan.featured ? '' : undefined}>
80
+ <Stack gap="group" class="plan-body">
81
+ <Stack gap="tight">
82
+ <Heading level={2} role="title">
83
+ {plan.name}
84
+ </Heading>
85
+ <Text subtle>{plan.blurb}</Text>
86
+ </Stack>
87
+ {plan.featured && (
88
+ <Badge tone="accent" emphasis="solid" class="plan-flag">
89
+ Most popular
90
+ </Badge>
91
+ )}
92
+ <Stack gap="hair">
93
+ <Text class="plan-price">{plan.price}</Text>
94
+ <Text role="caption" subtle>
95
+ {plan.unit}
96
+ </Text>
97
+ </Stack>
98
+ <Stack as="ul" gap="tight" class="plan-items">
99
+ {plan.items.map((item) => (
100
+ <li key={item}>
101
+ <CheckIcon /> {item}
102
+ </li>
103
+ ))}
104
+ </Stack>
105
+ <Link
106
+ href="/#join"
107
+ class="cta"
108
+ data-emphasis={plan.featured ? undefined : 'outline'}
109
+ >
110
+ {plan.cta}
111
+ </Link>
112
+ </Stack>
113
+ </Card>
114
+ ))}
115
+ </Grid>
116
+ </Center>
117
+ </section>
118
+
119
+ <section class="band band-sunken">
120
+ <Center max="narrow" gutter>
121
+ <Stack gap="group">
122
+ <Heading level={2} role="title">
123
+ Questions
124
+ </Heading>
125
+ <Accordion items={questions} variant="line" level={3} />
126
+ </Stack>
127
+ </Center>
128
+ </section>
129
+ </Stack>
130
+ )
131
+ }
@@ -0,0 +1,56 @@
1
+ import type { Meta } from '@weft-js/start'
2
+ import { Center, Heading, Stack, Text } from '@weft-js/warp'
3
+
4
+ export const meta: Meta = {
5
+ title: 'Privacy',
6
+ description: 'What Emberline collects, why, and how to have it removed.',
7
+ }
8
+ export const prerender = true
9
+
10
+ const sections = [
11
+ {
12
+ title: 'What we collect',
13
+ body: 'The waitlist stores your email address and the time you joined. This site sets no cookies and runs no analytics.',
14
+ },
15
+ {
16
+ title: 'Why we collect it',
17
+ body: 'We use your address to tell you when your place in line comes up, and for nothing else. We send no newsletters.',
18
+ },
19
+ {
20
+ title: 'Who sees it',
21
+ body: "Only the people who run Emberline, and the database host that stores it on our behalf. We don't sell or share addresses.",
22
+ },
23
+ {
24
+ title: 'How long we keep it',
25
+ body: 'Until the waitlist closes or you ask us to remove it, whichever comes first.',
26
+ },
27
+ {
28
+ title: 'Removing your address',
29
+ body: "Reply to any message from us and we'll delete your address within seven days.",
30
+ },
31
+ ]
32
+
33
+ export default function Privacy() {
34
+ return (
35
+ <section class="page-head">
36
+ <Center max="measure" gutter>
37
+ <Stack gap="section">
38
+ <Stack gap="related">
39
+ <Heading level={1} role="display">
40
+ Privacy
41
+ </Heading>
42
+ <Text subtle>Last updated 1 November 2026</Text>
43
+ </Stack>
44
+ {sections.map((section) => (
45
+ <Stack as="section" gap="related" key={section.title}>
46
+ <Heading level={2} role="title">
47
+ {section.title}
48
+ </Heading>
49
+ <Text>{section.body}</Text>
50
+ </Stack>
51
+ ))}
52
+ </Stack>
53
+ </Center>
54
+ </section>
55
+ )
56
+ }
@@ -0,0 +1,13 @@
1
+ import { mutation, redirect } from '@weft-js/start'
2
+ import { z } from 'zod'
3
+ import { waitlist } from '#/db/schema'
4
+
5
+ // Joining twice looks the same as joining once, so the form can't be used to find out who's on the list.
6
+ // The redirect is what makes this work with JavaScript off: a plain POST lands on the confirmation page too.
7
+ export const joinWaitlist = mutation({
8
+ input: z.object({ email: z.email("That doesn't look like an email address.") }),
9
+ run: async ({ input, ctx }) => {
10
+ await ctx.db.insert(waitlist).values({ email: input.email.toLowerCase() }).onConflictDoNothing()
11
+ redirect('/joined', 303)
12
+ },
13
+ })
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Emberline's own CSS, over warp's tokens. The brand, font and paper colours are warp() options in weft.config.ts;
3
+ * this file is the page's shape and the ember line. Change those options first, then the rules here.
4
+ */
5
+ html { scroll-behavior: smooth; scroll-padding-block-start: var(--warp-space-page); }
6
+
7
+ body { margin: 0; }
8
+ .site { display: flex; flex-direction: column; min-block-size: 100dvh; }
9
+ .site > main { flex: 1; }
10
+
11
+ /* the ember line */
12
+ .site-head::before {
13
+ content: "";
14
+ display: block;
15
+ block-size: 3px;
16
+ background: linear-gradient(90deg, var(--warp-color-accent-solid), var(--warp-color-caution-solid), transparent);
17
+ }
18
+ /* warp's <Center max="wide" gutter>, for the chrome: nothing in it runs, so it stays plain markup */
19
+ .wrap {
20
+ box-sizing: content-box;
21
+ max-inline-size: var(--warp-layout-wide);
22
+ margin-inline: auto;
23
+ padding-inline: max(var(--warp-space-page), var(--warp-safe-left)) max(var(--warp-space-page), var(--warp-safe-right));
24
+ }
25
+ .site-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--warp-space-group); }
26
+ .site-links, .site-head nav, .site-foot nav { display: flex; align-items: center; gap: var(--warp-space-group); }
27
+ .site-head .wrap { padding-block: var(--warp-space-related); }
28
+ .site-head nav a { padding-block: var(--warp-space-tight); color: var(--warp-color-accent-text); text-decoration: none; transition: color var(--warp-duration-fast, 120ms) ease; }
29
+ .site-head nav a:hover, .site-head nav a:focus-visible, .site-head nav a[aria-current] { color: var(--warp-color-text); }
30
+ .site-head nav a[aria-current] { box-shadow: inset 0 -2px var(--warp-color-accent-solid); }
31
+ .site-foot { margin-block-start: auto; border-block-start: 1px solid var(--warp-color-border); }
32
+ .site-foot > .wrap { padding-block: var(--warp-space-section); }
33
+ .site-foot p { margin: 0; color: var(--warp-color-text-subtle); }
34
+ .site-foot-brand { display: grid; gap: var(--warp-space-tight); }
35
+ .site-foot p.site-foot-legal {
36
+ margin-block-start: var(--warp-space-group);
37
+ color: var(--warp-color-text-faint);
38
+ font-size: var(--warp-text-caption-size);
39
+ line-height: var(--warp-text-caption-leading);
40
+ }
41
+ .site-foot nav { flex-wrap: wrap; }
42
+ @media (width < 600px) { .site-foot .site-bar { flex-direction: column; align-items: stretch; gap: var(--warp-space-section); } }
43
+
44
+ .brand {
45
+ font-size: var(--warp-text-heading-size);
46
+ font-weight: var(--warp-text-heading-weight);
47
+ color: var(--warp-color-text);
48
+ text-decoration: none;
49
+ }
50
+
51
+ /* a link that reads as the page's main action */
52
+ .cta {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: var(--warp-space-related) var(--warp-space-group);
57
+ border: 1px solid transparent;
58
+ border-radius: var(--warp-radius-control);
59
+ background: var(--warp-color-accent-solid);
60
+ color: var(--warp-color-accent-on-solid);
61
+ font-weight: var(--warp-text-heading-weight);
62
+ text-decoration: none;
63
+ white-space: nowrap;
64
+ transition: background-color var(--warp-duration-fast, 120ms) ease;
65
+ }
66
+ .cta:hover { background: var(--warp-color-accent-solid-hover); }
67
+ .cta[data-size="sm"] { padding: var(--warp-space-tight) var(--warp-space-related); }
68
+ .cta[data-emphasis="outline"] { background: transparent; border-color: var(--warp-color-accent-border); color: var(--warp-color-accent-text); }
69
+ .cta[data-emphasis="outline"]:hover { background: var(--warp-color-accent-soft); }
70
+ .cta[data-emphasis="ghost"] { background: transparent; color: inherit; }
71
+ .cta[data-emphasis="ghost"]:hover { background: var(--warp-color-surface-hover); }
72
+
73
+ .hero { padding-block: var(--warp-space-page); }
74
+ .hero-copy { align-self: center; }
75
+ .hero-art {
76
+ display: block;
77
+ inline-size: 100%;
78
+ block-size: auto;
79
+ border-radius: var(--warp-radius-surface);
80
+ }
81
+ .lede { font-size: var(--warp-text-heading-size); line-height: var(--warp-text-body-leading); }
82
+
83
+ .waitlist-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--warp-space-related); align-items: start; }
84
+ .waitlist-error { color: var(--warp-color-critical-text); }
85
+ .waitlist-error:empty { display: none; }
86
+
87
+ .band { padding-block: var(--warp-space-page); }
88
+ .band-sunken { background: var(--warp-color-surface-sunken); }
89
+ .band-tight { padding-block-start: 0; }
90
+ .band-head { max-inline-size: 36rem; }
91
+
92
+ .steps { counter-reset: step; list-style: none; padding: 0; margin: 0; }
93
+ .steps > li { counter-increment: step; border-block-start: 2px solid var(--warp-color-text); padding-block-start: var(--warp-space-related); }
94
+ .steps > li::before { content: counter(step); display: block; margin-block-end: var(--warp-space-related); font-family: var(--warp-font-mono); color: var(--warp-color-accent-text); }
95
+
96
+ .closing { padding-block: var(--warp-space-page); text-align: center; }
97
+ .page-head { padding-block: var(--warp-space-page); }
98
+ .page-head [data-align="center"] { text-align: center; }
99
+
100
+ .plan { position: relative; }
101
+ .plan h2 { color: var(--warp-color-text); }
102
+ .plan[data-featured] { outline: 2px solid var(--warp-color-accent-solid); outline-offset: -2px; }
103
+ .plan-flag { position: absolute; inset-block-start: var(--warp-space-group); inset-inline-end: var(--warp-space-group); }
104
+ .plan-body { block-size: 100%; }
105
+ .plan-price { color: var(--warp-color-text); font-size: var(--warp-text-title-size); font-weight: var(--warp-text-title-weight); }
106
+ .plan-items { list-style: none; padding: 0; margin: 0; flex: 1; }
107
+ .plan-items svg { color: var(--warp-color-accent-text); vertical-align: -2px; }
108
+
@@ -0,0 +1,44 @@
1
+ import { defineConfig } from '@weft-js/core/config'
2
+ import { warp } from '@weft-js/warp/plugin'
3
+ import { text } from '@weft-js/text'
4
+ import { media } from '@weft-js/plugin-media'
5
+
6
+ export default defineConfig({
7
+ site: {
8
+ name: 'Emberline',
9
+ locale: 'en_US',
10
+ title: '%s · Emberline',
11
+ description:
12
+ 'Emberline listens to your customer calls and turns them into a ranked list of what to build next.',
13
+ head: '<link rel="icon" href="/favicon.svg" type="image/svg+xml">',
14
+ },
15
+ plugins: [
16
+ warp({
17
+ seed: { accent: '#c2410c' },
18
+ dials: { expression: 'vivid' },
19
+ radius: 'round',
20
+ fonts: {
21
+ display: '"Figtree", "Figtree Fallback", ui-sans-serif, system-ui, sans-serif',
22
+ body: '"Figtree", "Figtree Fallback", ui-sans-serif, system-ui, sans-serif',
23
+ },
24
+ tokens: {
25
+ '--warp-color-page': 'light-dark(#f8f1e4, #1d1613)',
26
+ '--warp-color-surface': 'light-dark(#fdf8ee, #261d19)',
27
+ '--warp-color-surface-raised': 'light-dark(#fffcf5, #281e1a)',
28
+ '--warp-color-surface-sunken': 'light-dark(#f5eee0, #171110)',
29
+ },
30
+ }),
31
+ text({
32
+ measure: 'off',
33
+ fonts: {
34
+ body: {
35
+ family: 'Figtree',
36
+ src: 'assets/fonts/figtree.woff2',
37
+ fallback: 'Arial',
38
+ axes: { wght: [300, 900] },
39
+ },
40
+ },
41
+ }),
42
+ media(),
43
+ ],
44
+ })
@@ -0,0 +1,17 @@
1
+ {
2
+ "requires": { "db": "postgres", "auth": false },
3
+ "pages": ["/", "/pricing", "/privacy", "/joined"],
4
+ "dependencies": { "@weft-js/warp-icons": "0.0.0", "@weft-js/plugin-media": "0.0.0", "@weft-js/text": "0.0.0" },
5
+ "warp": {
6
+ "seed": { "accent": "#c2410c" },
7
+ "dials": { "expression": "vivid" },
8
+ "radius": "round",
9
+ "fonts": { "display": "\"Figtree\", \"Figtree Fallback\", ui-sans-serif, system-ui, sans-serif", "body": "\"Figtree\", \"Figtree Fallback\", ui-sans-serif, system-ui, sans-serif" },
10
+ "tokens": {
11
+ "--warp-color-page": "light-dark(#f8f1e4, #1d1613)",
12
+ "--warp-color-surface": "light-dark(#fdf8ee, #261d19)",
13
+ "--warp-color-surface-raised": "light-dark(#fffcf5, #281e1a)",
14
+ "--warp-color-surface-sunken": "light-dark(#f5eee0, #171110)"
15
+ }
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ 2fea01fb1d5e6e88
@@ -0,0 +1,82 @@
1
+ CREATE TABLE IF NOT EXISTS "todos" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "user_id" varchar(255) NOT NULL,
4
+ "title" text NOT NULL,
5
+ "done" boolean DEFAULT false NOT NULL,
6
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
7
+ );
8
+ --> statement-breakpoint
9
+ CREATE TABLE IF NOT EXISTS "account" (
10
+ "id" text PRIMARY KEY NOT NULL,
11
+ "accountId" text NOT NULL,
12
+ "providerId" text NOT NULL,
13
+ "userId" text NOT NULL,
14
+ "accessToken" text,
15
+ "refreshToken" text,
16
+ "idToken" text,
17
+ "accessTokenExpiresAt" timestamp with time zone,
18
+ "refreshTokenExpiresAt" timestamp with time zone,
19
+ "scope" text,
20
+ "password" text,
21
+ "createdAt" timestamp with time zone NOT NULL,
22
+ "updatedAt" timestamp with time zone NOT NULL
23
+ );
24
+ --> statement-breakpoint
25
+ CREATE TABLE IF NOT EXISTS "jwks" (
26
+ "id" text PRIMARY KEY NOT NULL,
27
+ "publicKey" text NOT NULL,
28
+ "privateKey" text NOT NULL,
29
+ "createdAt" timestamp with time zone NOT NULL,
30
+ "expiresAt" timestamp with time zone,
31
+ "alg" text,
32
+ "crv" text
33
+ );
34
+ --> statement-breakpoint
35
+ CREATE TABLE IF NOT EXISTS "session" (
36
+ "id" text PRIMARY KEY NOT NULL,
37
+ "expiresAt" timestamp with time zone NOT NULL,
38
+ "token" text NOT NULL,
39
+ "createdAt" timestamp with time zone NOT NULL,
40
+ "updatedAt" timestamp with time zone NOT NULL,
41
+ "ipAddress" text,
42
+ "userAgent" text,
43
+ "userId" text NOT NULL,
44
+ CONSTRAINT "session_token_unique" UNIQUE("token")
45
+ );
46
+ --> statement-breakpoint
47
+ CREATE TABLE IF NOT EXISTS "user" (
48
+ "id" text PRIMARY KEY NOT NULL,
49
+ "name" text NOT NULL,
50
+ "email" text NOT NULL,
51
+ "emailVerified" boolean NOT NULL,
52
+ "image" text,
53
+ "createdAt" timestamp with time zone NOT NULL,
54
+ "updatedAt" timestamp with time zone NOT NULL,
55
+ CONSTRAINT "user_email_unique" UNIQUE("email")
56
+ );
57
+ --> statement-breakpoint
58
+ CREATE TABLE IF NOT EXISTS "verification" (
59
+ "id" text PRIMARY KEY NOT NULL,
60
+ "identifier" text NOT NULL,
61
+ "value" text NOT NULL,
62
+ "expiresAt" timestamp with time zone NOT NULL,
63
+ "createdAt" timestamp with time zone,
64
+ "updatedAt" timestamp with time zone
65
+ );
66
+ --> statement-breakpoint
67
+ DO $$ BEGIN
68
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"todos"'::regclass AND c.contype = 'f' AND a.attname = 'user_id') THEN
69
+ ALTER TABLE "todos" ADD CONSTRAINT "todos_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
70
+ END IF;
71
+ END $$;--> statement-breakpoint
72
+ DO $$ BEGIN
73
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"account"'::regclass AND c.contype = 'f' AND a.attname = 'userId') THEN
74
+ ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
75
+ END IF;
76
+ END $$;--> statement-breakpoint
77
+ DO $$ BEGIN
78
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint c JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = c.conkey[1] WHERE c.conrelid = '"session"'::regclass AND c.contype = 'f' AND a.attname = 'userId') THEN
79
+ ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
80
+ END IF;
81
+ END $$;--> statement-breakpoint
82
+ CREATE INDEX IF NOT EXISTS "todos_user" ON "todos" USING btree ("user_id","created_at","id");