codex: components v2 maybe
This commit is contained in:
18
README.md
18
README.md
@@ -1,25 +1,11 @@
|
||||
<center>
|
||||
<img src="packages/website/src/assets/banner.png" alt="Reacord: Create interactive Discord messages using React">
|
||||
</center>
|
||||
|
||||
## Installation ∙ [](https://www.npmjs.com/package/reacord)
|
||||
|
||||
```console
|
||||
# npm
|
||||
npm install reacord react discord.js
|
||||
|
||||
# yarn
|
||||
yarn add reacord react discord.js
|
||||
|
||||
# pnpm
|
||||
pnpm add reacord react discord.js
|
||||
# bun
|
||||
bun add reacord react discord.js
|
||||
|
||||
```
|
||||
|
||||
## Get Started
|
||||
|
||||
[Visit the docs to get started.](https://reacord.mapleleaf.dev/guides/getting-started)
|
||||
|
||||
## Example
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
|
||||
17
package.json
17
package.json
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "reacord-monorepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "run-s --continue-on-error lint:*",
|
||||
"lint:eslint": "eslint . --fix --cache --cache-file=node_modules/.cache/.eslintcache --report-unused-disable-directives",
|
||||
"lint:prettier": "prettier . \"**/*.astro\" --write --cache --list-different",
|
||||
"lint:types": "tsc -b & pnpm -r --parallel run typecheck",
|
||||
"astro-sync": "pnpm --filter website exec astro sync",
|
||||
"lint:types": "bun run --cwd packages/helpers typecheck && bun run --cwd packages/reacord typecheck",
|
||||
"test": "vitest",
|
||||
"build": "pnpm -r run build",
|
||||
"build:website": "pnpm --filter website... run build",
|
||||
"start": "pnpm -C packages/website run start",
|
||||
"start:website": "pnpm -C packages/website run start",
|
||||
"release": "pnpm -r run build && changeset publish"
|
||||
"build": "bun run --cwd packages/reacord build",
|
||||
"release": "bun run build && changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.26.2",
|
||||
@@ -21,7 +20,6 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.3",
|
||||
"react": "^18.2.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
@@ -33,8 +31,7 @@
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"packages/website/public/api"
|
||||
"dist"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
|
||||
@@ -13,6 +13,13 @@ import type { ComponentEvent } from "../component-event"
|
||||
import { OptionNode } from "./option-node"
|
||||
import { omit } from "@reacord/helpers/omit.js"
|
||||
|
||||
export type SelectMenuType =
|
||||
| "string"
|
||||
| "user"
|
||||
| "role"
|
||||
| "mentionable"
|
||||
| "channel"
|
||||
|
||||
/** @category Select */
|
||||
export interface SelectProps {
|
||||
children?: ReactNode
|
||||
@@ -25,6 +32,20 @@ export interface SelectProps {
|
||||
/** The text shown when no value is selected */
|
||||
placeholder?: string
|
||||
|
||||
/**
|
||||
* The kind of select menu to render.
|
||||
*
|
||||
* Defaults to `string`.
|
||||
*/
|
||||
menuType?: SelectMenuType
|
||||
|
||||
/**
|
||||
* Limit the channel types shown in a channel select menu.
|
||||
*
|
||||
* This is only used when `menuType` is `channel`.
|
||||
*/
|
||||
channelTypes?: number[]
|
||||
|
||||
/** Set to true to allow multiple selected values */
|
||||
multiple?: boolean
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ReacordInstance } from "./instance.js"
|
||||
import { raise } from "@reacord/helpers/raise.js"
|
||||
import * as React from "react"
|
||||
import type { MessageStore } from "../internal/message-store.js"
|
||||
|
||||
const Context = React.createContext<ReacordInstance | undefined>(undefined)
|
||||
const MessageContext = React.createContext<MessageStore | undefined>(undefined)
|
||||
|
||||
export const InstanceProvider = Context.Provider
|
||||
export const MessageProvider = MessageContext.Provider
|
||||
|
||||
/**
|
||||
* Get the associated instance for the current component.
|
||||
@@ -18,3 +21,27 @@ export function useInstance(): ReacordInstance {
|
||||
raise("Could not find instance, was this component rendered via Reacord?")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message that the current component is rendered into.
|
||||
*
|
||||
* @category Core
|
||||
*/
|
||||
export function useMessage() {
|
||||
const store =
|
||||
React.useContext(MessageContext) ??
|
||||
raise("Could not find message store, was this component rendered via Reacord?")
|
||||
|
||||
const getSnapshot = React.useCallback(() => store.getSnapshot(), [store])
|
||||
|
||||
if (React.useSyncExternalStore) {
|
||||
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
||||
}
|
||||
|
||||
const [value, setValue] = React.useState(getSnapshot)
|
||||
React.useEffect(() => store.subscribe(() => setValue(getSnapshot())), [
|
||||
store,
|
||||
getSnapshot,
|
||||
])
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
super(config)
|
||||
|
||||
client.on("interactionCreate", (interaction) => {
|
||||
if (interaction.isButton() || interaction.isStringSelectMenu()) {
|
||||
if (interaction.isButton() || interaction.isAnySelectMenu()) {
|
||||
this.handleComponentInteraction(
|
||||
this.createReacordComponentInteraction(interaction),
|
||||
)
|
||||
@@ -154,7 +154,9 @@ export class ReacordDiscordJs extends Reacord {
|
||||
raise(`Channel ${channel.id} must be a text channel`)
|
||||
}
|
||||
|
||||
const message = await channel.send({
|
||||
const textChannel = channel as Discord.TextBasedChannel &
|
||||
Discord.PartialTextBasedChannelFields
|
||||
const message = await textChannel.send({
|
||||
...getDiscordMessageOptions(messageOptions),
|
||||
...messageCreateOptions,
|
||||
})
|
||||
@@ -212,41 +214,16 @@ export class ReacordDiscordJs extends Reacord {
|
||||
|
||||
const message: ComponentEventMessage =
|
||||
interaction.message instanceof Discord.Message
|
||||
? {
|
||||
...pick(interaction.message, [
|
||||
"id",
|
||||
"channelId",
|
||||
"authorId",
|
||||
"content",
|
||||
"tts",
|
||||
"mentionEveryone",
|
||||
]),
|
||||
timestamp: new Date(
|
||||
interaction.message.createdTimestamp,
|
||||
).toISOString(),
|
||||
editedTimestamp: interaction.message.editedTimestamp
|
||||
? new Date(interaction.message.editedTimestamp).toISOString()
|
||||
: undefined,
|
||||
mentions: interaction.message.mentions.users.map((u) => u.id),
|
||||
authorId: interaction.message.author.id,
|
||||
mentionEveryone: interaction.message.mentions.everyone,
|
||||
}
|
||||
? createComponentEventMessage(interaction.message)
|
||||
: raise("Message not found")
|
||||
|
||||
const member: ComponentEventGuildMember | undefined =
|
||||
interaction.member instanceof Discord.GuildMember
|
||||
? {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.member, [
|
||||
"id",
|
||||
"nick",
|
||||
"displayName",
|
||||
"avatarUrl",
|
||||
"displayAvatarUrl",
|
||||
"color",
|
||||
"pending",
|
||||
]),
|
||||
pick(interaction.member, ["nick", "avatarUrl", "pending"]),
|
||||
),
|
||||
id: interaction.member.id,
|
||||
displayName: interaction.member.displayName,
|
||||
roles: interaction.member.roles.cache.map((role) => role.id),
|
||||
joinedAt: interaction.member.joinedAt?.toISOString(),
|
||||
@@ -260,15 +237,17 @@ export class ReacordDiscordJs extends Reacord {
|
||||
|
||||
const guild: ComponentEventGuild | undefined = interaction.guild
|
||||
? {
|
||||
...pruneNullishValues(pick(interaction.guild, ["id", "name"])),
|
||||
id: interaction.guild.id,
|
||||
name: interaction.guild.name,
|
||||
member: member ?? raise("unexpected: member is undefined"),
|
||||
}
|
||||
: undefined
|
||||
|
||||
const user: ComponentEventUser = {
|
||||
...pruneNullishValues(
|
||||
pick(interaction.user, ["id", "username", "discriminator", "tag"]),
|
||||
),
|
||||
id: interaction.user.id,
|
||||
username: interaction.user.username,
|
||||
discriminator: interaction.user.discriminator,
|
||||
tag: interaction.user.tag,
|
||||
avatarUrl: interaction.user.avatarURL(),
|
||||
accentColor: interaction.user.accentColor ?? undefined,
|
||||
}
|
||||
@@ -331,7 +310,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
}
|
||||
}
|
||||
|
||||
if (interaction.isStringSelectMenu()) {
|
||||
if (interaction.isAnySelectMenu()) {
|
||||
return {
|
||||
...baseProps,
|
||||
type: "select",
|
||||
@@ -348,6 +327,7 @@ export class ReacordDiscordJs extends Reacord {
|
||||
|
||||
function createReacordMessage(message: Discord.Message): Message {
|
||||
return {
|
||||
data: createComponentEventMessage(message),
|
||||
edit: async (options) => {
|
||||
await message.edit(getDiscordMessageOptions(options))
|
||||
},
|
||||
@@ -357,6 +337,28 @@ function createReacordMessage(message: Discord.Message): Message {
|
||||
}
|
||||
}
|
||||
|
||||
function createComponentEventMessage(
|
||||
message: Discord.Message,
|
||||
): ComponentEventMessage {
|
||||
return {
|
||||
...pick(message, [
|
||||
"id",
|
||||
"channelId",
|
||||
"authorId",
|
||||
"content",
|
||||
"tts",
|
||||
"mentionEveryone",
|
||||
]),
|
||||
timestamp: new Date(message.createdTimestamp).toISOString(),
|
||||
editedTimestamp: message.editedTimestamp
|
||||
? new Date(message.editedTimestamp).toISOString()
|
||||
: undefined,
|
||||
mentions: message.mentions.users.map((u) => u.id),
|
||||
authorId: message.author.id,
|
||||
mentionEveryone: message.mentions.everyone,
|
||||
}
|
||||
}
|
||||
|
||||
function convertButtonStyleToEnum(style: MessageButtonOptions["style"]) {
|
||||
const styleMap = {
|
||||
primary: Discord.ButtonStyle.Primary,
|
||||
@@ -403,16 +405,60 @@ function getDiscordMessageOptions(reacordOptions: MessageOptions) {
|
||||
// future proofing
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (component.type === "select") {
|
||||
const {
|
||||
menuType,
|
||||
values,
|
||||
options: selectOptions,
|
||||
channelTypes,
|
||||
multiple,
|
||||
...rest
|
||||
} = component
|
||||
|
||||
if (menuType === "string" || menuType == undefined) {
|
||||
return {
|
||||
...component,
|
||||
type: Discord.ComponentType.SelectMenu,
|
||||
options: component.options.map((option) => ({
|
||||
...rest,
|
||||
type: Discord.ComponentType.StringSelect,
|
||||
options: selectOptions.map((option) => ({
|
||||
...option,
|
||||
default: component.values?.includes(option.value),
|
||||
default: values?.includes(option.value),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
if (menuType === "user") {
|
||||
return {
|
||||
...rest,
|
||||
type: Discord.ComponentType.UserSelect,
|
||||
}
|
||||
}
|
||||
|
||||
if (menuType === "role") {
|
||||
return {
|
||||
...rest,
|
||||
type: Discord.ComponentType.RoleSelect,
|
||||
}
|
||||
}
|
||||
|
||||
if (menuType === "mentionable") {
|
||||
return {
|
||||
...rest,
|
||||
type: Discord.ComponentType.MentionableSelect,
|
||||
}
|
||||
}
|
||||
|
||||
if (menuType === "channel") {
|
||||
return {
|
||||
...rest,
|
||||
type: Discord.ComponentType.ChannelSelect,
|
||||
channelTypes,
|
||||
}
|
||||
}
|
||||
|
||||
raise(
|
||||
`Unsupported select menu type: ${menuType ?? "string"}`,
|
||||
)
|
||||
}
|
||||
|
||||
component satisfies never
|
||||
throw new Error(
|
||||
`Invalid component type ${safeJsonStringify(component)}}`,
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react"
|
||||
import type { ComponentInteraction } from "../internal/interaction.js"
|
||||
import { reconciler } from "../internal/reconciler.js"
|
||||
import type { Renderer } from "../internal/renderers/renderer.js"
|
||||
import { InstanceProvider } from "./instance-context.js"
|
||||
import { InstanceProvider, MessageProvider } from "./instance-context.js"
|
||||
import type { ReacordInstance } from "./instance.js"
|
||||
|
||||
/** @category Core */
|
||||
@@ -54,7 +54,11 @@ export abstract class Reacord {
|
||||
const instance: ReacordInstance = {
|
||||
render: (content: ReactNode) => {
|
||||
reconciler.updateContainer(
|
||||
<InstanceProvider value={instance}>{content}</InstanceProvider>,
|
||||
<InstanceProvider value={instance}>
|
||||
<MessageProvider value={renderer.messageStore}>
|
||||
{content}
|
||||
</MessageProvider>
|
||||
</InstanceProvider>,
|
||||
container,
|
||||
)
|
||||
return instance
|
||||
|
||||
22
packages/reacord/library/internal/message-store.ts
Normal file
22
packages/reacord/library/internal/message-store.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ComponentEventMessage } from "../core/component-event"
|
||||
|
||||
export class MessageStore {
|
||||
private value: ComponentEventMessage | undefined
|
||||
private listeners = new Set<() => void>()
|
||||
|
||||
getSnapshot = () => this.value
|
||||
|
||||
subscribe = (listener: () => void) => {
|
||||
this.listeners.add(listener)
|
||||
return () => {
|
||||
this.listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
set(value: ComponentEventMessage | undefined) {
|
||||
this.value = value
|
||||
for (const listener of this.listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentEventMessage } from "../core/component-event"
|
||||
import type { EmbedOptions } from "../core/components/embed-options"
|
||||
import type { SelectProps } from "../core/components/select"
|
||||
import { last } from "@reacord/helpers/last"
|
||||
@@ -47,6 +48,7 @@ export interface MessageSelectOptionOptions {
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
data?: ComponentEventMessage
|
||||
edit(options: MessageOptions): Promise<void>
|
||||
delete(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const config: HostConfig<
|
||||
never, // SuspenseInstance,
|
||||
never, // HydratableInstance,
|
||||
never, // PublicInstance,
|
||||
never, // HostContext,
|
||||
null, // HostContext,
|
||||
true, // UpdatePayload,
|
||||
never, // ChildSet,
|
||||
number, // TimeoutHandle,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Container } from "../container.js"
|
||||
import type { ComponentInteraction } from "../interaction"
|
||||
import { MessageStore } from "../message-store.js"
|
||||
import type { Message, MessageOptions } from "../message"
|
||||
import type { Node } from "../node.js"
|
||||
import { Subject } from "rxjs"
|
||||
@@ -12,6 +13,7 @@ type UpdatePayload =
|
||||
|
||||
export abstract class Renderer {
|
||||
readonly nodes = new Container<Node<unknown>>()
|
||||
readonly messageStore = new MessageStore()
|
||||
private componentInteraction?: ComponentInteraction
|
||||
private message?: Message
|
||||
private active = true
|
||||
@@ -75,6 +77,7 @@ export abstract class Renderer {
|
||||
private async updateMessage(payload: UpdatePayload) {
|
||||
if (payload.action === "destroy") {
|
||||
this.updateSubscription.unsubscribe()
|
||||
this.messageStore.set(undefined)
|
||||
await this.message?.delete()
|
||||
return
|
||||
}
|
||||
@@ -113,5 +116,6 @@ export abstract class Renderer {
|
||||
}
|
||||
|
||||
this.message = await this.createMessage(payload.options)
|
||||
this.messageStore.set(this.message.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ export * from "./core/components/link"
|
||||
export * from "./core/components/option"
|
||||
export * from "./core/components/select"
|
||||
export * from "./core/instance"
|
||||
export { useInstance } from "./core/instance-context"
|
||||
export { useInstance, useMessage } from "./core/instance-context"
|
||||
export * from "./core/reacord"
|
||||
export * from "./core/reacord-discord-js"
|
||||
|
||||
@@ -36,22 +36,21 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "cpy ../../README.md ../../LICENSE . && tsup library/main.ts --target node18 --format cjs,esm --sourcemap --dts --dts-resolve",
|
||||
"build-watch": "pnpm build -- --watch",
|
||||
"build": "rm -rf dist && mkdir -p dist && cp ../../README.md ../../LICENSE . && bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external && bun build library/main.ts --target=node --outdir=dist --format=cjs --sourcemap=external --entry-naming=main.cjs && tsc -p tsconfig.build.json",
|
||||
"build-watch": "bun build library/main.ts --target=node --outdir=dist --format=esm --sourcemap=external --watch",
|
||||
"test": "vitest --coverage --no-watch",
|
||||
"test-dev": "vitest",
|
||||
"test-manual": "nodemon --exec tsx --ext ts,tsx ./scripts/discordjs-manual-test.tsx",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/react": "^18.2.27",
|
||||
"@types/react-reconciler": "^0.28.5",
|
||||
"react-reconciler": "^0.29.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"discord.js": "^14",
|
||||
"discord.js": "^14.25.1",
|
||||
"react": ">=17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -63,15 +62,13 @@
|
||||
"@reacord/helpers": "workspace:*",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"c8": "^8.0.1",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"discord.js": "^14.13.0",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nodemon": "^3.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"tsup": "^7.2.0",
|
||||
"tsx": "^3.13.0",
|
||||
"type-fest": "^4.4.0"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { dirname } from "node:path"
|
||||
import { createRequire } from "node:module"
|
||||
import { beforeAll, expect, test } from "vitest"
|
||||
|
||||
beforeAll(() => {
|
||||
spawnSync("pnpm", ["run", "build"])
|
||||
const cwd = dirname(dirname(fileURLToPath(import.meta.url)))
|
||||
spawnSync("bun", ["run", "build"], { cwd })
|
||||
})
|
||||
|
||||
test("can require commonjs", () => {
|
||||
|
||||
10
packages/reacord/tsconfig.build.json
Normal file
10
packages/reacord/tsconfig.build.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
11
packages/website/.gitignore
vendored
11
packages/website/.gitignore
vendored
@@ -1,11 +0,0 @@
|
||||
node_modules
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
||||
/public/api
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
*.out.css
|
||||
/api
|
||||
.astro
|
||||
@@ -1,56 +0,0 @@
|
||||
# website
|
||||
|
||||
## 0.4.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [11153df]
|
||||
- Updated dependencies [fb0a997]
|
||||
- reacord@0.6.0
|
||||
|
||||
## 0.4.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [ced48a3]
|
||||
- reacord@0.5.5
|
||||
|
||||
## 0.4.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [41c87e3]
|
||||
- reacord@0.5.4
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [104b175]
|
||||
- Updated dependencies [156cf90]
|
||||
- Updated dependencies [0bab505]
|
||||
- Updated dependencies [d76f316]
|
||||
- reacord@0.5.3
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [9813a01]
|
||||
- reacord@0.5.2
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [72f4a4a]
|
||||
- Updated dependencies [7536bde]
|
||||
- Updated dependencies [e335165]
|
||||
- reacord@0.5.1
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [aa65da5]
|
||||
- reacord@0.5.0
|
||||
@@ -1,20 +0,0 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
import prefetch from "@astrojs/prefetch"
|
||||
import react from "@astrojs/react"
|
||||
import tailwind from "@astrojs/tailwind"
|
||||
import { defineConfig } from "astro/config"
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
tailwind({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
react(),
|
||||
prefetch(),
|
||||
],
|
||||
markdown: {
|
||||
shikiConfig: {},
|
||||
},
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "website",
|
||||
"version": "0.4.7",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"dev": "run-p --race --print-label dev:*",
|
||||
"dev:typedoc": "typedoc --watch",
|
||||
"dev:astro": "astro dev",
|
||||
"start": "astro preview",
|
||||
"build": "typedoc && astro build",
|
||||
"typecheck": "astro check && tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/prefetch": "^0.3.0",
|
||||
"@astrojs/react": "^2.3.2",
|
||||
"@fontsource/jetbrains-mono": "^4.5.12",
|
||||
"@fontsource/rubik": "^4.5.14",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@reacord/helpers": "workspace:^",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"astro": "^2.10.15",
|
||||
"clsx": "^2.0.0",
|
||||
"reacord": "workspace:*",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^4.0.0",
|
||||
"@total-typescript/ts-reset": "^0.5.1",
|
||||
"@types/node": "^20.8.4",
|
||||
"@types/react": "^18.2.27",
|
||||
"@types/react-dom": "^18.2.12",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typedoc": "^0.25.2",
|
||||
"wait-on": "^7.0.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="53" height="35" viewBox="0 0 53 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="3" cy="3" r="1" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 146 B |
Binary file not shown.
|
Before Width: | Height: | Size: 658 B |
@@ -1,22 +0,0 @@
|
||||
---
|
||||
import { HeartIcon } from "@heroicons/react/20/solid"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import ExternalLink from "./external-link.astro"
|
||||
|
||||
export interface Props {
|
||||
class?: string
|
||||
}
|
||||
---
|
||||
|
||||
<footer class={twMerge("text-xs opacity-75", Astro.props.class)}>
|
||||
<address class="not-italic">
|
||||
© {new Date().getFullYear()}
|
||||
<ExternalLink class="link" href="https://github.com/itsMapleLeaf">
|
||||
itsMapleLeaf
|
||||
</ExternalLink>
|
||||
</address>
|
||||
<p>
|
||||
Coded with <HeartIcon className="inline w-4 align-sub" /> using{" "}
|
||||
<ExternalLink class="link" href="https://astro.build">Astro</ExternalLink>
|
||||
</p>
|
||||
</footer>
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
---
|
||||
export type Props = astroHTML.JSX.AnchorHTMLAttributes
|
||||
---
|
||||
|
||||
<a rel="noopener noreferrer" target="_blank" {...Astro.props}>
|
||||
<slot />
|
||||
</a>
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
import { getCollection } from "astro:content"
|
||||
import Layout from "./layout.astro"
|
||||
import MainNavigation from "./main-navigation.astro"
|
||||
|
||||
const guides = await getCollection("guides")
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="isolate">
|
||||
<header
|
||||
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
||||
>
|
||||
<div class="container">
|
||||
<MainNavigation />
|
||||
</div>
|
||||
</header>
|
||||
<main class="container mt-8 flex items-start gap-4">
|
||||
<nav class="sticky top-24 hidden w-48 md:block">
|
||||
<h2 class="text-2xl">Guides</h2>
|
||||
<ul class="mt-3 flex flex-col items-start gap-2">
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<li>
|
||||
<a class="link" href={`/guides/${guide.slug}`}>
|
||||
{guide.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="prose prose-invert min-w-0 flex-1 pb-8">
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -1,201 +0,0 @@
|
||||
import clsx from "clsx"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import blobComfyUrl from "~/assets/blob-comfy.png"
|
||||
import cursorIbeamUrl from "~/assets/cursor-ibeam.png"
|
||||
import cursorUrl from "~/assets/cursor.png"
|
||||
import { raise } from "@reacord/helpers/raise.ts"
|
||||
|
||||
const defaultState = {
|
||||
chatInputText: "",
|
||||
chatInputCursorVisible: true,
|
||||
messageVisible: false,
|
||||
count: 0,
|
||||
cursorLeft: "25%",
|
||||
cursorBottom: "-15px",
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
const animationFrame = () =>
|
||||
new Promise((resolve) => requestAnimationFrame(resolve))
|
||||
|
||||
export function LandingAnimation() {
|
||||
const [state, setState] = useState(defaultState)
|
||||
const chatInputRef = useRef<HTMLDivElement>(null)
|
||||
const addRef = useRef<HTMLDivElement>(null)
|
||||
const deleteRef = useRef<HTMLDivElement>(null)
|
||||
const cursorRef = useRef<HTMLImageElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const animateClick = (element: HTMLElement) =>
|
||||
element.animate(
|
||||
[{ transform: `translateY(2px)` }, { transform: `translateY(0px)` }],
|
||||
300,
|
||||
)
|
||||
|
||||
let running = true
|
||||
|
||||
void (async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
while (running) {
|
||||
setState(defaultState)
|
||||
await delay(700)
|
||||
|
||||
for (const letter of "/counter") {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
chatInputText: state.chatInputText + letter,
|
||||
}))
|
||||
await delay(100)
|
||||
}
|
||||
|
||||
await delay(1000)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
messageVisible: true,
|
||||
chatInputText: "",
|
||||
}))
|
||||
await delay(1000)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
cursorLeft: "70px",
|
||||
cursorBottom: "40px",
|
||||
}))
|
||||
await delay(1500)
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
setState((state) => ({
|
||||
...state,
|
||||
count: state.count + 1,
|
||||
chatInputCursorVisible: false,
|
||||
}))
|
||||
animateClick(addRef.current ?? raise("addRef is null"))
|
||||
await delay(700)
|
||||
}
|
||||
|
||||
await delay(500)
|
||||
|
||||
setState((state) => ({
|
||||
...state,
|
||||
cursorLeft: "140px",
|
||||
}))
|
||||
await delay(1000)
|
||||
|
||||
animateClick(deleteRef.current ?? raise("deleteRef is null"))
|
||||
setState((state) => ({ ...state, messageVisible: false }))
|
||||
await delay(1000)
|
||||
|
||||
setState(() => ({
|
||||
...defaultState,
|
||||
chatInputCursorVisible: false,
|
||||
}))
|
||||
await delay(500)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
running = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let running = true
|
||||
|
||||
void (async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
while (running) {
|
||||
const cursor = cursorRef.current ?? raise("cursorRef is null")
|
||||
const chatInput = chatInputRef.current ?? raise("chatInputRef is null")
|
||||
|
||||
// check if the cursor is in the input
|
||||
const cursorRect = cursor.getBoundingClientRect()
|
||||
const chatInputRect = chatInput.getBoundingClientRect()
|
||||
|
||||
const isOverInput =
|
||||
cursorRef.current &&
|
||||
chatInputRef.current &&
|
||||
cursorRect.top + cursorRect.height / 2 > chatInputRect.top
|
||||
|
||||
cursor.src = isOverInput ? cursorIbeamUrl : cursorUrl
|
||||
|
||||
await animationFrame()
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
running = false
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="animate-fade-in pointer-events-none relative grid select-none gap-2"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
"rounded-lg bg-slate-800 p-4 shadow transition",
|
||||
state.messageVisible ? "opacity-100" : "-translate-y-2 opacity-0",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="h-12 w-12 rounded-full bg-black/25 bg-contain bg-no-repeat p-2">
|
||||
<img
|
||||
src={blobComfyUrl}
|
||||
alt=""
|
||||
className="h-full w-full scale-90 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold">comfybot</p>
|
||||
<p>this button was clicked {state.count} times</p>
|
||||
<div className="mt-2 flex flex-row gap-3">
|
||||
<div
|
||||
ref={addRef}
|
||||
className="rounded bg-emerald-700 px-3 py-1.5 text-sm text-white"
|
||||
>
|
||||
+1
|
||||
</div>
|
||||
<div
|
||||
ref={deleteRef}
|
||||
className="rounded bg-red-700 px-3 py-1.5 text-sm text-white"
|
||||
>
|
||||
🗑 delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="rounded-lg bg-slate-700 px-4 pb-2 pt-1.5 shadow"
|
||||
ref={chatInputRef}
|
||||
>
|
||||
<span
|
||||
className={clsx(
|
||||
"text-sm after:relative after:-left-[2px] after:-top-px after:content-[attr(data-after)]",
|
||||
state.chatInputCursorVisible
|
||||
? "after:opacity-100"
|
||||
: "after:opacity-0",
|
||||
)}
|
||||
data-after="|"
|
||||
>
|
||||
{state.chatInputText || (
|
||||
<span className="absolute block translate-y-1 opacity-50">
|
||||
Message #showing-off-reacord
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={cursorUrl}
|
||||
alt=""
|
||||
className="absolute scale-75 bg-transparent transition-all duration-500"
|
||||
style={{ left: state.cursorLeft, bottom: state.cursorBottom }}
|
||||
ref={cursorRef}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
import "@fontsource/jetbrains-mono/500.css"
|
||||
import "@fontsource/rubik/variable.css"
|
||||
import packageJson from "reacord/package.json"
|
||||
import bannerUrl from "~/assets/banner.png"
|
||||
import faviconUrl from "~/assets/favicon.png"
|
||||
import "~/styles/tailwind.css"
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" class="bg-slate-900 text-slate-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<meta name="description" content={packageJson.description} />
|
||||
<meta name="theme-color" content="#21754b" />
|
||||
<meta property="og:url" content="https://reacord.mapleleaf.dev/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Reacord" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Create interactive Discord messages using React"
|
||||
/>
|
||||
<meta property="og:image" content={bannerUrl} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:domain" content="reacord.mapleleaf.dev" />
|
||||
<meta name="twitter:url" content="https://reacord.mapleleaf.dev/" />
|
||||
<meta name="twitter:title" content="Reacord" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Create interactive Discord messages using React"
|
||||
/>
|
||||
<meta name="twitter:image" content={bannerUrl} />
|
||||
|
||||
<title>Reacord</title>
|
||||
|
||||
<link rel="icon" href={faviconUrl} />
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CodeBracketIcon,
|
||||
DocumentTextIcon,
|
||||
} from "@heroicons/react/20/solid"
|
||||
import { Bars3Icon } from "@heroicons/react/24/outline"
|
||||
import { getCollection } from "astro:content"
|
||||
import AppLogo from "./app-logo.astro"
|
||||
import ExternalLink from "./external-link.astro"
|
||||
import MenuItem from "./menu-item.astro"
|
||||
import Menu from "./menu.astro"
|
||||
|
||||
const links = [
|
||||
{
|
||||
href: "/guides/getting-started",
|
||||
label: "Guides",
|
||||
icon: DocumentTextIcon,
|
||||
component: "a",
|
||||
prefetch: true,
|
||||
},
|
||||
{
|
||||
href: "/api/",
|
||||
label: "API Reference",
|
||||
icon: CodeBracketIcon,
|
||||
component: "a",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/itsMapleLeaf/reacord",
|
||||
label: "GitHub",
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
component: ExternalLink,
|
||||
},
|
||||
]
|
||||
|
||||
const guides = await getCollection("guides")
|
||||
---
|
||||
|
||||
<nav class="flex h-16 items-center justify-between">
|
||||
<a href="/">
|
||||
<AppLogo class="w-32" />
|
||||
<span class="sr-only">Home</span>
|
||||
</a>
|
||||
<div class="hidden gap-4 md:flex">
|
||||
{
|
||||
links.map((link) => (
|
||||
<link.component
|
||||
href={link.href}
|
||||
class="link inline-flex items-center gap-1"
|
||||
rel={link.prefetch ? "prefetch" : undefined}
|
||||
>
|
||||
<link.icon className="inline-icon" />
|
||||
{link.label}
|
||||
</link.component>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<Menu>
|
||||
<Fragment slot="button">
|
||||
<Bars3Icon className="w-6" />
|
||||
<span class="sr-only">Menu</span>
|
||||
</Fragment>
|
||||
{
|
||||
links.map((link) => (
|
||||
<link.component href={link.href}>
|
||||
<MenuItem icon={link.icon} label={link.label} />
|
||||
</link.component>
|
||||
))
|
||||
}
|
||||
<hr class="border-black/25" />
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<a href={`/guides/${guide.slug}`} rel="prefetch">
|
||||
<MenuItem icon={DocumentTextIcon} label={guide.data.title} />
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</Menu>
|
||||
</nav>
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
icon: (props: { class?: string; className?: string }) => unknown
|
||||
label: string
|
||||
}
|
||||
---
|
||||
|
||||
<div
|
||||
class="flex w-full items-center gap-1 px-3 py-2 text-left font-medium opacity-50 transition hover:text-emerald-500 hover:opacity-100"
|
||||
>
|
||||
<Astro.props.icon class="inline-icon" className="inline-icon" />
|
||||
<span class="flex-1">{Astro.props.label}</span>
|
||||
</div>
|
||||
@@ -1,30 +0,0 @@
|
||||
<details class="relative md:hidden" data-menu>
|
||||
<summary
|
||||
class="-m-2 cursor-pointer list-none p-2 transition hover:text-emerald-500"
|
||||
>
|
||||
<slot name="button" />
|
||||
</summary>
|
||||
<div
|
||||
class="absolute right-0 top-[calc(100%+8px)] z-10 max-h-[calc(100vh-5rem)] w-48 overflow-y-auto overflow-x-hidden rounded-lg bg-slate-800 shadow"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<script>
|
||||
for (const menu of document.querySelectorAll<HTMLDetailsElement>(
|
||||
"[data-menu]",
|
||||
)) {
|
||||
window.addEventListener("click", (event) => {
|
||||
if (!menu.contains(event.target as Node)) {
|
||||
menu.open = false
|
||||
}
|
||||
})
|
||||
menu.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
menu.open = false
|
||||
menu.querySelector("summary")!.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
export type Props = astroHTML.JSX.AnchorHTMLAttributes & {
|
||||
href: string
|
||||
}
|
||||
|
||||
const removeTrailingSlash = (str: string) => str.replace(/\/$/, "")
|
||||
|
||||
const linkUrl = new URL(Astro.props.href, Astro.url)
|
||||
|
||||
const isActive =
|
||||
removeTrailingSlash(Astro.url.pathname) ===
|
||||
removeTrailingSlash(linkUrl.pathname)
|
||||
---
|
||||
|
||||
<a {...Astro.props} data-active={isActive || undefined}>
|
||||
<slot />
|
||||
</a>
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineCollection, z } from "astro:content"
|
||||
|
||||
export const collections = {
|
||||
guides: defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
}),
|
||||
}),
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Learn how to get started with Reacord.
|
||||
slug: getting-started
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
These guides assume some familiarity with [JavaScript](https://developer.mozilla.org/en-US/docs/Web/javascript), [TypeScript](https://www.typescriptlang.org/), [React](https://reactjs.org), [Discord.js](https://discord.js.org) and the [Discord API](https://discord.dev). Keep these pages as reference if you need it.
|
||||
|
||||
## Setup from template
|
||||
|
||||
[Use this starter template](https://github.com/itsMapleLeaf/reacord-starter) to get off the ground quickly.
|
||||
|
||||
## Adding to an existing project
|
||||
|
||||
Install Reacord and dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install reacord react discord.js
|
||||
|
||||
# yarn
|
||||
yarn add reacord react discord.js
|
||||
|
||||
# pnpm
|
||||
pnpm add reacord react discord.js
|
||||
```
|
||||
|
||||
Create a Discord.js client and a Reacord instance:
|
||||
|
||||
```ts
|
||||
import { Client, Events } from "discord.js"
|
||||
import { ReacordDiscordJs } from "reacord"
|
||||
|
||||
const client = new Client()
|
||||
const reacord = new ReacordDiscordJs(client)
|
||||
|
||||
client.once(Events.ClientReady, () => {
|
||||
console.log("Ready!")
|
||||
})
|
||||
|
||||
await client.login(process.env.BOT_TOKEN)
|
||||
```
|
||||
@@ -1,215 +0,0 @@
|
||||
---
|
||||
title: Sending Messages
|
||||
description: Sending messages by creating Reacord instances
|
||||
slug: sending-messages
|
||||
---
|
||||
|
||||
# Sending Messages with Instances
|
||||
|
||||
You can send messages via Reacord to a channel like so.
|
||||
|
||||
```tsx
|
||||
client.once(Events.ClientReady, () => {
|
||||
const channel = await client.channels.fetch("abc123deadbeef")
|
||||
reacord.createChannelMessage(channel).render("Hello, world!")
|
||||
})
|
||||
```
|
||||
|
||||
The `.createChannelMessage()` function creates a **Reacord instance**. You can pass strings, numbers, or anything that can be rendered by React, such as JSX!
|
||||
|
||||
Components rendered through this instance can include state and effects, and the message on Discord will update automatically.
|
||||
|
||||
```tsx
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
function Uptime() {
|
||||
const [startTime] = useState(Date.now())
|
||||
const [currentTime, setCurrentTime] = useState(Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTime(Date.now())
|
||||
}, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
return <>this message has been shown for {currentTime - startTime}ms</>
|
||||
}
|
||||
|
||||
client.once(Events.ClientReady, () => {
|
||||
const instance = reacord.createChannelMessage(channel)
|
||||
instance.render(<Uptime />)
|
||||
})
|
||||
```
|
||||
|
||||
The instance can be rendered to multiple times, which will update the message each time.
|
||||
|
||||
```tsx
|
||||
interface HelloProps {
|
||||
subject: string
|
||||
}
|
||||
|
||||
const Hello = ({ subject }: HelloProps) => <>Hello, {subject}!</>
|
||||
|
||||
client.once(Events.ClientReady, () => {
|
||||
const instance = reacord.createChannelMessage(channel)
|
||||
instance.render(<Hello subject="World" />)
|
||||
instance.render(<Hello subject="Moon" />)
|
||||
})
|
||||
```
|
||||
|
||||
You can specify various options for the message:
|
||||
|
||||
```tsx
|
||||
const instance = reacord.createChannelMessage(channel, {
|
||||
tts: true,
|
||||
reply: {
|
||||
messageReference: someMessage.id,
|
||||
},
|
||||
flags: [MessageFlags.SuppressNotifications],
|
||||
})
|
||||
```
|
||||
|
||||
See the [Discord.js docs](https://discord.js.org/#/docs/discord.js/main/typedef/MessageCreateOptions) for all of the available options.
|
||||
|
||||
## Cleaning Up Instances
|
||||
|
||||
If you no longer want to use the instance, you can clean it up in a few ways:
|
||||
|
||||
- `instance.destroy()` - This will remove the message.
|
||||
- `instance.deactivate()` - This will keep the message, but it will disable the components on the message, and no longer listen to user interactions.
|
||||
|
||||
By default, Reacord has a max limit on the number of active instances, and deactivates older instances to conserve memory. This can be configured through the Reacord options:
|
||||
|
||||
```ts
|
||||
const reacord = new ReacordDiscordJs(client, {
|
||||
// after sending four messages,
|
||||
// the first one will be deactivated
|
||||
maxInstances: 3,
|
||||
})
|
||||
```
|
||||
|
||||
## Discord Slash Commands
|
||||
|
||||
<aside>
|
||||
This section also applies to other kinds of application commands, such as context menu commands.
|
||||
</aside>
|
||||
|
||||
To reply to a command interaction, use the `.createInteractionReply()` function. This function returns an instance that works the same way as the one from `.createChannelMessage()`. Here's an example:
|
||||
|
||||
```tsx
|
||||
import { Client, Events } from "discord.js"
|
||||
import { Button, ReacordDiscordJs } from "reacord"
|
||||
import * as React from "react"
|
||||
|
||||
const client = new Client({ intents: [] })
|
||||
const reacord = new ReacordDiscordJs(client)
|
||||
|
||||
client.once(Events.ClientReady, () => {
|
||||
client.application?.commands.create({
|
||||
name: "ping",
|
||||
description: "pong!",
|
||||
})
|
||||
})
|
||||
|
||||
client.on(Events.InteractionCreate, (interaction) => {
|
||||
if (interaction.isCommand() && interaction.commandName === "ping") {
|
||||
// Use the createInteractionReply() function instead of createChannelMessage
|
||||
reacord.createInteractionReply(interaction).render(<>pong!</>)
|
||||
}
|
||||
})
|
||||
|
||||
await client.login(process.env.DISCORD_TOKEN)
|
||||
```
|
||||
|
||||
<aside>
|
||||
This example uses <a href="https://discord.com/developers/docs/interactions/application-commands#registering-a-command">global commands</a>, so the command might take a while to show up 😅
|
||||
</aside>
|
||||
|
||||
However, the process of creating commands can get really repetitive and error-prone. A command framework could help with this, or you could make a small helper:
|
||||
|
||||
```tsx
|
||||
import type { Client, CommandInteraction } from "discord.js"
|
||||
|
||||
interface Command {
|
||||
// Command name
|
||||
name: string
|
||||
// A mandatory description for the command
|
||||
description: string
|
||||
// Specific handler for the command
|
||||
run: (interaction: CommandInteraction) => Promise<void> | void
|
||||
}
|
||||
|
||||
function handleCommands(client: Client, commands: Command[]) {
|
||||
client.once(Events.ClientReady, () => {
|
||||
for (const { name, description } of commands) {
|
||||
client.application?.commands.create({ name, description })
|
||||
}
|
||||
})
|
||||
|
||||
client.on(Events.InteractionCreate, (interaction) => {
|
||||
if (interaction.isCommand()) {
|
||||
for (const command of commands) {
|
||||
if (interaction.commandName === command.name) {
|
||||
command.run(interaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "ping",
|
||||
description: "pong!",
|
||||
run: (interaction) => {
|
||||
reacord.createInteractionReply(interaction).render(<>pong!</>)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hi",
|
||||
description: "say hi",
|
||||
run: (interaction) => {
|
||||
reacord.createInteractionReply(interaction).render(<>hi</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
## Ephemeral Command Replies
|
||||
|
||||
Ephemeral replies are replies that only appear for one user. To create them, use the `.createInteractionReply()` function and provide `ephemeral` option.
|
||||
|
||||
```tsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "pong",
|
||||
description: "pong, but in secret",
|
||||
run: (interaction) => {
|
||||
reacord
|
||||
.createInteractionReply(interaction, { ephemeral: true })
|
||||
.render(<>(pong)</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
## Text-to-Speech Command Replies
|
||||
|
||||
Additionally interaction replies may have `tts` option to turn on text-to-speech ability for the reply. To create such reply, use `.createInteractionReply()` function and provide `tts` option.
|
||||
|
||||
```tsx
|
||||
handleCommands(client, [
|
||||
{
|
||||
name: "pong",
|
||||
description: "pong, but converted into audio",
|
||||
run: (interaction) => {
|
||||
reacord
|
||||
.createInteractionReply(interaction, { tts: true })
|
||||
.render(<>pong!</>)
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
title: Embeds
|
||||
description: Using embed components
|
||||
slug: embeds
|
||||
---
|
||||
|
||||
# Embeds
|
||||
|
||||
Reacord comes with an `<Embed />` component for sending rich embeds.
|
||||
|
||||
```tsx
|
||||
import { Embed } from "reacord"
|
||||
|
||||
interface FancyMessageProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
function FancyMessage({ title, description }: FancyMessageProps) {
|
||||
return (
|
||||
<Embed
|
||||
title={title}
|
||||
description={description}
|
||||
color={0x00ff00}
|
||||
timestamp={Date.now()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
reacord
|
||||
.createChannelMessage(channel)
|
||||
.render(<FancyMessage title="Hello" description="World" />)
|
||||
```
|
||||
|
||||
Reacord also comes with multiple embed components, for defining embeds on a piece-by-piece basis. This enables composition:
|
||||
|
||||
```tsx
|
||||
import { Embed, EmbedTitle } from "reacord"
|
||||
|
||||
interface FancyDetailsProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
function FancyDetails({ title, description }: FancyDetailsProps) {
|
||||
return (
|
||||
<>
|
||||
<EmbedTitle>{title}</EmbedTitle>
|
||||
{/* embed descriptions are just text */}
|
||||
{description}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface FancyMessageProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
function FancyMessage({ children }: FancyMessageProps) {
|
||||
return (
|
||||
<Embed color={0x00ff00} timestamp={Date.now()}>
|
||||
{children}
|
||||
</Embed>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
reacord.createChannelMessage(channel).render(
|
||||
<FancyMessage>
|
||||
<FancyDetails title="Hello" description="World" />
|
||||
</FancyMessage>,
|
||||
)
|
||||
```
|
||||
|
||||
See the [API Reference](/api/index.html#EmbedAuthorProps) for the full list of embed components.
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: Buttons
|
||||
description: Using button components
|
||||
slug: buttons
|
||||
---
|
||||
|
||||
# Buttons
|
||||
|
||||
Use the `<Button />` component to create a message with a button, and use the `onClick` callback to respond to button clicks.
|
||||
|
||||
```tsx
|
||||
import { Button } from "reacord"
|
||||
import { useState } from "react"
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<>
|
||||
You've clicked the button {count} times.
|
||||
<Button label="+1" onClick={() => setCount(count + 1)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The `onClick` callback receives an `event` object. It includes some information, such as the user who clicked the button, and functions for creating new replies in response. These functions return message instances.
|
||||
|
||||
```tsx
|
||||
import { Button, type ComponentEvent } from "reacord"
|
||||
|
||||
export function SuspiciousButton() {
|
||||
function handleClick(event: ComponentEvent) {
|
||||
const name = event.guild.member.displayName ?? event.user.username
|
||||
|
||||
const publicReply = event.reply(`${name} clicked the button. wow`)
|
||||
setTimeout(() => publicReply.destroy(), 3000)
|
||||
|
||||
const privateReply = event.reply("good job, you clicked it", {
|
||||
ephemeral: true,
|
||||
})
|
||||
privateReply.deactivate() // we don't need to listen to updates on this
|
||||
}
|
||||
|
||||
return <Button label="click me i dare you" onClick={handleClick} />
|
||||
}
|
||||
```
|
||||
|
||||
See the [API reference](/api/index.html#ButtonProps) for more information.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
title: Links
|
||||
description: Using link components
|
||||
slug: links
|
||||
---
|
||||
|
||||
# Links
|
||||
|
||||
In Discord, links are a type of button, and they work similarly. Clicking on it leads you to the given URL. They only have one style, and can't be listened to for clicks.
|
||||
|
||||
```tsx
|
||||
import { Link } from "reacord"
|
||||
|
||||
export function AwesomeLinks() {
|
||||
return (
|
||||
<>
|
||||
<Link label="look at this" url="https://google.com" />
|
||||
<Link label="wow" url="https://youtube.com/watch?v=dQw4w9WgXcQ" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
See the [API reference](/api/index.html#Link) for more information.
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
title: Select Menus
|
||||
description: Using select menu components
|
||||
slug: select-menus
|
||||
---
|
||||
|
||||
# Select Menus
|
||||
|
||||
To create a select menu, use the `Select` component, and pass a list of `Option` components as children. Use the `value` prop to set a currently selected value. You can respond to changes in the value via `onChangeValue`.
|
||||
|
||||
```tsx
|
||||
import { Button, Option, Select } from "reacord"
|
||||
import { useState } from "react"
|
||||
|
||||
interface FruitSelectProps {
|
||||
onConfirm: (choice: string) => void
|
||||
}
|
||||
|
||||
export function FruitSelect({ onConfirm }: FruitSelectProps) {
|
||||
const [value, setValue] = useState<string>()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
placeholder="choose a fruit"
|
||||
value={value}
|
||||
onChangeValue={setValue}
|
||||
>
|
||||
<Option value="🍎" />
|
||||
<Option value="🍌" />
|
||||
<Option value="🍒" />
|
||||
</Select>
|
||||
<Button
|
||||
label="confirm"
|
||||
disabled={value == undefined}
|
||||
onClick={() => {
|
||||
if (value) onConfirm(value)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
const instance = reacord.createChannelMessage(channel).render(
|
||||
<FruitSelect
|
||||
onConfirm={(value) => {
|
||||
instance.render(`you chose ${value}`)
|
||||
instance.deactivate()
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
```
|
||||
|
||||
For a multi-select, use the `multiple` prop, then you can use `values` and `onChangeMultiple` to handle multiple values.
|
||||
|
||||
```tsx
|
||||
interface FruitSelectProps {
|
||||
onConfirm: (choices: string[]) => void
|
||||
}
|
||||
|
||||
export function FruitSelect({ onConfirm }: FruitSelectProps) {
|
||||
const [values, setValues] = useState<string[]>([])
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder="choose a fruit"
|
||||
values={values}
|
||||
onChangeMultiple={setValues}
|
||||
>
|
||||
<Option value="🍎" />
|
||||
<Option value="🍌" />
|
||||
<Option value="🍒" />
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The Select component accepts a variety of props beyond what's shown here. See the [API reference](/api/index.html#SelectChangeEvent) for more information.
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
title: useInstance
|
||||
description: Using useInstance to get the current instance within a component
|
||||
slug: use-instance
|
||||
---
|
||||
|
||||
# useInstance
|
||||
|
||||
You can use `useInstance` to get the current [instance](/guides/sending-messages) within a component. This can be used to let a component destroy or deactivate itself.
|
||||
|
||||
```tsx
|
||||
import { Button, useInstance } from "reacord"
|
||||
|
||||
export function SelfDestruct() {
|
||||
const instance = useInstance()
|
||||
return (
|
||||
<Button
|
||||
style="danger"
|
||||
label="delete this"
|
||||
onClick={() => instance.destroy()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
reacord.createChannelMessage(channel).render(<SelfDestruct />)
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
title: Using Reacord with other libraries
|
||||
description: Adapting Reacord to another Discord library
|
||||
slug: custom-adapters
|
||||
---
|
||||
|
||||
# Using Reacord with other libraries
|
||||
|
||||
Reacord's core is built to be library agnostic, and can be adapted to libraries other than Discord.js. However, Discord.js is the only built-in adapter at the moment, and the adapter API is still a work in progress.
|
||||
|
||||
If you're interested in creating a custom adapter, [see the code for the Discord.js adapter as an example](https://github.com/itsMapleLeaf/reacord/blob/main/packages/reacord/library/core/reacord-discord-js.ts). Feel free to [create an issue on GitHub](https://github.com/itsMapleLeaf/reacord/issues/new) if you run into issues.
|
||||
3
packages/website/src/env.d.ts
vendored
3
packages/website/src/env.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
/// <reference types="astro/client" />
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
@@ -1,95 +0,0 @@
|
||||
---
|
||||
import type { GetStaticPaths } from "astro"
|
||||
import { getCollection, type CollectionEntry } from "astro:content"
|
||||
import AppFooter from "~/components/app-footer.astro"
|
||||
import Layout from "~/components/layout.astro"
|
||||
import MainNavigation from "~/components/main-navigation.astro"
|
||||
import NavLink from "~/components/nav-link.astro"
|
||||
|
||||
export interface Props {
|
||||
guide: CollectionEntry<"guides">
|
||||
}
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async () => {
|
||||
const guides = await getCollection("guides")
|
||||
return guides.map((guide) => ({
|
||||
params: { slug: guide.slug },
|
||||
props: { guide },
|
||||
}))
|
||||
}
|
||||
|
||||
const guides = await getCollection("guides")
|
||||
const { Content } = await Astro.props.guide.render()
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div class="isolate">
|
||||
<header
|
||||
class="sticky top-0 z-10 flex bg-slate-700/30 shadow backdrop-blur-sm transition"
|
||||
>
|
||||
<div class="container">
|
||||
<MainNavigation />
|
||||
</div>
|
||||
</header>
|
||||
<main class="container mt-8 flex items-start gap-4">
|
||||
<nav
|
||||
class="sticky top-24 hidden h-[calc(100vh-theme(spacing.28))] w-48 flex-col gap-3 md:flex"
|
||||
>
|
||||
<h2 class="text-2xl">Guides</h2>
|
||||
<ul class="flex flex-col items-start gap-2">
|
||||
{
|
||||
guides.map((guide) => (
|
||||
<li>
|
||||
<NavLink
|
||||
class="link data-[active]:link-active"
|
||||
href={`/guides/${guide.slug}`}
|
||||
rel="prefetch"
|
||||
>
|
||||
{guide.data.title}
|
||||
</NavLink>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
<AppFooter class="mt-auto" />
|
||||
</nav>
|
||||
<article class="-mt-8 min-w-0 max-w-none flex-1 pb-8">
|
||||
<Content />
|
||||
</article>
|
||||
</main>
|
||||
<AppFooter class="mx-auto mb-4 text-center md:hidden" />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
article :global(:where(h1, h2, h3, h4, h5, h6)) {
|
||||
@apply mb-3 mt-8 font-light;
|
||||
}
|
||||
article :global(h1) {
|
||||
@apply text-3xl lg:text-4xl;
|
||||
}
|
||||
article :global(h2) {
|
||||
@apply text-2xl;
|
||||
}
|
||||
article :global(h3) {
|
||||
@apply text-xl;
|
||||
}
|
||||
article :global(p) {
|
||||
@apply my-3;
|
||||
}
|
||||
article :global(a) {
|
||||
@apply font-medium text-emerald-400 hover:no-underline;
|
||||
}
|
||||
article :global(strong) {
|
||||
@apply font-medium text-emerald-400;
|
||||
}
|
||||
article :global(code) {
|
||||
@apply rounded border border-slate-800 bg-slate-950 px-1 py-0.5 leading-none text-slate-300;
|
||||
}
|
||||
article :global(pre) {
|
||||
@apply my-4 overflow-x-auto rounded-md border border-slate-800 !bg-slate-950 px-4 py-3 font-monospace;
|
||||
}
|
||||
article :global(pre code) {
|
||||
@apply border-none bg-transparent p-0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
import dotsBackgroundUrl from "~/assets/dots-background.svg"
|
||||
import AppFooter from "~/components/app-footer.astro"
|
||||
import AppLogo from "~/components/app-logo.astro"
|
||||
import { LandingAnimation } from "~/components/landing-animation"
|
||||
import Layout from "~/components/layout.astro"
|
||||
import MainNavigation from "~/components/main-navigation.astro"
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<div
|
||||
class="fixed inset-0 rotate-6 scale-125 opacity-20"
|
||||
style={{ backgroundImage: `url(${dotsBackgroundUrl})` }}
|
||||
>
|
||||
</div>
|
||||
<div class="relative flex min-h-screen min-w-0 flex-col gap-4 pb-4">
|
||||
<header class="container">
|
||||
<MainNavigation />
|
||||
</header>
|
||||
<div class="my-auto flex flex-col gap-4 px-4">
|
||||
<AppLogo class="mx-auto w-full max-w-lg" />
|
||||
|
||||
<div class="isolate mx-auto h-44 w-full max-w-md">
|
||||
<LandingAnimation client:only />
|
||||
</div>
|
||||
|
||||
<p class="-mb-1 text-center text-lg font-light">
|
||||
Create interactive Discord messages with React.
|
||||
</p>
|
||||
|
||||
<div class="flex gap-4 self-center">
|
||||
<a href="/guides/getting-started" class="button button-solid">
|
||||
Get Started
|
||||
</a>
|
||||
|
||||
<!-- <UncontrolledModal
|
||||
button={(button) => (
|
||||
<button {...button} class={buttonClass({ variant: "semiblack" })}>
|
||||
Show Code
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<div class="text-sm sm:text-base">
|
||||
<LandingCode />
|
||||
</div>
|
||||
</UncontrolledModal> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container text-center">
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -1,59 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:focus {
|
||||
@apply outline-none;
|
||||
}
|
||||
:focus-visible {
|
||||
@apply ring-2 ring-inset ring-emerald-500;
|
||||
}
|
||||
|
||||
pre,
|
||||
code {
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.container {
|
||||
@apply mx-auto w-full max-w-screen-lg px-4;
|
||||
}
|
||||
|
||||
.inline-icon {
|
||||
@apply inline w-5 align-sub;
|
||||
}
|
||||
|
||||
.link {
|
||||
@apply relative inline-block font-medium opacity-60 transition-opacity hover:opacity-100;
|
||||
}
|
||||
.link::after {
|
||||
@apply absolute bottom-[-2px] block h-px w-full translate-y-[3px] bg-current opacity-0 transition content-[''];
|
||||
}
|
||||
.link:hover::after {
|
||||
@apply -translate-y-px opacity-50;
|
||||
}
|
||||
.link-active {
|
||||
@apply text-emerald-500 opacity-100;
|
||||
}
|
||||
|
||||
.button {
|
||||
@apply mt-4 inline-block rounded-lg bg-black/25 px-4 py-2.5 text-xl transition hover:-translate-y-0.5 hover:bg-black/40 hover:shadow active:translate-y-0 active:transition-none;
|
||||
}
|
||||
.button-solid {
|
||||
@apply bg-emerald-700 hover:bg-emerald-800;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.5s;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
import config from "../../tailwind.config.ts"
|
||||
|
||||
export default {
|
||||
...config,
|
||||
content: ["./src/**/*.{ts,tsx,md,astro}"],
|
||||
} satisfies Config
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "public/api"]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"entryPoints": ["../reacord/library/main.ts"],
|
||||
"out": ["public/api"],
|
||||
"tsconfig": "../reacord/tsconfig.json",
|
||||
"excludeInternal": true,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"categorizeByGroup": false,
|
||||
"preserveWatchOutput": true,
|
||||
"githubPages": false,
|
||||
"readme": "none",
|
||||
"categoryOrder": [
|
||||
"Core",
|
||||
"Embed",
|
||||
"Button",
|
||||
"Link",
|
||||
"Select",
|
||||
"Action Row",
|
||||
"Component Event",
|
||||
"*"
|
||||
]
|
||||
}
|
||||
9699
pnpm-lock.yaml
generated
9699
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,2 @@
|
||||
packages:
|
||||
- packages/*
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export default {
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ["RubikVariable", "sans-serif"],
|
||||
monospace: ["'JetBrains Mono'", "monospace"],
|
||||
},
|
||||
boxShadow: {
|
||||
DEFAULT: "0 2px 9px 0 rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3)",
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
corePlugins: {
|
||||
container: false,
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "@itsmapleleaf/configs/tsconfig"
|
||||
"extends": "@itsmapleleaf/configs/tsconfig.bundler.json"
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"github": {
|
||||
"silent": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user