mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
⭐ feat(www): Finish up on the onboarding (#202)
## Description This is an attempt to finish up on the onboarding and creating a team ## Type of Change - [ ] Bug fix (non-breaking change) - [x] New feature (non-breaking change) - [ ] Breaking change (fix or feature that changes existing functionality) - [ ] Documentation update - [ ] Other (please describe): ## Checklist - [ ] I have updated relevant documentation - [x] My code follows the project's coding style - [x] My changes generate no new warnings/errors
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
import { teamID } from "./drizzle/types";
|
|
||||||
import { prefixes } from "./utils";
|
import { prefixes } from "./utils";
|
||||||
export module Examples {
|
export module Examples {
|
||||||
export const Id = (prefix: keyof typeof prefixes) =>
|
export const Id = (prefix: keyof typeof prefixes) =>
|
||||||
@@ -26,4 +25,9 @@ export module Examples {
|
|||||||
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Polar = {
|
||||||
|
teamID: Id("team"),
|
||||||
|
timeSeen: new Date("2025-02-23T13:39:52.249Z"),
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Resource } from "sst";
|
|
||||||
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
|
||||||
|
|
||||||
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
|
|
||||||
|
|
||||||
export module Polar {
|
|
||||||
export const client = polar;
|
|
||||||
}
|
|
||||||
169
packages/core/src/polar/index.ts
Normal file
169
packages/core/src/polar/index.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { fn } from "../utils";
|
||||||
|
import { Resource } from "sst";
|
||||||
|
import { eq, and } from "../drizzle";
|
||||||
|
import { useTeam } from "../actor";
|
||||||
|
import { createEvent } from "../event";
|
||||||
|
import { polarTable, Standing } from "./polar.sql";
|
||||||
|
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
||||||
|
import { useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
|
const polar = new PolarSdk({ accessToken: Resource.PolarSecret.value, server: Resource.App.stage !== "production" ? "sandbox" : "production" });
|
||||||
|
|
||||||
|
export module Polar {
|
||||||
|
export const client = polar;
|
||||||
|
|
||||||
|
export const Info = z.object({
|
||||||
|
teamID: z.string(),
|
||||||
|
customerID: z.string(),
|
||||||
|
subscriptionID: z.string().nullable(),
|
||||||
|
subscriptionItemID: z.string().nullable(),
|
||||||
|
standing: z.enum(Standing),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Info = z.infer<typeof Info>;
|
||||||
|
|
||||||
|
export const Checkout = z.object({
|
||||||
|
annual: z.boolean().optional(),
|
||||||
|
successUrl: z.string(),
|
||||||
|
cancelUrl: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CheckoutSession = z.object({
|
||||||
|
url: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CustomerSubscriptionEventType = [
|
||||||
|
"created",
|
||||||
|
"updated",
|
||||||
|
"deleted",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const Events = {
|
||||||
|
CustomerSubscriptionEvent: createEvent(
|
||||||
|
"polar.customer-subscription-event",
|
||||||
|
z.object({
|
||||||
|
type: z.enum(CustomerSubscriptionEventType),
|
||||||
|
status: z.string(),
|
||||||
|
teamID: z.string().min(1),
|
||||||
|
customerID: z.string().min(1),
|
||||||
|
subscriptionID: z.string().min(1),
|
||||||
|
subscriptionItemID: z.string().min(1),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export function get() {
|
||||||
|
return useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(polarTable)
|
||||||
|
.where(eq(polarTable.teamID, useTeam()))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows.map(serialize).at(0)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fromUserEmail = fn(z.string().min(1), async (email) => {
|
||||||
|
try {
|
||||||
|
const customers = await client.customers.list({ email })
|
||||||
|
|
||||||
|
if (customers.result.items.length === 0) {
|
||||||
|
return await client.customers.create({ email })
|
||||||
|
} else {
|
||||||
|
return customers.result.items[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
//FIXME: This is the issue [Polar.sh/#5147](https://github.com/polarsource/polar/issues/5147)
|
||||||
|
// console.log("error", err)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setCustomerID = fn(Info.shape.customerID, async (customerID) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.insert(polarTable)
|
||||||
|
.values({
|
||||||
|
teamID: useTeam(),
|
||||||
|
customerID,
|
||||||
|
standing: "new",
|
||||||
|
})
|
||||||
|
.execute(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setSubscription = fn(
|
||||||
|
Info.pick({
|
||||||
|
subscriptionID: true,
|
||||||
|
subscriptionItemID: true,
|
||||||
|
}),
|
||||||
|
(input) =>
|
||||||
|
useTransaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.update(polarTable)
|
||||||
|
.set({
|
||||||
|
subscriptionID: input.subscriptionID,
|
||||||
|
subscriptionItemID: input.subscriptionItemID,
|
||||||
|
})
|
||||||
|
.where(eq(polarTable.teamID, useTeam()))
|
||||||
|
.returning()
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows.map(serialize).at(0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const removeSubscription = fn(
|
||||||
|
z.string().min(1),
|
||||||
|
(stripeSubscriptionID) =>
|
||||||
|
useTransaction((tx) =>
|
||||||
|
tx
|
||||||
|
.update(polarTable)
|
||||||
|
.set({
|
||||||
|
subscriptionItemID: null,
|
||||||
|
subscriptionID: null,
|
||||||
|
})
|
||||||
|
.where(and(eq(polarTable.subscriptionID, stripeSubscriptionID)))
|
||||||
|
.execute(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setStanding = fn(
|
||||||
|
Info.pick({
|
||||||
|
subscriptionID: true,
|
||||||
|
standing: true,
|
||||||
|
}),
|
||||||
|
(input) =>
|
||||||
|
useTransaction((tx) =>
|
||||||
|
tx
|
||||||
|
.update(polarTable)
|
||||||
|
.set({ standing: input.standing })
|
||||||
|
.where(and(eq(polarTable.subscriptionID, input.subscriptionID!)))
|
||||||
|
.execute(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fromCustomerID = fn(Info.shape.customerID, (customerID) =>
|
||||||
|
useTransaction((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(polarTable)
|
||||||
|
.where(and(eq(polarTable.customerID, customerID)))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows.map(serialize).at(0)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
function serialize(
|
||||||
|
input: typeof polarTable.$inferSelect,
|
||||||
|
): z.infer<typeof Info> {
|
||||||
|
return {
|
||||||
|
teamID: input.teamID,
|
||||||
|
customerID: input.customerID,
|
||||||
|
subscriptionID: input.subscriptionID,
|
||||||
|
subscriptionItemID: input.subscriptionItemID,
|
||||||
|
standing: input.standing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/core/src/polar/polar.sql.ts
Normal file
22
packages/core/src/polar/polar.sql.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { timestamps, teamID } from "../drizzle/types";
|
||||||
|
import { teamIndexes, teamTable } from "../team/team.sql";
|
||||||
|
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const Standing = ["new", "good", "overdue"] as const;
|
||||||
|
|
||||||
|
export const polarTable = pgTable(
|
||||||
|
"polar",
|
||||||
|
{
|
||||||
|
teamID: teamID.teamID.primaryKey().references(() => teamTable.id),
|
||||||
|
...timestamps,
|
||||||
|
customerID: varchar("customer_id", { length: 255 }).notNull(),
|
||||||
|
subscriptionID: varchar("subscription_id", { length: 255 }),
|
||||||
|
subscriptionItemID: varchar("subscription_item_id", {
|
||||||
|
length: 255,
|
||||||
|
}),
|
||||||
|
standing: text("standing", { enum: Standing }).notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
...teamIndexes(table),
|
||||||
|
})
|
||||||
|
)
|
||||||
@@ -3,13 +3,13 @@ import { Resource } from "sst";
|
|||||||
import { bus } from "sst/aws/bus";
|
import { bus } from "sst/aws/bus";
|
||||||
import { Common } from "../common";
|
import { Common } from "../common";
|
||||||
import { createID, fn } from "../utils";
|
import { createID, fn } from "../utils";
|
||||||
import { VisibleError } from "../error";
|
|
||||||
import { Examples } from "../examples";
|
import { Examples } from "../examples";
|
||||||
import { teamTable } from "./team.sql";
|
import { teamTable } from "./team.sql";
|
||||||
import { createEvent } from "../event";
|
import { createEvent } from "../event";
|
||||||
import { assertActor } from "../actor";
|
import { assertActor, withActor } from "../actor";
|
||||||
import { and, eq, sql } from "../drizzle";
|
import { and, eq, sql } from "../drizzle";
|
||||||
import { memberTable } from "../member/member.sql";
|
import { memberTable } from "../member/member.sql";
|
||||||
|
import { HTTPException } from 'hono/http-exception';
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
|
|
||||||
export module Team {
|
export module Team {
|
||||||
@@ -45,11 +45,11 @@ export module Team {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class WorkspaceExistsError extends VisibleError {
|
export class TeamExistsError extends HTTPException {
|
||||||
constructor(slug: string) {
|
constructor(slug: string) {
|
||||||
super(
|
super(
|
||||||
"team.slug_exists",
|
400,
|
||||||
`there is already a workspace named "${slug}"`,
|
{ message: `There is already a team named "${slug}"`, }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,15 +65,16 @@ export module Team {
|
|||||||
slug: input.slug,
|
slug: input.slug,
|
||||||
name: input.name
|
name: input.name
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing({ target: teamTable.slug })
|
||||||
.returning({ insertedID: teamTable.id })
|
|
||||||
|
|
||||||
if (result.length === 0) throw new WorkspaceExistsError(input.slug);
|
if (!result.rowCount) throw new TeamExistsError(input.slug);
|
||||||
|
|
||||||
await afterTx(() =>
|
await afterTx(() =>
|
||||||
|
withActor({ type: "system", properties: { teamID: id } }, () =>
|
||||||
bus.publish(Resource.Bus, Events.Created, {
|
bus.publish(Resource.Bus, Events.Created, {
|
||||||
teamID: id,
|
teamID: id,
|
||||||
}),
|
})
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return id;
|
return id;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { Polar } from "../polar";
|
||||||
|
import { Team } from "../team";
|
||||||
import { bus } from "sst/aws/bus";
|
import { bus } from "sst/aws/bus";
|
||||||
import { Common } from "../common";
|
import { Common } from "../common";
|
||||||
import { createID, fn } from "../utils";
|
import { createID, fn } from "../utils";
|
||||||
@@ -11,7 +13,6 @@ import { assertActor, withActor } from "../actor";
|
|||||||
import { memberTable } from "../member/member.sql";
|
import { memberTable } from "../member/member.sql";
|
||||||
import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
|
import { and, eq, isNull, asc, getTableColumns, sql } from "../drizzle";
|
||||||
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
import { afterTx, createTransaction, useTransaction } from "../drizzle/transaction";
|
||||||
import { Team } from "../team";
|
|
||||||
|
|
||||||
|
|
||||||
export module User {
|
export module User {
|
||||||
@@ -106,12 +107,8 @@ export module User {
|
|||||||
|
|
||||||
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
|
//FIXME: Do this much later, as Polar.sh has so many inconsistencies for fuck's sake
|
||||||
|
|
||||||
// const customer = await Polar.client.customers.create({
|
const customer = await Polar.fromUserEmail(input.email)
|
||||||
// email: input.email,
|
console.log("customer", customer)
|
||||||
// metadata: {
|
|
||||||
// userID,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
const name = sanitizeUsername(input.name);
|
const name = sanitizeUsername(input.name);
|
||||||
|
|
||||||
@@ -131,6 +128,7 @@ export module User {
|
|||||||
avatarUrl: input.avatarUrl,
|
avatarUrl: input.avatarUrl,
|
||||||
email: input.email,
|
email: input.email,
|
||||||
discriminator: Number(discriminator),
|
discriminator: Number(discriminator),
|
||||||
|
polarCustomerID: customer?.id
|
||||||
})
|
})
|
||||||
await afterTx(() =>
|
await afterTx(() =>
|
||||||
withActor({
|
withActor({
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ export module AccountApi {
|
|||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const actor = assertActor("user");
|
const actor = assertActor("user");
|
||||||
const currentUser = await User.fromID(actor.properties.userID)
|
const [currentUser, teams] = await Promise.all([User.fromID(actor.properties.userID), User.teams()])
|
||||||
if (!currentUser) return c.json({ error: "This account does not exist, it may have been deleted" }, 404)
|
|
||||||
|
if (!currentUser) return c.json({ error: "This account does not exist; it may have been deleted" }, 404)
|
||||||
|
|
||||||
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
|
const { id, email, name, polarCustomerID, avatarUrl, discriminator } = currentUser
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
@@ -51,10 +53,10 @@ export module AccountApi {
|
|||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
|
teams,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
discriminator,
|
discriminator,
|
||||||
polarCustomerID,
|
polarCustomerID,
|
||||||
teams: await User.teams(),
|
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
media="(prefers-color-scheme: light)"
|
media="(prefers-color-scheme: light)"
|
||||||
content="hsla(0,0%,98%)"
|
content="#f5f5f5"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="theme-color"
|
name="theme-color"
|
||||||
media="(prefers-color-scheme: dark)"
|
media="(prefers-color-scheme: dark)"
|
||||||
content="hsla(0,0%,0%)"
|
content="#171717"
|
||||||
/>
|
/>
|
||||||
<link
|
<link
|
||||||
rel="apple-touch-icon"
|
rel="apple-touch-icon"
|
||||||
|
|||||||
@@ -19,14 +19,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/geist-mono": "^5.0.1",
|
"@fontsource-variable/geist-mono": "^5.0.1",
|
||||||
"@nestri/core": "*",
|
|
||||||
"@fontsource-variable/mona-sans": "^5.0.1",
|
"@fontsource-variable/mona-sans": "^5.0.1",
|
||||||
"@fontsource/geist-sans": "^5.1.0",
|
"@fontsource/geist-sans": "^5.1.0",
|
||||||
"@macaron-css/core": "^1.5.2",
|
"@macaron-css/core": "^1.5.2",
|
||||||
"@macaron-css/solid": "^1.5.3",
|
"@macaron-css/solid": "^1.5.3",
|
||||||
|
"@modular-forms/solid": "^0.25.1",
|
||||||
|
"@nestri/core": "*",
|
||||||
"@solid-primitives/storage": "^4.3.1",
|
"@solid-primitives/storage": "^4.3.1",
|
||||||
"@solidjs/router": "^0.15.3",
|
"@solidjs/router": "^0.15.3",
|
||||||
"modern-normalize": "^3.0.1",
|
"modern-normalize": "^3.0.1",
|
||||||
"solid-js": "^1.9.5"
|
"solid-js": "^1.9.5",
|
||||||
|
"valibot": "^1.0.0-rc.3",
|
||||||
|
"zod": "^3.24.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,9 @@ import '@fontsource/geist-sans/600.css';
|
|||||||
import '@fontsource/geist-sans/700.css';
|
import '@fontsource/geist-sans/700.css';
|
||||||
import '@fontsource/geist-sans/800.css';
|
import '@fontsource/geist-sans/800.css';
|
||||||
import '@fontsource/geist-sans/900.css';
|
import '@fontsource/geist-sans/900.css';
|
||||||
import { TeamCreate } from './pages/new';
|
|
||||||
import { styled } from "@macaron-css/solid";
|
import { styled } from "@macaron-css/solid";
|
||||||
import { useStorage } from './providers/account';
|
import { useStorage } from './providers/account';
|
||||||
|
import { CreateTeamComponent } from './pages/new';
|
||||||
import { darkClass, lightClass, theme } from './ui/theme';
|
import { darkClass, lightClass, theme } from './ui/theme';
|
||||||
import { AuthProvider, useAuth } from './providers/auth';
|
import { AuthProvider, useAuth } from './providers/auth';
|
||||||
import { Navigate, Route, Router } from "@solidjs/router";
|
import { Navigate, Route, Router } from "@solidjs/router";
|
||||||
@@ -34,10 +34,10 @@ globalStyle("html", {
|
|||||||
// Hardcode colors
|
// Hardcode colors
|
||||||
"@media": {
|
"@media": {
|
||||||
"(prefers-color-scheme: light)": {
|
"(prefers-color-scheme: light)": {
|
||||||
backgroundColor: "hsla(0,0%,98%)",
|
backgroundColor: "#f5f5f5",
|
||||||
},
|
},
|
||||||
"(prefers-color-scheme: dark)": {
|
"(prefers-color-scheme: dark)": {
|
||||||
backgroundColor: "hsla(0,0%,0%)",
|
backgroundColor: "#1e1e1e",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -87,35 +87,13 @@ export const App: Component = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
component={(props) => (
|
component={(props) => (
|
||||||
<AuthProvider>
|
// <AuthProvider>
|
||||||
{props.children}
|
|
||||||
</AuthProvider>
|
|
||||||
// <CommandBar>
|
|
||||||
// <ReplicacheStatusProvider>
|
|
||||||
// <DummyProvider>
|
|
||||||
// <DummyConfigProvider>
|
|
||||||
// <FlagsProvider>
|
|
||||||
// <RealtimeProvider />
|
|
||||||
// <LocalProvider>
|
|
||||||
// <LocalLogsProvider>
|
|
||||||
// <GlobalCommands />
|
|
||||||
// {props.children}
|
// {props.children}
|
||||||
// </LocalLogsProvider>
|
props.children
|
||||||
// </LocalProvider>
|
|
||||||
// </FlagsProvider>
|
|
||||||
// </DummyConfigProvider>
|
|
||||||
// </DummyProvider>
|
|
||||||
// </ReplicacheStatusProvider>
|
|
||||||
// </AuthProvider>
|
// </AuthProvider>
|
||||||
// </CommandBar>
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* <Route path="local" component={Local} />
|
<Route path="new" component={CreateTeamComponent} />
|
||||||
<Route path="debug" component={DebugRoute} />
|
|
||||||
<Route path="design" component={Design} />
|
|
||||||
<Route path="workspace" component={WorkspaceCreate} />
|
|
||||||
<Route path=":workspaceSlug">{WorkspaceRoute}</Route> */}
|
|
||||||
<Route path="new" component={TeamCreate} />
|
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
component={() => {
|
component={() => {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export function DefaultState() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
We are logging you in
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,124 @@
|
|||||||
|
import * as v from "valibot"
|
||||||
|
import { styled } from "@macaron-css/solid";
|
||||||
import { Text } from "@nestri/www/ui/text";
|
import { Text } from "@nestri/www/ui/text";
|
||||||
|
import { utility } from "@nestri/www/ui/utility";
|
||||||
|
import { theme } from "@nestri/www/ui/theme";
|
||||||
|
import { FormField, Input, Select } from "@nestri/www/ui/form";
|
||||||
import { Container, FullScreen } from "@nestri/www/ui/layout";
|
import { Container, FullScreen } from "@nestri/www/ui/layout";
|
||||||
|
import { createForm, required, email, valiForm } from "@modular-forms/solid";
|
||||||
|
import { Button } from "@nestri/www/ui";
|
||||||
|
|
||||||
|
// const nameRegex = /^[a-z]+$/
|
||||||
|
|
||||||
|
const FieldList = styled("div", {
|
||||||
|
base: {
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 380,
|
||||||
|
...utility.stack(5),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hr = styled("hr", {
|
||||||
|
base: {
|
||||||
|
border: 0,
|
||||||
|
backgroundColor: theme.color.gray.d400,
|
||||||
|
width: "100%",
|
||||||
|
height: 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Plan = {
|
||||||
|
Pro: 'Pro',
|
||||||
|
Basic: 'Basic',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const schema = v.object({
|
||||||
|
plan: v.pipe(
|
||||||
|
v.enum(Plan),
|
||||||
|
),
|
||||||
|
display_name: v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.maxLength(32, 'Please use 32 characters at maximum.'),
|
||||||
|
),
|
||||||
|
slug: v.pipe(
|
||||||
|
v.string(),
|
||||||
|
v.minLength(2, 'Please use 2 characters at minimum.'),
|
||||||
|
// v.regex(nameRegex, "Use only small letters, no numbers or special characters"),
|
||||||
|
v.maxLength(48, 'Please use 48 characters at maximum.'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function CreateTeamComponent() {
|
||||||
|
const [form, { Form, Field }] = createForm({
|
||||||
|
validate: valiForm(schema),
|
||||||
|
});
|
||||||
|
|
||||||
export function TeamCreate() {
|
|
||||||
return (
|
return (
|
||||||
<FullScreen>
|
<FullScreen>
|
||||||
<Container flow="column" >
|
<Container horizontal="center" style={{ width: "100%", padding: "1rem", }} space="1" >
|
||||||
<Text align="center" spacing="lg" size="4xl" weight="semibold">
|
<Container style={{ "width": "100%","max-width": "380px" }} horizontal="start" space="3" >
|
||||||
Your first deploy is just a sign-up away.
|
<Text font="heading" spacing="none" size="3xl" weight="semibold">
|
||||||
|
Create a Team
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={{ color: theme.color.gray.d900 }} size="sm">
|
||||||
|
Choose something that your teammates will recognize
|
||||||
|
</Text>
|
||||||
|
<Hr />
|
||||||
</Container>
|
</Container>
|
||||||
|
<Form style={{ width: "100%", "max-width": "380px" }}>
|
||||||
|
<FieldList>
|
||||||
|
<Field type="string" name="slug">
|
||||||
|
{(field, props) => (
|
||||||
|
<FormField
|
||||||
|
label="Team Name"
|
||||||
|
hint={
|
||||||
|
field.error
|
||||||
|
&& field.error
|
||||||
|
// : "Needs to be lowercase, unique, and URL friendly."
|
||||||
|
}
|
||||||
|
color={field.error ? "danger" : "primary"}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
autofocus
|
||||||
|
placeholder="Jane Doe's Team"
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field type="string" name="plan">
|
||||||
|
{(field, props) => (
|
||||||
|
<FormField
|
||||||
|
label="Plan Type"
|
||||||
|
hint={
|
||||||
|
field.error
|
||||||
|
&& field.error
|
||||||
|
// : "Needs to be lowercase, unique, and URL friendly."
|
||||||
|
}
|
||||||
|
color={field.error ? "danger" : "primary"}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...props}
|
||||||
|
value={field.value}
|
||||||
|
badges={[
|
||||||
|
{ label: "Basic", color: "gray" },
|
||||||
|
{ label: "Pro", color: "blue" }
|
||||||
|
]}
|
||||||
|
options={[
|
||||||
|
{ label: "I'll be playing all by myself", value: 'Basic' },
|
||||||
|
{ label: "I'll be playing with friends and family", value: 'Pro' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Button color="brand">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</FieldList>
|
||||||
|
</Form>
|
||||||
|
</Container>
|
||||||
|
|
||||||
</FullScreen>
|
</FullScreen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@ interface Storage {
|
|||||||
current?: string;
|
current?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Fix bug where authenticator deletes auth state for no reason
|
||||||
|
|
||||||
export const client = createClient({
|
export const client = createClient({
|
||||||
issuer: import.meta.env.VITE_AUTH_URL,
|
issuer: import.meta.env.VITE_AUTH_URL,
|
||||||
clientID: "web",
|
clientID: "web",
|
||||||
@@ -83,6 +85,7 @@ export const { use: useAuth, provider: AuthProvider } =
|
|||||||
access: account.access,
|
access: account.access,
|
||||||
})
|
})
|
||||||
if (result.err) {
|
if (result.err) {
|
||||||
|
console.log("error", result.err)
|
||||||
if ("id" in account)
|
if ("id" in account)
|
||||||
setStore(produce((state) => {
|
setStore(produce((state) => {
|
||||||
delete state.accounts[account.id];
|
delete state.accounts[account.id];
|
||||||
@@ -98,7 +101,7 @@ export const { use: useAuth, provider: AuthProvider } =
|
|||||||
authorization: `Bearer ${tokens.access}`,
|
authorization: `Bearer ${tokens.access}`,
|
||||||
},
|
},
|
||||||
}).then(async (response) => {
|
}).then(async (response) => {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -115,6 +118,7 @@ export const { use: useAuth, provider: AuthProvider } =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok)
|
if (!response.ok)
|
||||||
|
console.log("error from account", response.json())
|
||||||
setStore(
|
setStore(
|
||||||
produce((state) => {
|
produce((state) => {
|
||||||
delete state.accounts[account.id];
|
delete state.accounts[account.id];
|
||||||
34
packages/www/src/ui/button.ts
Normal file
34
packages/www/src/ui/button.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { theme } from "./theme";
|
||||||
|
import { styled } from "@macaron-css/solid";
|
||||||
|
|
||||||
|
export const Button = styled("button", {
|
||||||
|
base: {
|
||||||
|
borderRadius: 6,
|
||||||
|
border: "1px solid transparent",
|
||||||
|
padding: `${theme.space[2]} ${theme.space[4]}`,
|
||||||
|
fontWeight: 500,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
lineHeight: "normal",
|
||||||
|
fontFamily: theme.font.family.heading,
|
||||||
|
textAlign: "center",
|
||||||
|
transitionDelay: "0s, 0s",
|
||||||
|
transitionDuration: "0.2s, 0.2s",
|
||||||
|
transitionProperty: "background-color, border",
|
||||||
|
transitionTimingFunction: "ease-out, ease-out",
|
||||||
|
display: "inline-flex",
|
||||||
|
gap: theme.space[1.5],
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
":disabled": {
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
brand: {
|
||||||
|
backgroundColor: theme.color.brand,
|
||||||
|
color: "#FFF",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
310
packages/www/src/ui/form.tsx
Normal file
310
packages/www/src/ui/form.tsx
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import { theme } from "./theme";
|
||||||
|
import { styled } from "@macaron-css/solid"
|
||||||
|
import { CSSProperties } from "@macaron-css/core";
|
||||||
|
import { ComponentProps, createMemo, For, JSX, Show, splitProps } from "solid-js";
|
||||||
|
import { Container } from "./layout";
|
||||||
|
import { utility } from "./utility";
|
||||||
|
|
||||||
|
export const inputStyles: CSSProperties = {
|
||||||
|
lineHeight: theme.font.lineHeight,
|
||||||
|
appearance: "none",
|
||||||
|
fontSize: theme.font.size.sm,
|
||||||
|
borderRadius: theme.borderRadius,
|
||||||
|
padding: `0 ${theme.space[3]}`,
|
||||||
|
height: theme.input.size.base,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.color.gray.d400,
|
||||||
|
color: theme.color.d1000.gray,
|
||||||
|
backgroundColor: theme.color.background.d100,
|
||||||
|
// transition: `box-shadow ${theme.colorFadeDuration}`,
|
||||||
|
// boxShadow: `
|
||||||
|
// 0 0 0 1px inset ${theme.color.input.border},
|
||||||
|
// ${theme.color.input.shadow}
|
||||||
|
// `,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputDisabledStyles: CSSProperties = {
|
||||||
|
opacity: 0.5,
|
||||||
|
backgroundColor: theme.color.background.d200,
|
||||||
|
color: theme.color.gray.d400,
|
||||||
|
cursor: "default",
|
||||||
|
// boxShadow: `0 0 0 1px inset ${theme.color.input.border}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputFocusStyles: CSSProperties = {
|
||||||
|
outlineOffset: 3,
|
||||||
|
outline: `${theme.color.gray.d600} solid 2px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputDangerTextStyles: CSSProperties = {
|
||||||
|
color: theme.color.red.d700,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const inputDangerFocusStyles: CSSProperties = {
|
||||||
|
...inputDangerTextStyles,
|
||||||
|
outlineColor: theme.color.red.d700,
|
||||||
|
// boxShadow: `
|
||||||
|
// 0 0 1px 1px inset hsla(${theme.color.red.l2}, 100%),
|
||||||
|
// ${theme.color.input.shadow}
|
||||||
|
// `,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Root = styled("label", {
|
||||||
|
base: {
|
||||||
|
...utility.stack(2),
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
primary: {
|
||||||
|
color: theme.color.gray.d900
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
color: theme.color.red.d900
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormFieldProps = ComponentProps<typeof Root> & {
|
||||||
|
hint?: JSX.Element;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Input = styled("input", {
|
||||||
|
base: {
|
||||||
|
...inputStyles,
|
||||||
|
":focus": {
|
||||||
|
...inputFocusStyles,
|
||||||
|
},
|
||||||
|
":disabled": {
|
||||||
|
...inputDisabledStyles,
|
||||||
|
},
|
||||||
|
"::placeholder": {
|
||||||
|
color: theme.color.gray.d800
|
||||||
|
}
|
||||||
|
// selectors: {
|
||||||
|
// [`${Root.selector({ color: "danger" })} &`]: {
|
||||||
|
// ...inputDangerFocusStyles,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
primary: {},
|
||||||
|
danger: {
|
||||||
|
...inputDangerFocusStyles,
|
||||||
|
":focus": {
|
||||||
|
...inputDangerFocusStyles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
sm: {
|
||||||
|
height: theme.input.size.sm,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const InputRadio = styled("input", {
|
||||||
|
base: {
|
||||||
|
padding: 0,
|
||||||
|
// borderRadius: 0,
|
||||||
|
WebkitAppearance: "none",
|
||||||
|
appearance: "none",
|
||||||
|
/* For iOS < 15 to remove gradient background */
|
||||||
|
backgroundColor: theme.color.background.d100,
|
||||||
|
/* Not removed via appearance */
|
||||||
|
margin: 0,
|
||||||
|
font: "inherit",
|
||||||
|
color: "currentColor",
|
||||||
|
width: "1.15em",
|
||||||
|
height: "1.15em",
|
||||||
|
border: "0.15em solid currentColor",
|
||||||
|
borderRadius: "50%",
|
||||||
|
transform: "translateY(-0.075em)",
|
||||||
|
display: "grid",
|
||||||
|
position: "relative",
|
||||||
|
placeContent: "center",
|
||||||
|
":before": {
|
||||||
|
content: "",
|
||||||
|
width: "0.68em",
|
||||||
|
height: "0.68em",
|
||||||
|
borderRadius: "50%",
|
||||||
|
transform: " scale(0)",
|
||||||
|
transition: "120ms transform ease-in-out",
|
||||||
|
boxShadow: `inset 1em 1em ${theme.color.blue.d700}`
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
"&:checked::before": {
|
||||||
|
transform: "scale(1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const Label = styled("p", {
|
||||||
|
base: {
|
||||||
|
fontWeight: 500,
|
||||||
|
letterSpacing: -0.1,
|
||||||
|
fontSize: theme.font.size.mono_sm,
|
||||||
|
textTransform: "capitalize",
|
||||||
|
fontFamily: theme.font.family.heading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const InputLabel = styled("label", {
|
||||||
|
base: {
|
||||||
|
letterSpacing: -0.1,
|
||||||
|
fontSize: theme.font.size.sm,
|
||||||
|
lineHeight: theme.font.lineHeight,
|
||||||
|
height: theme.input.size.base,
|
||||||
|
appearance: "none",
|
||||||
|
padding: `0 ${theme.space[3]}`,
|
||||||
|
borderWidth: 0,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderColor: theme.color.gray.d400,
|
||||||
|
color: theme.color.gray.d800,
|
||||||
|
backgroundColor: theme.color.background.d100,
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
gap: "1em",
|
||||||
|
":focus-within": {
|
||||||
|
color: theme.color.d1000.gray
|
||||||
|
},
|
||||||
|
":first-child": {
|
||||||
|
borderTopRightRadius: theme.borderRadius,
|
||||||
|
borderTopLeftRadius: theme.borderRadius,
|
||||||
|
},
|
||||||
|
":last-child": {
|
||||||
|
borderBottomWidth: 0,
|
||||||
|
borderBottomRightRadius: theme.borderRadius,
|
||||||
|
borderBottomLeftRadius: theme.borderRadius,
|
||||||
|
},
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: theme.color.background.d200,
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
"&:has(input:checked)": {
|
||||||
|
color: theme.color.d1000.gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Hint = styled("p", {
|
||||||
|
base: {
|
||||||
|
fontSize: theme.font.size.sm,
|
||||||
|
lineHeight: theme.font.lineHeight,
|
||||||
|
color: theme.color.gray.d800,
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
primary: {},
|
||||||
|
danger: {
|
||||||
|
color: theme.color.red.d700,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: "primary",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function FormField(props: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<Root {...props}>
|
||||||
|
<Container space="2">
|
||||||
|
<Show when={props.label}>
|
||||||
|
<Label color={props.color}>{props.label}</Label>
|
||||||
|
</Show>
|
||||||
|
{props.children}
|
||||||
|
</Container>
|
||||||
|
<Show when={props.hint}>
|
||||||
|
<Hint color={props.color}>{props.hint!}</Hint>
|
||||||
|
</Show>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectProps = {
|
||||||
|
ref: (element: HTMLInputElement) => void;
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
onInput: JSX.EventHandler<HTMLInputElement, InputEvent>;
|
||||||
|
onChange: JSX.EventHandler<HTMLInputElement, Event>;
|
||||||
|
onBlur: JSX.EventHandler<HTMLInputElement, FocusEvent>;
|
||||||
|
options: { label: string; value: string }[];
|
||||||
|
badges?: { label: string; color: keyof typeof theme.color.d1000 }[];
|
||||||
|
required?: boolean;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputRadioContainer = styled("div", {
|
||||||
|
base: {
|
||||||
|
...inputStyles,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "auto",
|
||||||
|
position: "relative",
|
||||||
|
padding: 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Badge = styled("div", {
|
||||||
|
base: {
|
||||||
|
color: "#FFF",
|
||||||
|
marginLeft: "auto",
|
||||||
|
borderRadius: 9999,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
padding: "0 6px",
|
||||||
|
fontSize: theme.font.size.xs
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
export function Select(props: SelectProps) {
|
||||||
|
// Split select element props
|
||||||
|
const [, inputProps] = splitProps(props, [
|
||||||
|
'class',
|
||||||
|
'value',
|
||||||
|
'options',
|
||||||
|
'badges',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputRadioContainer>
|
||||||
|
<For each={props.options}>
|
||||||
|
{({ label, value }, key) => (
|
||||||
|
<InputLabel for={label}>
|
||||||
|
<InputRadio
|
||||||
|
{...inputProps}
|
||||||
|
type="radio"
|
||||||
|
value={value}
|
||||||
|
id={label}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
<Show when={props.badges}>
|
||||||
|
{props.badges &&
|
||||||
|
<Badge style={{"background-color": theme.color[props.badges[key()].color].d700 }}>
|
||||||
|
{props.badges[key()].label}
|
||||||
|
</Badge>
|
||||||
|
}
|
||||||
|
</Show>
|
||||||
|
</InputLabel>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
</InputRadioContainer >
|
||||||
|
);
|
||||||
|
}
|
||||||
6
packages/www/src/ui/index.ts
Normal file
6
packages/www/src/ui/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./form"
|
||||||
|
export * from "./layout"
|
||||||
|
export * from "./text"
|
||||||
|
export * from "./theme"
|
||||||
|
export * from "./utility"
|
||||||
|
export * from "./button"
|
||||||
@@ -4,7 +4,6 @@ import { styled } from "@macaron-css/solid";
|
|||||||
export const FullScreen = styled("div", {
|
export const FullScreen = styled("div", {
|
||||||
base: {
|
base: {
|
||||||
inset: 0,
|
inset: 0,
|
||||||
zIndex: 0,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -21,28 +20,97 @@ export const FullScreen = styled("div", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// export const Container = styled("div", {
|
||||||
|
// base: {
|
||||||
|
// backgroundColor: theme.color.background.d100,
|
||||||
|
// borderColor: theme.color.gray.d400,
|
||||||
|
// padding: "64px 80px 48px",
|
||||||
|
// justifyContent: "center",
|
||||||
|
// borderStyle: "solid",
|
||||||
|
// position: "relative",
|
||||||
|
// borderRadius: 12,
|
||||||
|
// alignItems: "center",
|
||||||
|
// maxWidth: 550,
|
||||||
|
// borderWidth: 1,
|
||||||
|
// display: "flex",
|
||||||
|
// },
|
||||||
|
// variants: {
|
||||||
|
// flow: {
|
||||||
|
// column: {
|
||||||
|
// flexDirection: "column"
|
||||||
|
// },
|
||||||
|
// row: {
|
||||||
|
// flexDirection: "row"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
export const Container = styled("div", {
|
export const Container = styled("div", {
|
||||||
base: {
|
base: {
|
||||||
backgroundColor: theme.color.background.d100,
|
|
||||||
borderColor: theme.color.gray.d400,
|
|
||||||
padding: "64px 80px 48px",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderStyle: "solid",
|
|
||||||
position: "relative",
|
|
||||||
borderRadius: 12,
|
|
||||||
alignItems: "center",
|
|
||||||
maxWidth: 550,
|
|
||||||
borderWidth: 1,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
flow: {
|
space: (() => {
|
||||||
column: {
|
const result = {} as Record<`${keyof (typeof theme)["space"]}`, any>;
|
||||||
flexDirection: "column"
|
for (const key in theme.space) {
|
||||||
|
const value = theme.space[key as keyof typeof theme.space];
|
||||||
|
result[key as keyof typeof theme.space] = {
|
||||||
|
gap: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})(),
|
||||||
|
rounded: (() => {
|
||||||
|
const result = {} as Record<`${keyof (typeof theme)["space"]}`, any>;
|
||||||
|
for (const key in theme.space) {
|
||||||
|
const value = theme.space[key as keyof typeof theme.space];
|
||||||
|
result[key as keyof typeof theme.space] = {
|
||||||
|
borderRadius: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})(),
|
||||||
|
highlighted: {
|
||||||
|
true: {
|
||||||
|
borderColor: theme.color.gray.d400,
|
||||||
|
backgroundColor: theme.color.background.d100,
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderWidth: 1,
|
||||||
|
padding: "64px 80px 48px",
|
||||||
|
maxWidth: 550,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
row: {
|
flex: {
|
||||||
flexDirection: "row"
|
true: {
|
||||||
}
|
flex: "1 1 auto",
|
||||||
}
|
},
|
||||||
}
|
false: {
|
||||||
})
|
flex: "0 0 auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
horizontal: {
|
||||||
|
center: {
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
alignItems: "flex-end",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
center: {
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
start: {
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,9 +4,9 @@ import { utility } from "./utility";
|
|||||||
import { CSSProperties } from "@macaron-css/core";
|
import { CSSProperties } from "@macaron-css/core";
|
||||||
|
|
||||||
export const Text = styled("span", {
|
export const Text = styled("span", {
|
||||||
base: {
|
// base: {
|
||||||
textWrap: "balance"
|
// textWrap: "balance"
|
||||||
},
|
// },
|
||||||
variants: {
|
variants: {
|
||||||
leading: {
|
leading: {
|
||||||
base: {
|
base: {
|
||||||
@@ -122,6 +122,15 @@ export const Text = styled("span", {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
})(),
|
})(),
|
||||||
|
font: (() => {
|
||||||
|
const result = {} as Record<`${keyof typeof theme.font.family}`, any>;
|
||||||
|
for (const [key, value] of Object.entries(theme.font.family)) {
|
||||||
|
result[key as keyof typeof theme.font.family] = {
|
||||||
|
fontFamily: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})(),
|
||||||
color: (() => {
|
color: (() => {
|
||||||
const record = {} as Record<keyof typeof theme.color.text, CSSProperties>;
|
const record = {} as Record<keyof typeof theme.color.text, CSSProperties>;
|
||||||
for (const [key, _value] of Object.entries(theme.color.text)) {
|
for (const [key, _value] of Object.entries(theme.color.text)) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createTheme } from "@macaron-css/core";
|
|||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
colorFadeDuration: "0.15s",
|
colorFadeDuration: "0.15s",
|
||||||
borderRadius: "4px",
|
borderRadius: "6px",
|
||||||
textBoldWeight: "600",
|
textBoldWeight: "600",
|
||||||
iconOpacity: "0.85",
|
iconOpacity: "0.85",
|
||||||
modalWidth: {
|
modalWidth: {
|
||||||
@@ -16,6 +16,13 @@ const constants = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formInput = {
|
||||||
|
size: {
|
||||||
|
base: "40px",
|
||||||
|
sm: "32px",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const space = {
|
const space = {
|
||||||
px: "1px",
|
px: "1px",
|
||||||
0: "0px",
|
0: "0px",
|
||||||
@@ -96,7 +103,7 @@ const light = (() => {
|
|||||||
d100: 'hsla(0,0%,95%)',
|
d100: 'hsla(0,0%,95%)',
|
||||||
d200: 'hsla(0,0%,92%)',
|
d200: 'hsla(0,0%,92%)',
|
||||||
d300: 'hsla(0,0%,90%)',
|
d300: 'hsla(0,0%,90%)',
|
||||||
d400: 'hsla(0,0%,92%)',
|
d400: 'hsla(0,0%,82%)',
|
||||||
d500: 'hsla(0,0%,79%)',
|
d500: 'hsla(0,0%,79%)',
|
||||||
d600: 'hsla(0,0%,66%)',
|
d600: 'hsla(0,0%,66%)',
|
||||||
d700: 'hsla(0,0%,56%)',
|
d700: 'hsla(0,0%,56%)',
|
||||||
@@ -206,12 +213,13 @@ const light = (() => {
|
|||||||
teal: "hsla(171,80%,13%)",
|
teal: "hsla(171,80%,13%)",
|
||||||
purple: "hsla(276,100%,15)",
|
purple: "hsla(276,100%,15)",
|
||||||
pink: "hsla(333,74%,15%)",
|
pink: "hsla(333,74%,15%)",
|
||||||
grayAlpha: " hsla(0,0%,0%,0.91)"
|
grayAlpha: " hsla(0,0%,0%,0.91)",
|
||||||
}
|
}
|
||||||
|
const brand = "#FF4F01"
|
||||||
|
|
||||||
const background = {
|
const background = {
|
||||||
d100: 'hsla(0,0%,100%)',
|
d100: '#f5f5f5',
|
||||||
d200: 'hsla(0,0%,98%)'
|
d200: 'oklch(from #f5f5f5 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)'
|
||||||
};
|
};
|
||||||
|
|
||||||
const contrastFg = '#ffffff';
|
const contrastFg = '#ffffff';
|
||||||
@@ -248,6 +256,7 @@ const light = (() => {
|
|||||||
focusBorder,
|
focusBorder,
|
||||||
focusColor,
|
focusColor,
|
||||||
d1000,
|
d1000,
|
||||||
|
brand,
|
||||||
text
|
text
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
@@ -365,13 +374,16 @@ const dark = (() => {
|
|||||||
teal: "hsla(166,71%,93%)",
|
teal: "hsla(166,71%,93%)",
|
||||||
purple: "hsla(281,73%,96%)",
|
purple: "hsla(281,73%,96%)",
|
||||||
pink: "hsla( 333,90%,96%)",
|
pink: "hsla( 333,90%,96%)",
|
||||||
grayAlpha: "hsla(0,0%,100%,0.92)"
|
grayAlpha: "hsla(0,0%,100%,0.92)",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const brand = "#FF4F01"
|
||||||
|
|
||||||
const background = {
|
const background = {
|
||||||
d100: 'hsla(0,0%,4%)',
|
d200: '#171717',
|
||||||
d200: 'hsla(0,0%,0%)'
|
d100: "oklch(from #171717 calc(l + (-0.06 * clamp(0, calc((l - 0.714) * 1000), 1) + 0.03)) c h)"
|
||||||
};
|
};
|
||||||
|
|
||||||
const contrastFg = '#ffffff';
|
const contrastFg = '#ffffff';
|
||||||
const focusBorder = `0 0 0 1px ${grayAlpha.d600}, 0px 0px 0px 4px rgba(255,255,255,0.24)`;
|
const focusBorder = `0 0 0 1px ${grayAlpha.d600}, 0px 0px 0px 4px rgba(255,255,255,0.24)`;
|
||||||
const focusColor = blue.d900
|
const focusColor = blue.d900
|
||||||
@@ -406,7 +418,8 @@ const dark = (() => {
|
|||||||
focusBorder,
|
focusBorder,
|
||||||
focusColor,
|
focusColor,
|
||||||
d1000,
|
d1000,
|
||||||
text
|
text,
|
||||||
|
brand
|
||||||
};
|
};
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -415,6 +428,7 @@ export const [lightClass, theme] = createTheme({
|
|||||||
space,
|
space,
|
||||||
font,
|
font,
|
||||||
color: light,
|
color: light,
|
||||||
|
input: formInput
|
||||||
});
|
});
|
||||||
|
|
||||||
export const darkClass = createTheme(theme, {
|
export const darkClass = createTheme(theme, {
|
||||||
@@ -423,4 +437,5 @@ export const darkClass = createTheme(theme, {
|
|||||||
space,
|
space,
|
||||||
font,
|
font,
|
||||||
color: dark,
|
color: dark,
|
||||||
|
input: formInput
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user