@glassly/create-miniapp 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @glassly/create-miniapp
2
+
3
+ Scaffold a new [Glassly](https://github.com/tetramo-labs/glassly)
4
+ miniapp project — the `bun create` starter for apps that run on Glassly
5
+ smart glasses.
6
+
7
+ ## Usage
8
+
9
+ ```sh
10
+ bunx @glassly/create-miniapp my-app
11
+ cd my-app
12
+ bun install
13
+ bun run dev
14
+ ```
15
+
16
+ > **Bun-only.** The scaffolder and the generated project's tooling run under
17
+ > [Bun](https://bun.sh) — use `bun` / `bunx`, not `npx`.
18
+
19
+ The generated project comes wired with
20
+ [`@glassly/miniapp`](https://www.npmjs.com/package/@glassly/miniapp) (the SDK
21
+ runtime) and [`@glassly/miniapp-cli`](https://www.npmjs.com/package/@glassly/miniapp-cli)
22
+ (the `dev` / `build` / `pack` author flow), a `miniapp.json` manifest, and a
23
+ TypeScript setup. Dependency pins are stamped at publish time to the exact
24
+ SDK versions this scaffolder shipped with, so a generated project installs
25
+ on any release channel.
26
+
27
+ To publish a finished miniapp to the Glassly Miniapp Store, see
28
+ [`@glassly/cli`](https://www.npmjs.com/package/@glassly/cli) (`glassly login` /
29
+ `glassly publish`).
30
+
31
+ ## Part of Glassly
32
+
33
+ Source lives in the [Glassly monorepo](https://github.com/tetramo-labs/glassly)
34
+ under `sdk/create-miniapp`. Issues and contributions welcome there.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ // Extensionless launcher: npm strips bin entries that point at .ts files.
3
+ import "./index.ts"
package/bin/index.ts ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import * as fs from "fs"
4
+ import * as path from "path"
5
+ import {intro, outro, text, select, isCancel, cancel, note} from "@clack/prompts"
6
+
7
+ type GlassesType = "display" | "camera"
8
+
9
+ interface CliFlags {
10
+ projectName?: string
11
+ type?: GlassesType
12
+ }
13
+
14
+ function parseArgs(argv: string[]): CliFlags {
15
+ const flags: CliFlags = {}
16
+ for (const arg of argv) {
17
+ if (arg.startsWith("--type=")) {
18
+ const value = arg.slice("--type=".length)
19
+ if (value === "display" || value === "camera") {
20
+ flags.type = value
21
+ } else {
22
+ console.error(`Invalid --type value: "${value}". Expected "display" or "camera".`)
23
+ process.exit(1)
24
+ }
25
+ } else if (!arg.startsWith("-") && !flags.projectName) {
26
+ flags.projectName = arg
27
+ }
28
+ }
29
+ return flags
30
+ }
31
+
32
+ function toKebabCase(str: string): string {
33
+ return str
34
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
35
+ .replace(/[\s_]+/g, "-")
36
+ .toLowerCase()
37
+ }
38
+
39
+ function validateProjectName(name: string): string | undefined {
40
+ if (!name.trim()) return "Project name is required"
41
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*$/.test(name)) {
42
+ return "Use letters, numbers, dashes, underscores, or spaces (cannot start with a symbol)"
43
+ }
44
+ if (fs.existsSync(path.resolve(process.cwd(), name))) {
45
+ return `Directory "${name}" already exists`
46
+ }
47
+ return undefined
48
+ }
49
+
50
+ function bailIfCancelled<T>(value: T | symbol): T {
51
+ if (isCancel(value)) {
52
+ cancel("Cancelled.")
53
+ process.exit(0)
54
+ }
55
+ return value as T
56
+ }
57
+
58
+ const flags = parseArgs(process.argv.slice(2))
59
+
60
+ intro("create-miniapp")
61
+
62
+ let projectName = flags.projectName
63
+ if (projectName) {
64
+ const err = validateProjectName(projectName)
65
+ if (err) {
66
+ cancel(err)
67
+ process.exit(1)
68
+ }
69
+ } else {
70
+ projectName = bailIfCancelled(
71
+ await text({
72
+ message: "What's your project name?",
73
+ placeholder: "my-miniapp",
74
+ validate: validateProjectName,
75
+ }),
76
+ )
77
+ }
78
+
79
+ let glassesType = flags.type
80
+ if (!glassesType) {
81
+ glassesType = bailIfCancelled(
82
+ await select<GlassesType>({
83
+ message: "What kind of glasses is this miniapp for?",
84
+ options: [
85
+ {
86
+ value: "display",
87
+ label: "Display glasses",
88
+ hint: "Even Realities G1/G2, Vuzix Z100",
89
+ },
90
+ {
91
+ value: "camera",
92
+ label: "Camera glasses",
93
+ hint: "Mentra Live",
94
+ },
95
+ ],
96
+ }),
97
+ )
98
+ }
99
+
100
+ const targetDir = path.resolve(process.cwd(), projectName)
101
+ const templateDir = path.resolve(import.meta.dirname, "..", "template")
102
+
103
+ const kebabName = toKebabCase(projectName)
104
+ const packageName = `com.glassly.${kebabName}`
105
+ const hardwareRequirements =
106
+ glassesType === "camera"
107
+ ? [
108
+ {type: "CAMERA", level: "REQUIRED"},
109
+ {type: "MICROPHONE", level: "REQUIRED"},
110
+ ]
111
+ : [
112
+ {type: "DISPLAY", level: "REQUIRED"},
113
+ {type: "MICROPHONE", level: "REQUIRED"},
114
+ ]
115
+
116
+ function renderFile(srcPath: string, destPath: string): void {
117
+ if (path.basename(srcPath) === "miniapp.json") {
118
+ const raw = fs.readFileSync(srcPath, "utf-8")
119
+ const manifest = JSON.parse(raw.replace(/\{\{packageName\}\}/g, packageName))
120
+ manifest.hardwareRequirements = hardwareRequirements
121
+ fs.writeFileSync(destPath, JSON.stringify(manifest, null, 2) + "\n")
122
+ return
123
+ }
124
+
125
+ const content = fs.readFileSync(srcPath, "utf-8").replace(/\{\{packageName\}\}/g, packageName)
126
+ fs.writeFileSync(destPath, content)
127
+ }
128
+
129
+ function copyDir(src: string, dest: string): void {
130
+ fs.mkdirSync(dest, {recursive: true})
131
+ for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
132
+ const srcPath = path.join(src, entry.name)
133
+ // npm strips literal `.gitignore` files on publish; the template
134
+ // ships it as `_gitignore` instead and we rename on copy so the
135
+ // scaffolded project starts with a proper `.gitignore`.
136
+ const renamed = entry.name === "_gitignore" ? ".gitignore" : entry.name
137
+ const destPath = path.join(dest, renamed)
138
+ if (entry.isDirectory()) {
139
+ copyDir(srcPath, destPath)
140
+ } else {
141
+ renderFile(srcPath, destPath)
142
+ }
143
+ }
144
+ }
145
+
146
+ copyDir(templateDir, targetDir)
147
+
148
+ note(`cd ${projectName}\nbun install\nbun dev`, "Next steps")
149
+
150
+ outro(`Created miniapp in ./${projectName}`)
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@glassly/create-miniapp",
3
+ "version": "0.1.0-dev.1",
4
+ "description": "Scaffold a new Glassly miniapp project",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "tag": "dev"
9
+ },
10
+ "bin": {
11
+ "create-miniapp": "bin/create-miniapp"
12
+ },
13
+ "scripts": {
14
+ "build": "echo 'No build needed \u2014 runs via bun'"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.0.0"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "template"
25
+ ],
26
+ "keywords": [
27
+ "glassly",
28
+ "glassly",
29
+ "miniapp",
30
+ "create",
31
+ "scaffold"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tetramo-labs/glassly.git",
36
+ "directory": "sdk/create-miniapp"
37
+ },
38
+ "homepage": "https://github.com/tetramo-labs/glassly/tree/dev/sdk/create-miniapp#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/tetramo-labs/glassly/issues"
41
+ }
42
+ }
@@ -0,0 +1,18 @@
1
+ # Miniapp implementation rules
2
+
3
+ - Put glasses subscriptions, `session.*` calls, durable state, and logic that
4
+ must survive UI closure in `src/background/`.
5
+ - Put React, rendering, DOM access, and browser UI libraries in `src/ui/`.
6
+ - The layers do not share memory. Exchange serializable data through the typed
7
+ `glassly.*` / `session.ui.*` channels declared in `src/shared/channels.ts`.
8
+ - The background is a bare JavaScript engine, not a browser or Node. Supported
9
+ globals are `console`, timers, `queueMicrotask`, `fetch`, `WebSocket`,
10
+ `localStorage`, `crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`,
11
+ `TextDecoder`, `atob`, `btoa`, and the SDK-supported `AbortController` subset.
12
+ - Do not use `window`, `document`, DOM elements, `performance`,
13
+ `XMLHttpRequest`, `crypto.subtle`, Node built-ins, `process`, or runtime module
14
+ resolution in `src/background/`. Use `Date.now()` instead of
15
+ `performance.now()`.
16
+ - `bun run dev` is live and temporary. Multiple dev miniapps can coexist when
17
+ they use distinct manifest package names. Use `bun run release` for a
18
+ persistent local install that runs without the development computer.
@@ -0,0 +1,37 @@
1
+ # Glassly miniapp
2
+
3
+ This project has two runtimes with different jobs:
4
+
5
+ - `src/background/` is always on while the miniapp is enabled. Put glasses
6
+ subscriptions, `session.*` calls, durable state, and logic that must keep
7
+ running after the UI closes here.
8
+ - `src/ui/` is a normal WebView that exists only while the user has the miniapp
9
+ open. Put React, rendering, DOM access, and browser UI libraries here.
10
+
11
+ The background is a bare JavaScript engine, not a browser or Node. It supports
12
+ `console`, timers, `fetch`, `WebSocket`, `localStorage`,
13
+ `crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`, `TextDecoder`,
14
+ `atob`, `btoa`, and the SDK-supported `AbortController` subset. It does not
15
+ support `window`, `document`, DOM elements, `performance`, `XMLHttpRequest`,
16
+ `crypto.subtle`, Node built-ins, `process`, or runtime module resolution. Use
17
+ `Date.now()` instead of `performance.now()`.
18
+
19
+ `bun run build` checks background source (including imported shared files) and
20
+ reports common unsupported browser or Node APIs with a file, line, and supported
21
+ alternative before it emits a bundle.
22
+
23
+ The layers do not share memory. Exchange serializable data through the typed
24
+ `glassly.*` / `session.ui.*` message bus declared in `src/shared/channels.ts`.
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ bun run dev # live, temporary; keep the computer and CLI running
30
+ bun run release # persistent local install that runs without the computer
31
+ bun run build # build both layers into dist/
32
+ ```
33
+
34
+ The Glassly App keeps one dev entry per manifest package name, so you can scan and
35
+ test multiple dev miniapps side by side. It also caches each entry's name and
36
+ icon. Rescanning the same package updates that entry. Use `bun run release` when
37
+ you want the miniapp to run without the computer.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.zip
5
+ .env
6
+ .env.local
7
+ .DS_Store
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Production build script — two-output bundle.
3
+ *
4
+ * Two-layer miniapps ship TWO bundles in one zip:
5
+ * - dist/background/index.js — JSContext entry, no DOM
6
+ * - dist/ui/index.html + assets — WebView entry, full DOM
7
+ *
8
+ * Bun.build() handles both. The background bundle uses `format: "iife"`
9
+ * because JSC + Zipline/QuickJS evaluate it as a classic script — any
10
+ * top-level ESM `export` is a SyntaxError there. `@glassly/miniapp/background`
11
+ * is bundled in (not external) because the JSContext has no module
12
+ * resolver; the host's polyfill installs `__dispatch`/`__deliver` but
13
+ * does NOT provide the SDK surface. The UI bundle uses target: "browser"
14
+ * with no externals.
15
+ *
16
+ * Build-time env vars: any `PUBLIC_*` key in your shell or
17
+ * miniapp-local `.env` gets inlined into both bundles via define.
18
+ * Anything inlined into the UI bundle is visible in WebView network
19
+ * requests and source maps — secrets MUST live behind your own backend
20
+ * rather than in PUBLIC_*.
21
+ *
22
+ * Add bundler plugins (Tailwind, Vue, etc.) by pushing them into the
23
+ * matching `plugins` array. For example, with Tailwind v4 (UI side only):
24
+ *
25
+ * const tailwind = (await import("bun-plugin-tailwind")).default
26
+ * uiBuild.plugins.push(tailwind)
27
+ *
28
+ * Output goes to ./dist with the shape `glassly-miniapp release` expects.
29
+ */
30
+
31
+ import {rm} from "fs/promises"
32
+ import {backgroundRuntimeGuardPlugin, reactSingletonPlugin} from "@glassly/miniapp-cli/build-helpers"
33
+
34
+ const distDir = "./dist"
35
+
36
+ // Wipe dist/ so old chunks don't accumulate across builds.
37
+ await rm(distDir, {recursive: true, force: true})
38
+
39
+ // Pick up PUBLIC_* env vars from the shell / .env. Bun loads .env
40
+ // from cwd automatically; we just need to forward the keys to define.
41
+ const define: Record<string, string> = {}
42
+ for (const [k, v] of Object.entries(process.env)) {
43
+ if (k.startsWith("PUBLIC_") && typeof v === "string") {
44
+ define[`process.env.${k}`] = JSON.stringify(v)
45
+ }
46
+ }
47
+
48
+ const backgroundResult = await Bun.build({
49
+ entrypoints: ["./src/background/index.ts"],
50
+ outdir: `${distDir}/background`,
51
+ target: "browser",
52
+ format: "iife",
53
+ plugins: [backgroundRuntimeGuardPlugin(import.meta.url)],
54
+ minify: false,
55
+ define,
56
+ })
57
+
58
+ if (!backgroundResult.success) {
59
+ console.error("Background build failed:")
60
+ for (const log of backgroundResult.logs) console.error(log)
61
+ process.exit(1)
62
+ }
63
+
64
+ const uiResult = await Bun.build({
65
+ entrypoints: ["./src/ui/index.html"],
66
+ outdir: `${distDir}/ui`,
67
+ target: "browser",
68
+ plugins: [reactSingletonPlugin(import.meta.url)],
69
+ minify: true,
70
+ define,
71
+ })
72
+
73
+ if (!uiResult.success) {
74
+ console.error("UI build failed:")
75
+ for (const log of uiResult.logs) console.error(log)
76
+ process.exit(1)
77
+ }
78
+
79
+ // Silence on success — the dev-server's `reload →` line is the
80
+ // developer-facing confirmation. Failures already print via the
81
+ // .success branches above.
Binary file
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "./node_modules/@glassly/miniapp-cli/schema/miniapp.schema.json",
3
+ "packageName": "{{packageName}}",
4
+ "version": "1.0.0",
5
+ "name": "My Miniapp",
6
+ "description": "A Glassly miniapp",
7
+ "icon": "icon.png",
8
+ "type": "standard",
9
+ "sdkVersion": "0.3.0",
10
+ "minHostVersion": "1.42.0",
11
+ "entry": {
12
+ "background": "background/index.js",
13
+ "ui": "ui/index.html"
14
+ },
15
+ "permissions": [],
16
+ "hardwareRequirements": [
17
+ {"type": "DISPLAY", "level": "REQUIRED", "description": "Renders text + UI on glasses"}
18
+ ]
19
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "glassly-miniapp dev",
6
+ "build": "bun run build.ts",
7
+ "typecheck": "tsc --noEmit",
8
+ "release": "glassly-miniapp release",
9
+ "pack": "glassly-miniapp pack"
10
+ },
11
+ "dependencies": {
12
+ "@glassly/miniapp": "0.1.0-dev.62",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@glassly/miniapp-cli": "0.1.0-dev.1",
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "typescript": "^5.6.0"
21
+ }
22
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Background JSContext entry point. The Glassly host loads this file
3
+ * inside a per-miniapp JSContext (iOS-JSC / Android-QuickJS) and calls
4
+ * the `registerMiniapp` handler once CONNECT completes. All glasses-side
5
+ * logic lives in here.
6
+ *
7
+ * Background runs **always** while the host app process is alive — even
8
+ * when the user hasn't opened the miniapp's UI. Register your event
9
+ * subscriptions inside the handler; the runtime tears them down when the
10
+ * miniapp is disabled or uninstalled.
11
+ */
12
+
13
+ import {registerMiniapp} from "@glassly/miniapp/background"
14
+ import type {Channels} from "../shared/channels"
15
+
16
+ registerMiniapp<Channels>(async (session) => {
17
+ // Hydrate any persisted state from session.storage (string KV — JSON-encode
18
+ // structured data yourself).
19
+ // const stored = await session.storage.get("notes")
20
+ // const notes: Note[] = stored ? JSON.parse(stored) : []
21
+
22
+ // Glasses button → demo display. display.render() replaces the whole frame:
23
+ // describe everything that should be on screen and the host diffs it against
24
+ // the previous frame (stable ids update in place; render([]) clears). Box
25
+ // coordinates are raw device px — read `session.capabilities.display`
26
+ // (populated on the "ready" event) for the real canvas size.
27
+ session.input.onButtonPress(() => {
28
+ const d = session.capabilities?.display
29
+ void session.display.render([
30
+ {
31
+ type: "text",
32
+ id: "hello",
33
+ box: {x: 0, y: 0, w: d?.width ?? 576, h: d?.height ?? 288},
34
+ text: "Hello from GlasslyJS",
35
+ },
36
+ ])
37
+ })
38
+
39
+ // UI bus: forward "ping" → "pong" with a roundtrip timestamp.
40
+ session.ui.onOpen(() => {
41
+ // Push initial state to the WebView whenever it opens.
42
+ session.ui.send("pong", {at: Date.now(), roundtripMs: 0})
43
+ })
44
+ session.ui.on("ping", ({at}) => {
45
+ const roundtripMs = Date.now() - at
46
+ session.ui.send("pong", {at: Date.now(), roundtripMs})
47
+ })
48
+ })
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Typed channel registry — single source of truth for the names + payload
3
+ * shapes that flow between this miniapp's background JSContext and its UI
4
+ * WebView. Both halves import this file at build time; the bundler inlines
5
+ * the declarations so there's no runtime resolution.
6
+ *
7
+ * Add a key per channel. The TypeScript generic on `glassly.send` /
8
+ * `glassly.on` / `session.ui.send` / `session.ui.on` enforces names + payload
9
+ * shapes at compile time so the two halves can't drift.
10
+ */
11
+
12
+ export interface Channels {
13
+ // WebView → background
14
+ "ping": {at: number}
15
+
16
+ // background → WebView
17
+ "pong": {at: number; roundtripMs: number}
18
+ }
19
+
20
+ declare global {
21
+ // Augment the `glassly` global from @glassly/miniapp/ui with this miniapp's
22
+ // typed channel registry so authors get compile-time enforcement on every
23
+ // glassly.send / glassly.on call without re-declaring it.
24
+ // eslint-disable-next-line no-var
25
+ var glassly: import("@glassly/miniapp/ui").TypedUiGlobal<Channels>
26
+ }
@@ -0,0 +1,26 @@
1
+ import {useEffect, useState} from "react"
2
+
3
+ export function App() {
4
+ const [roundtripMs, setRoundtripMs] = useState<number | null>(null)
5
+
6
+ useEffect(() => {
7
+ const unsub = glassly.on("pong", ({roundtripMs}) => {
8
+ setRoundtripMs(roundtripMs)
9
+ })
10
+ return unsub
11
+ }, [])
12
+
13
+ const ping = () => {
14
+ glassly.send("ping", {at: Date.now()})
15
+ }
16
+
17
+ return (
18
+ <div className="app">
19
+ <h1>{`{{packageName}}`}</h1>
20
+ <p>Round-trip to background: {roundtripMs == null ? "—" : `${roundtripMs} ms`}</p>
21
+ <button type="button" onClick={ping}>
22
+ Ping background
23
+ </button>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
6
+ <title>{{packageName}}</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="./main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * WebView entry point. Imports the `glassly` global from the auto-injected
3
+ * host shim and the typed `Channels` registry from src/shared/. The
4
+ * WebView has zero direct native access — all hardware calls (glasses
5
+ * display, sensors, BLE, etc.) go through `glassly.send(channel, payload)`
6
+ * to background, which translates them into `session.*` SDK calls.
7
+ */
8
+
9
+ import {createRoot} from "react-dom/client"
10
+ import {MiniappProvider} from "@glassly/miniapp/ui"
11
+ import "../shared/channels"
12
+ import {App} from "./App"
13
+ import "./styles.css"
14
+
15
+ const root = document.getElementById("root")
16
+ if (!root) {
17
+ throw new Error("Missing #root element in index.html")
18
+ }
19
+
20
+ // MiniappProvider syncs <html class="dark"> with the host color scheme.
21
+ // Purely a CSS theme bridge — does NOT construct a MiniappSession.
22
+ createRoot(root).render(
23
+ <MiniappProvider>
24
+ <App />
25
+ </MiniappProvider>,
26
+ )
27
+
28
+ // MUST call glassly.ready() on bootstrap so the host knows the WebView is
29
+ // mounted and can flush any buffered `session.ui.send` calls.
30
+ glassly.ready()
@@ -0,0 +1,43 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ background: #0b0b0b;
9
+ color: #f5f5f5;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ }
15
+
16
+ .app {
17
+ text-align: center;
18
+ padding: 24px;
19
+ }
20
+
21
+ h1 {
22
+ font-size: 1.5rem;
23
+ margin: 0 0 12px;
24
+ }
25
+
26
+ p {
27
+ margin: 8px 0 24px;
28
+ opacity: 0.7;
29
+ }
30
+
31
+ button {
32
+ font-size: 1rem;
33
+ padding: 12px 24px;
34
+ border-radius: 12px;
35
+ border: none;
36
+ background: #4a9eff;
37
+ color: white;
38
+ cursor: pointer;
39
+ }
40
+
41
+ button:active {
42
+ transform: scale(0.97);
43
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "./dist",
11
+ "rootDir": ".",
12
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
15
+ }