create-sting-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/bin/create-sting-app.js +8 -0
- package/lib/run.js +268 -0
- package/package.json +30 -0
- package/template/vanilla-js/_gitignore +4 -0
- package/template/vanilla-js/index.html +22 -0
- package/template/vanilla-js/src/main.js +15 -0
- package/template/vanilla-js/src/style.css +63 -0
- package/template/vanilla-ts/_gitignore +4 -0
- package/template/vanilla-ts/index.html +22 -0
- package/template/vanilla-ts/src/main.ts +15 -0
- package/template/vanilla-ts/src/stingjs.d.ts +9 -0
- package/template/vanilla-ts/src/style.css +63 -0
- package/template/vanilla-ts/tsconfig.json +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# create-sting-app
|
|
2
|
+
|
|
3
|
+
Scaffold a StingJS starter app with JavaScript or TypeScript.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create sting@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You can also run the package directly:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-sting-app@latest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
With a target directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create sting@latest my-sting-app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Select a template without prompts:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm create sting@latest my-sting-app -- --template typescript --yes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Template values:
|
|
30
|
+
- `javascript` (`js`)
|
|
31
|
+
- `typescript` (`ts`)
|
|
32
|
+
|
|
33
|
+
## Local Development
|
|
34
|
+
|
|
35
|
+
From repo root:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node create-sting-app/bin/create-sting-app.js my-sting-app
|
|
39
|
+
```
|
package/lib/run.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import fsp from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import process from "node:process"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
import { createInterface } from "node:readline/promises"
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
9
|
+
const __dirname = path.dirname(__filename)
|
|
10
|
+
|
|
11
|
+
const templates = {
|
|
12
|
+
javascript: path.resolve(__dirname, "../template/vanilla-js"),
|
|
13
|
+
typescript: path.resolve(__dirname, "../template/vanilla-ts"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const defaultProjectName = "sting-app"
|
|
17
|
+
|
|
18
|
+
export async function run(argv = process.argv.slice(2), options = {}) {
|
|
19
|
+
const cliName = options.cliName || "create-sting-app"
|
|
20
|
+
const parsed = parseArgs(argv)
|
|
21
|
+
|
|
22
|
+
if (parsed.help) {
|
|
23
|
+
printHelp(cliName)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const resolved = await resolveScaffoldOptions({
|
|
28
|
+
projectArg: parsed.projectArg,
|
|
29
|
+
templateArg: parsed.templateArg,
|
|
30
|
+
skipPrompts: parsed.skipPrompts,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const projectNameInput = resolved.projectName
|
|
34
|
+
const template = resolved.template
|
|
35
|
+
const templateRoot = templates[template]
|
|
36
|
+
|
|
37
|
+
if (!templateRoot) {
|
|
38
|
+
throw new Error(`unknown template: ${template}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const targetDir = path.resolve(process.cwd(), projectNameInput)
|
|
42
|
+
const inCurrentDir = projectNameInput === "."
|
|
43
|
+
|
|
44
|
+
const rawName = inCurrentDir ? path.basename(process.cwd()) : projectNameInput
|
|
45
|
+
const packageName = toPackageName(rawName)
|
|
46
|
+
|
|
47
|
+
await ensureTargetDir(targetDir, parsed.force)
|
|
48
|
+
await copyDir(templateRoot, targetDir)
|
|
49
|
+
|
|
50
|
+
const pkg = buildPackageJson({ packageName, template })
|
|
51
|
+
const pkgPath = path.join(targetDir, "package.json")
|
|
52
|
+
await fsp.writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf8")
|
|
53
|
+
|
|
54
|
+
console.log("\nDone. Created StingJS app:")
|
|
55
|
+
console.log(` ${targetDir}`)
|
|
56
|
+
console.log(`\nTemplate: ${template}`)
|
|
57
|
+
console.log("\nNext steps:")
|
|
58
|
+
|
|
59
|
+
if (!inCurrentDir) {
|
|
60
|
+
const relativeTarget = path.relative(process.cwd(), targetDir)
|
|
61
|
+
const cdTarget = !relativeTarget || relativeTarget.startsWith("..")
|
|
62
|
+
? targetDir
|
|
63
|
+
: relativeTarget
|
|
64
|
+
console.log(` cd ${cdTarget}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(" npm install")
|
|
68
|
+
console.log(" npm run dev")
|
|
69
|
+
console.log(`\nRun \`${cliName} --help\` for options.`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseArgs(argv) {
|
|
73
|
+
let projectArg = ""
|
|
74
|
+
let templateArg = ""
|
|
75
|
+
let force = false
|
|
76
|
+
let skipPrompts = false
|
|
77
|
+
let help = false
|
|
78
|
+
|
|
79
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
80
|
+
const arg = argv[index]
|
|
81
|
+
|
|
82
|
+
if (arg === "--force" || arg === "-f") {
|
|
83
|
+
force = true
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (arg === "--yes" || arg === "-y") {
|
|
88
|
+
skipPrompts = true
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (arg === "--help" || arg === "-h") {
|
|
93
|
+
help = true
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (arg === "--template" || arg === "-t") {
|
|
98
|
+
const nextArg = argv[index + 1]
|
|
99
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
100
|
+
throw new Error("missing value for --template. Use --template javascript or --template typescript.")
|
|
101
|
+
}
|
|
102
|
+
templateArg = nextArg
|
|
103
|
+
index += 1
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (arg.startsWith("--template=")) {
|
|
108
|
+
templateArg = arg.slice("--template=".length)
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!arg.startsWith("-") && !projectArg) {
|
|
113
|
+
projectArg = arg
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { projectArg, templateArg, force, skipPrompts, help }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function printHelp(cliName) {
|
|
121
|
+
console.log(cliName)
|
|
122
|
+
console.log("")
|
|
123
|
+
console.log("Usage:")
|
|
124
|
+
console.log(` ${cliName} [project-name] [options]`)
|
|
125
|
+
console.log("")
|
|
126
|
+
console.log("Options:")
|
|
127
|
+
console.log(" -t, --template <name> javascript | typescript")
|
|
128
|
+
console.log(" -f, --force allow scaffolding into a non-empty directory")
|
|
129
|
+
console.log(" -y, --yes skip prompts and use defaults")
|
|
130
|
+
console.log(" -h, --help show this message")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function resolveScaffoldOptions({ projectArg, templateArg, skipPrompts }) {
|
|
134
|
+
const normalizedProject = projectArg && projectArg.trim() ? projectArg.trim() : ""
|
|
135
|
+
const normalizedTemplate = normalizeTemplate(templateArg)
|
|
136
|
+
|
|
137
|
+
if (skipPrompts) {
|
|
138
|
+
return {
|
|
139
|
+
projectName: normalizedProject || defaultProjectName,
|
|
140
|
+
template: normalizedTemplate || "javascript",
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (normalizedProject && normalizedTemplate) {
|
|
145
|
+
return {
|
|
146
|
+
projectName: normalizedProject,
|
|
147
|
+
template: normalizedTemplate,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const projectName = normalizedProject || await promptProjectName(rl)
|
|
155
|
+
const template = normalizedTemplate || await promptTemplate(rl)
|
|
156
|
+
return { projectName, template }
|
|
157
|
+
} finally {
|
|
158
|
+
rl.close()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function promptProjectName(rl) {
|
|
163
|
+
const answer = (await rl.question(`Project name (${defaultProjectName}): `)).trim()
|
|
164
|
+
return answer || defaultProjectName
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function promptTemplate(rl) {
|
|
168
|
+
for (;;) {
|
|
169
|
+
const answer = (await rl.question("Language? [1] JavaScript [2] TypeScript (1): ")).trim().toLowerCase()
|
|
170
|
+
|
|
171
|
+
if (!answer || answer === "1" || answer === "javascript" || answer === "js") {
|
|
172
|
+
return "javascript"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (answer === "2" || answer === "typescript" || answer === "ts") {
|
|
176
|
+
return "typescript"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("Please enter 1 (JavaScript) or 2 (TypeScript).")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeTemplate(input) {
|
|
184
|
+
if (typeof input !== "string") return ""
|
|
185
|
+
|
|
186
|
+
const normalized = input.trim().toLowerCase()
|
|
187
|
+
if (!normalized) return ""
|
|
188
|
+
|
|
189
|
+
if (normalized === "javascript" || normalized === "js" || normalized === "vanilla" || normalized === "vanilla-js") {
|
|
190
|
+
return "javascript"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (normalized === "typescript" || normalized === "ts" || normalized === "vanilla-ts") {
|
|
194
|
+
return "typescript"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new Error(`unknown template \"${input}\". Use \"javascript\" or \"typescript\".`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildPackageJson({ packageName, template }) {
|
|
201
|
+
const base = {
|
|
202
|
+
name: packageName,
|
|
203
|
+
private: true,
|
|
204
|
+
version: "0.0.0",
|
|
205
|
+
type: "module",
|
|
206
|
+
scripts: {
|
|
207
|
+
dev: "vite",
|
|
208
|
+
build: "vite build",
|
|
209
|
+
preview: "vite preview",
|
|
210
|
+
},
|
|
211
|
+
dependencies: {
|
|
212
|
+
stingjs: "^1.0.0",
|
|
213
|
+
},
|
|
214
|
+
devDependencies: {
|
|
215
|
+
vite: "^7.3.1",
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (template === "typescript") {
|
|
220
|
+
base.scripts.typecheck = "tsc --noEmit"
|
|
221
|
+
base.devDependencies.typescript = "^5.9.3"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return base
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function ensureTargetDir(targetDir, force) {
|
|
228
|
+
if (!fs.existsSync(targetDir)) {
|
|
229
|
+
await fsp.mkdir(targetDir, { recursive: true })
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const entries = await fsp.readdir(targetDir)
|
|
234
|
+
if (entries.length === 0) return
|
|
235
|
+
|
|
236
|
+
if (!force) {
|
|
237
|
+
throw new Error(`target directory is not empty: ${targetDir}. Use --force to continue.`)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function copyDir(srcDir, destDir) {
|
|
242
|
+
const entries = await fsp.readdir(srcDir, { withFileTypes: true })
|
|
243
|
+
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
const srcPath = path.join(srcDir, entry.name)
|
|
246
|
+
const name = entry.name === "_gitignore" ? ".gitignore" : entry.name
|
|
247
|
+
const destPath = path.join(destDir, name)
|
|
248
|
+
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
await fsp.mkdir(destPath, { recursive: true })
|
|
251
|
+
await copyDir(srcPath, destPath)
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await fsp.copyFile(srcPath, destPath)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toPackageName(input) {
|
|
260
|
+
const trimmed = input.trim().toLowerCase()
|
|
261
|
+
const sanitized = trimmed
|
|
262
|
+
.replace(/^\.+/, "")
|
|
263
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
264
|
+
.replace(/^-+/, "")
|
|
265
|
+
.replace(/-+$/, "")
|
|
266
|
+
|
|
267
|
+
return sanitized || defaultProjectName
|
|
268
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-sting-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a StingJS app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-sting-app": "./bin/create-sting-app.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
"./cli": "./lib/run.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"template",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"stingjs",
|
|
20
|
+
"create-sting",
|
|
21
|
+
"create-sting-app",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"vite",
|
|
24
|
+
"typescript"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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" />
|
|
6
|
+
<title>Sting App</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main class="app" x-data="counter">
|
|
10
|
+
<h1>StingJS Starter</h1>
|
|
11
|
+
<p class="hint">Counter demo powered by StingJS directives.</p>
|
|
12
|
+
|
|
13
|
+
<div class="row">
|
|
14
|
+
<button x-on:click="dec">Decrease</button>
|
|
15
|
+
<span class="count" x-text="count"></span>
|
|
16
|
+
<button x-on:click="inc">Increase</button>
|
|
17
|
+
</div>
|
|
18
|
+
</main>
|
|
19
|
+
|
|
20
|
+
<script type="module" src="/src/main.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sting from "stingjs"
|
|
2
|
+
import "./style.css"
|
|
3
|
+
|
|
4
|
+
sting.data("counter", () => {
|
|
5
|
+
const [count, setCount] = sting.signal(0)
|
|
6
|
+
|
|
7
|
+
const inc = () => setCount((value) => value + 1)
|
|
8
|
+
const dec = () => setCount((value) => value - 1)
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
count,
|
|
12
|
+
inc,
|
|
13
|
+
dec,
|
|
14
|
+
}
|
|
15
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
3
|
+
line-height: 1.4;
|
|
4
|
+
color: #0f172a;
|
|
5
|
+
background: radial-gradient(circle at top, #dbeafe 0%, #eff6ff 45%, #ffffff 100%);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
display: grid;
|
|
16
|
+
place-items: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app {
|
|
20
|
+
width: min(560px, calc(100vw - 32px));
|
|
21
|
+
background: #ffffff;
|
|
22
|
+
border: 1px solid #bfdbfe;
|
|
23
|
+
border-radius: 16px;
|
|
24
|
+
padding: 24px;
|
|
25
|
+
box-shadow: 0 16px 36px rgba(30, 64, 175, 0.16);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
margin: 0 0 8px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hint {
|
|
33
|
+
margin: 0 0 20px;
|
|
34
|
+
color: #334155;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.row {
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
button {
|
|
44
|
+
border: 1px solid #93c5fd;
|
|
45
|
+
background: #eff6ff;
|
|
46
|
+
color: #1d4ed8;
|
|
47
|
+
border-radius: 10px;
|
|
48
|
+
padding: 8px 14px;
|
|
49
|
+
font-size: 0.95rem;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
button:hover {
|
|
55
|
+
background: #dbeafe;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.count {
|
|
59
|
+
min-width: 52px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
font-size: 1.5rem;
|
|
62
|
+
font-weight: 700;
|
|
63
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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" />
|
|
6
|
+
<title>Sting App</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<main class="app" x-data="counter">
|
|
10
|
+
<h1>StingJS Starter</h1>
|
|
11
|
+
<p class="hint">Counter demo powered by StingJS directives.</p>
|
|
12
|
+
|
|
13
|
+
<div class="row">
|
|
14
|
+
<button x-on:click="dec">Decrease</button>
|
|
15
|
+
<span class="count" x-text="count"></span>
|
|
16
|
+
<button x-on:click="inc">Increase</button>
|
|
17
|
+
</div>
|
|
18
|
+
</main>
|
|
19
|
+
|
|
20
|
+
<script type="module" src="/src/main.ts"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sting from "stingjs"
|
|
2
|
+
import "./style.css"
|
|
3
|
+
|
|
4
|
+
sting.data("counter", () => {
|
|
5
|
+
const [count, setCount] = sting.signal(0)
|
|
6
|
+
|
|
7
|
+
const inc = () => setCount((value) => value + 1)
|
|
8
|
+
const dec = () => setCount((value) => value - 1)
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
count,
|
|
12
|
+
inc,
|
|
13
|
+
dec,
|
|
14
|
+
}
|
|
15
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: "Avenir Next", "Segoe UI", sans-serif;
|
|
3
|
+
line-height: 1.4;
|
|
4
|
+
color: #0f172a;
|
|
5
|
+
background: radial-gradient(circle at top, #dbeafe 0%, #eff6ff 45%, #ffffff 100%);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
margin: 0;
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
display: grid;
|
|
16
|
+
place-items: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.app {
|
|
20
|
+
width: min(560px, calc(100vw - 32px));
|
|
21
|
+
background: #ffffff;
|
|
22
|
+
border: 1px solid #bfdbfe;
|
|
23
|
+
border-radius: 16px;
|
|
24
|
+
padding: 24px;
|
|
25
|
+
box-shadow: 0 16px 36px rgba(30, 64, 175, 0.16);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
h1 {
|
|
29
|
+
margin: 0 0 8px;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hint {
|
|
33
|
+
margin: 0 0 20px;
|
|
34
|
+
color: #334155;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.row {
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: 12px;
|
|
40
|
+
align-items: center;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
button {
|
|
44
|
+
border: 1px solid #93c5fd;
|
|
45
|
+
background: #eff6ff;
|
|
46
|
+
color: #1d4ed8;
|
|
47
|
+
border-radius: 10px;
|
|
48
|
+
padding: 8px 14px;
|
|
49
|
+
font-size: 0.95rem;
|
|
50
|
+
font-weight: 600;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
button:hover {
|
|
55
|
+
background: #dbeafe;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.count {
|
|
59
|
+
min-width: 52px;
|
|
60
|
+
text-align: center;
|
|
61
|
+
font-size: 1.5rem;
|
|
62
|
+
font-weight: 700;
|
|
63
|
+
}
|