@lessonkit/cli 0.9.2 → 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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { createRequire } from "module";
3
3
  import { Command } from "commander";
4
4
 
5
5
  // src/commands/init.ts
6
+ import { slugifyId } from "@lessonkit/core";
6
7
  import { cp, mkdir, readdir, readFile, writeFile } from "fs/promises";
7
8
  import { existsSync } from "fs";
8
9
  import { basename, dirname, join, resolve } from "path";
@@ -99,9 +100,6 @@ function getTemplateDir() {
99
100
  }
100
101
  return candidates[0];
101
102
  }
102
- function slugifyName(name) {
103
- return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "my-course";
104
- }
105
103
  async function isDirEmpty(dir) {
106
104
  if (!existsSync(dir)) return true;
107
105
  const entries = await readdir(dir);
@@ -151,14 +149,14 @@ async function applyTemplateSubstitutions(projectDir, projectName, slug) {
151
149
  }
152
150
  async function runInit(opts, logger) {
153
151
  const cwd = process.cwd();
154
- const rawName = opts.name ?? (opts.here ? slugifyName(basename(process.cwd()) || "my-course") : void 0);
152
+ const rawName = opts.name ?? (opts.here ? slugifyId(basename(process.cwd()) || "my-course") : void 0);
155
153
  if (!rawName && !opts.here) {
156
154
  throw new CliError("Project name is required. Usage: lessonkit init <name> or lessonkit init --here", {
157
155
  code: "INVALID_PROJECT",
158
156
  exitCode: EXIT_INVALID_PROJECT
159
157
  });
160
158
  }
161
- const slug = slugifyName(rawName ?? "my-course");
159
+ const slug = slugifyId(rawName ?? "my-course");
162
160
  const projectName = rawName ?? slug;
163
161
  const projectDir = opts.here ? cwd : resolve(cwd, slug);
164
162
  if (!opts.here && existsSync(projectDir)) {
@@ -209,18 +207,13 @@ async function runInit(opts, logger) {
209
207
  import { readFileSync, existsSync as existsSync2 } from "fs";
210
208
  import { readFile as readFile2 } from "fs/promises";
211
209
  import { dirname as dirname2, join as join2, parse, resolve as resolve2 } from "path";
212
- import { validateDescriptor, validateProjectPaths } from "@lessonkit/lxpack";
210
+ import { parseLessonkitManifest } from "@lessonkit/lxpack";
213
211
  var LESSONKIT_JSON = "lessonkit.json";
214
212
  var PACKAGE_JSON = "package.json";
215
- var DEFAULT_PATHS = {
216
- spaDistDir: "dist",
217
- lxpackOutDir: ".lxpack/course",
218
- outputBaseDir: ".lxpack/out"
219
- };
220
213
  function isProjectManifest(configPath) {
221
214
  try {
222
215
  const raw = JSON.parse(readFileSync(configPath, "utf8"));
223
- return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object";
216
+ return raw.schemaVersion === 1 && typeof raw.name === "string" && raw.course !== null && typeof raw.course === "object" && !Array.isArray(raw.course);
224
217
  } catch {
225
218
  return false;
226
219
  }
@@ -253,124 +246,66 @@ async function loadLessonkitJson(projectRoot) {
253
246
  exitCode: EXIT_INVALID_PROJECT
254
247
  });
255
248
  }
256
- if (!raw || typeof raw !== "object") {
257
- throw new CliError(`${configPath} must be a JSON object.`, {
258
- code: "INVALID_PROJECT",
259
- exitCode: EXIT_INVALID_PROJECT
260
- });
261
- }
262
- const config = raw;
263
- const schemaVersion = config.schemaVersion;
264
- if (schemaVersion !== 1) {
265
- throw new CliError(`${configPath}: schemaVersion must be 1 (got ${String(schemaVersion)}).`, {
266
- code: "INVALID_PROJECT",
267
- exitCode: EXIT_INVALID_PROJECT
268
- });
249
+ const parsed = parseLessonkitManifest(raw, configPath, projectRoot);
250
+ if (!parsed.ok) {
251
+ throwManifestCliError(configPath, parsed.issues);
269
252
  }
270
- const name = config.name;
271
- if (typeof name !== "string" || !name.trim()) {
272
- throw new CliError(`${configPath}: "name" must be a non-empty string.`, {
273
- code: "INVALID_PROJECT",
274
- exitCode: EXIT_INVALID_PROJECT
275
- });
253
+ return {
254
+ root: projectRoot,
255
+ schemaVersion: 1,
256
+ name: parsed.manifest.name,
257
+ course: parsed.manifest.course,
258
+ paths: parsed.manifest.paths
259
+ };
260
+ }
261
+ function throwManifestCliError(configPath, issues) {
262
+ const layoutIssue = issues.find((i) => i.path === "course.layout");
263
+ if (layoutIssue?.message.includes("per-lesson-spa")) {
264
+ throw new CliError(
265
+ `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
266
+ { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
267
+ );
276
268
  }
277
- const courseRaw = config.course;
278
- if (!courseRaw || typeof courseRaw !== "object") {
279
- throw new CliError(`${configPath}: "course" must be an object.`, {
269
+ const lessonsIssue = issues.find((i) => i.path === "course.lessons");
270
+ if (lessonsIssue) {
271
+ throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
280
272
  code: "INVALID_PROJECT",
281
273
  exitCode: EXIT_INVALID_PROJECT
282
274
  });
283
275
  }
284
- const courseObj = courseRaw;
285
- if (courseObj.lessons !== void 0 && !Array.isArray(courseObj.lessons)) {
286
- throw new CliError(`${configPath}: "course.lessons" must be an array.`, {
276
+ const spaDistTypeIssue = issues.find((i) => i.path === "paths.spaDistDir");
277
+ if (spaDistTypeIssue && spaDistTypeIssue.message.includes("non-empty string")) {
278
+ throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
287
279
  code: "INVALID_PROJECT",
288
280
  exitCode: EXIT_INVALID_PROJECT
289
281
  });
290
282
  }
291
- if (courseObj.assessments !== void 0 && !Array.isArray(courseObj.assessments)) {
292
- throw new CliError(`${configPath}: "course.assessments" must be an array.`, {
283
+ const courseSpaIssue = issues.find((i) => i.path === "course.spaDistDir");
284
+ if (courseSpaIssue) {
285
+ throw new CliError(`${configPath}: ${courseSpaIssue.message}`, {
293
286
  code: "INVALID_PROJECT",
294
287
  exitCode: EXIT_INVALID_PROJECT
295
288
  });
296
289
  }
297
- const validation = validateDescriptor(courseRaw);
298
- if (!validation.ok) {
299
- throw new CliError(`${configPath}: invalid course descriptor.`, {
290
+ if (issues.some((i) => i.path.startsWith("paths."))) {
291
+ throw new CliError(`${configPath}: invalid paths.`, {
300
292
  code: "INVALID_PROJECT",
301
293
  exitCode: EXIT_INVALID_PROJECT,
302
- issues: validation.issues.map((i) => ({
303
- path: i.path,
304
- message: i.message
305
- }))
294
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
306
295
  });
307
296
  }
308
- if (validation.descriptor.layout === "per-lesson-spa") {
309
- throw new CliError(
310
- `${configPath}: per-lesson-spa layout is not supported by lessonkit package yet. Use single-spa or package via @lessonkit/lxpack directly.`,
311
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
312
- );
313
- }
314
- const pathsRaw = config.paths;
315
- const paths = { ...DEFAULT_PATHS };
316
- if (pathsRaw !== void 0 && (typeof pathsRaw !== "object" || pathsRaw === null)) {
317
- throw new CliError(`${configPath}: "paths" must be an object.`, {
297
+ const schemaIssue = issues.find((i) => i.path === "schemaVersion");
298
+ if (schemaIssue) {
299
+ throw new CliError(`${configPath}: schemaVersion must be 1 (got ${schemaIssue.message.replace(/^must be 1 \(got /, "").replace(/\)$/, "")}).`, {
318
300
  code: "INVALID_PROJECT",
319
301
  exitCode: EXIT_INVALID_PROJECT
320
302
  });
321
303
  }
322
- if (pathsRaw && typeof pathsRaw === "object") {
323
- const p = pathsRaw;
324
- if (p.spaDistDir !== void 0) {
325
- if (typeof p.spaDistDir !== "string" || !p.spaDistDir.trim()) {
326
- throw new CliError(`${configPath}: "paths.spaDistDir" must be a non-empty string.`, {
327
- code: "INVALID_PROJECT",
328
- exitCode: EXIT_INVALID_PROJECT
329
- });
330
- }
331
- paths.spaDistDir = p.spaDistDir;
332
- }
333
- if (p.lxpackOutDir !== void 0) {
334
- if (typeof p.lxpackOutDir !== "string" || !p.lxpackOutDir.trim()) {
335
- throw new CliError(`${configPath}: "paths.lxpackOutDir" must be a non-empty string.`, {
336
- code: "INVALID_PROJECT",
337
- exitCode: EXIT_INVALID_PROJECT
338
- });
339
- }
340
- paths.lxpackOutDir = p.lxpackOutDir;
341
- }
342
- if (p.outputBaseDir !== void 0) {
343
- if (typeof p.outputBaseDir !== "string" || !p.outputBaseDir.trim()) {
344
- throw new CliError(`${configPath}: "paths.outputBaseDir" must be a non-empty string.`, {
345
- code: "INVALID_PROJECT",
346
- exitCode: EXIT_INVALID_PROJECT
347
- });
348
- }
349
- paths.outputBaseDir = p.outputBaseDir;
350
- }
351
- }
352
- const courseSpaDistDir = validation.descriptor.spaDistDir?.trim();
353
- if (courseSpaDistDir && courseSpaDistDir !== paths.spaDistDir) {
354
- throw new CliError(
355
- `${configPath}: "course.spaDistDir" (${courseSpaDistDir}) differs from "paths.spaDistDir" (${paths.spaDistDir}). Use paths.spaDistDir for CLI build and package.`,
356
- { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
357
- );
358
- }
359
- const pathIssues = validateProjectPaths(projectRoot, paths);
360
- if (pathIssues.length) {
361
- throw new CliError(`${configPath}: invalid paths.`, {
362
- code: "INVALID_PROJECT",
363
- exitCode: EXIT_INVALID_PROJECT,
364
- issues: pathIssues
365
- });
366
- }
367
- return {
368
- root: projectRoot,
369
- schemaVersion: 1,
370
- name,
371
- course: validation.descriptor,
372
- paths
373
- };
304
+ throw new CliError(`${configPath}: invalid lessonkit manifest.`, {
305
+ code: "INVALID_PROJECT",
306
+ exitCode: EXIT_INVALID_PROJECT,
307
+ issues: issues.map((i) => ({ path: i.path, message: i.message }))
308
+ });
374
309
  }
375
310
  async function loadProject(cwd = process.cwd()) {
376
311
  const root = findProjectRoot(cwd);
@@ -388,7 +323,7 @@ async function readPackageJson(projectRoot) {
388
323
  }
389
324
  }
390
325
  function assertViteProject(pkg, projectRoot) {
391
- const vite = pkg.devDependencies?.vite ?? pkg.dependencies?.vite ?? (existsSync2(join2(projectRoot, "node_modules", ".bin", "vite")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite.cmd")) ? "present" : void 0);
326
+ const vite = pkg.devDependencies?.vite ?? pkg.dependencies?.vite ?? (existsSync2(join2(projectRoot, "node_modules", "vite", "bin", "vite.js")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite")) || existsSync2(join2(projectRoot, "node_modules", ".bin", "vite.cmd")) ? "present" : void 0);
392
327
  if (!vite) {
393
328
  throw new CliError(
394
329
  `No Vite dependency found in ${join2(projectRoot, PACKAGE_JSON)}. LessonKit projects require Vite.`,
@@ -396,28 +331,25 @@ function assertViteProject(pkg, projectRoot) {
396
331
  );
397
332
  }
398
333
  }
399
- function resolveViteBin(projectRoot) {
334
+ function resolveViteJs(projectRoot) {
400
335
  let dir = resolve2(projectRoot);
401
336
  const fsRoot = parse(dir).root;
402
337
  while (true) {
403
- const binDir = join2(dir, "node_modules", ".bin");
404
- const bin = join2(binDir, "vite");
405
- if (existsSync2(bin)) return bin;
406
- const binCmd = join2(binDir, "vite.cmd");
407
- if (existsSync2(binCmd)) return binCmd;
338
+ const viteJs = join2(dir, "node_modules", "vite", "bin", "vite.js");
339
+ if (existsSync2(viteJs)) return viteJs;
408
340
  if (dir === fsRoot) break;
409
341
  dir = dirname2(dir);
410
342
  }
411
343
  throw new CliError(
412
- `Vite binary not found near ${projectRoot}. Run npm install in the project first.`,
344
+ `Vite not found near ${projectRoot}. Run npm install in the project first.`,
413
345
  { code: "INVALID_PROJECT", exitCode: EXIT_INVALID_PROJECT }
414
346
  );
415
347
  }
416
- function assertNode20ForLxpack() {
348
+ function assertNode18ForLxpack() {
417
349
  const major = Number(process.versions.node.split(".")[0]);
418
- if (major < 20) {
350
+ if (major < 18) {
419
351
  throw new CliError(
420
- `LMS packaging requires Node.js 20+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
352
+ `LMS packaging requires Node.js 18+ (current: ${process.versions.node}). See docs/PACKAGING.md.`,
421
353
  { code: "NODE_VERSION", exitCode: EXIT_INVALID_PROJECT }
422
354
  );
423
355
  }
@@ -479,17 +411,19 @@ async function runDev(opts) {
479
411
  const project = await loadProject(opts.cwd ?? process.cwd());
480
412
  const pkg = await readPackageJson(project.root);
481
413
  assertViteProject(pkg, project.root);
482
- const viteBin = resolveViteBin(project.root);
483
- await runCommand(viteBin, opts.viteArgs ?? [], { cwd: project.root });
414
+ const viteJs = resolveViteJs(project.root);
415
+ await runCommand(process.execPath, [viteJs, ...opts.viteArgs ?? []], { cwd: project.root });
484
416
  return { ok: true, command: "dev", projectRoot: project.root };
485
417
  }
486
418
  async function runBuild(opts) {
487
419
  const project = await loadProject(opts.cwd ?? process.cwd());
488
420
  const pkg = await readPackageJson(project.root);
489
421
  assertViteProject(pkg, project.root);
490
- const viteBin = resolveViteBin(project.root);
422
+ const viteJs = resolveViteJs(project.root);
491
423
  const buildArgs = resolveViteBuildArgs(project);
492
- await runCommand(viteBin, [...buildArgs, ...opts.viteArgs ?? []], { cwd: project.root });
424
+ await runCommand(process.execPath, [viteJs, ...buildArgs, ...opts.viteArgs ?? []], {
425
+ cwd: project.root
426
+ });
493
427
  return { ok: true, command: "build", projectRoot: project.root };
494
428
  }
495
429
 
@@ -524,7 +458,7 @@ async function runPackage(opts) {
524
458
  }
525
459
  return { ok: true, target, projectRoot: project.root, distDir };
526
460
  }
527
- assertNode20ForLxpack();
461
+ assertNode18ForLxpack();
528
462
  if (!opts.noBuild || !existsSync3(distDir)) {
529
463
  await runBuild({ cwd: project.root, json: opts.json });
530
464
  }
@@ -598,7 +532,10 @@ async function handleCommand(fn, logger, json) {
598
532
  function createProgram(baseLogger = console) {
599
533
  const program = new Command();
600
534
  program.name("lessonkit").description("LessonKit CLI").version(version);
601
- program.command("init").description("Initialize a LessonKit project from the Vite + React template").argument("[name]", "Project directory name").option("--here", "Initialize in the current directory").option("--skip-install", "Skip npm install").option("--force", "Initialize into a non-empty directory").option("--json", "Emit structured JSON result").action(async (name, opts) => {
535
+ program.command("init").description("Initialize a LessonKit project from the Vite + React template").argument("[name]", "Project directory name").option("--here", "Initialize in the current directory").option("--skip-install", "Skip npm install").option(
536
+ "--force",
537
+ "With --here, allow init when the directory is empty or contains only dotfiles"
538
+ ).option("--json", "Emit structured JSON result").action(async (name, opts) => {
602
539
  const logger = createLogger({ json: opts.json });
603
540
  await handleCommand(
604
541
  () => runInit({ name, here: opts.here, skipInstall: opts.skipInstall, force: opts.force, json: opts.json }, logger),
@@ -632,13 +569,27 @@ function createProgram(baseLogger = console) {
632
569
  program.command("package").description("Build or package for web / LMS delivery").requiredOption("--target <target>", `Export target (${PACKAGE_TARGETS.join(", ")})`).option("--cwd <dir>", "Project root directory").option("--no-build", "Skip implicit Vite build for LMS targets").option("--out <path>", "Override output artifact path").option("--json", "Emit structured JSON result").action(async (opts) => {
633
570
  const logger = createLogger({ json: opts.json });
634
571
  await handleCommand(
635
- () => runPackage({
636
- target: opts.target,
637
- cwd: opts.cwd,
638
- noBuild: opts.build === false,
639
- out: opts.out,
640
- json: opts.json
641
- }),
572
+ async () => {
573
+ const result = await runPackage({
574
+ target: opts.target,
575
+ cwd: opts.cwd,
576
+ noBuild: opts.build === false,
577
+ out: opts.out,
578
+ json: opts.json
579
+ });
580
+ if (!opts.json && result.ok) {
581
+ if (result.target === "react-vite" && "distDir" in result) {
582
+ logger.log(`Built react-vite \u2192 ${result.distDir}`);
583
+ } else if ("outputPath" in result || "outputDir" in result) {
584
+ const dest = result.outputPath ?? result.outputDir;
585
+ const count = "fileCount" in result ? result.fileCount : void 0;
586
+ logger.log(
587
+ `Packaged ${result.target}${dest ? ` \u2192 ${dest}` : ""}${count != null ? ` (${count} files)` : ""}`
588
+ );
589
+ }
590
+ }
591
+ return result;
592
+ },
642
593
  logger,
643
594
  Boolean(opts.json)
644
595
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lessonkit/cli",
3
- "version": "0.9.2",
3
+ "version": "1.0.0",
4
4
  "private": false,
5
5
  "description": "LessonKit CLI — init, dev, build, and package learning experiences.",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,8 @@
42
42
  "lint": "echo \"(no lint configured yet)\""
43
43
  },
44
44
  "dependencies": {
45
- "@lessonkit/lxpack": "0.9.2",
45
+ "@lessonkit/core": "1.0.0",
46
+ "@lessonkit/lxpack": "1.0.0",
46
47
  "commander": "^14.0.1"
47
48
  },
48
49
  "engines": {
@@ -1,19 +1,21 @@
1
- # LessonKit Vite + React template
1
+ # LessonKit starter template
2
2
 
3
- [![Documentation](https://readthedocs.org/projects/lessonkit/badge/?version=latest)](https://lessonkit.readthedocs.io/en/latest/)
4
- [![License](https://img.shields.io/github/license/eddiethedean/lessonkit)](https://github.com/eddiethedean/lessonkit/blob/main/LICENSE)
3
+ Vite + React scaffold for new LessonKit courses. Created by `lessonkit init`.
5
4
 
6
- Starter template copied by `lessonkit init`. See the [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) and [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html).
7
-
8
- ## Run
5
+ ## Commands
9
6
 
10
7
  ```bash
11
8
  npm install
12
- npm run dev
9
+ npm run dev # lessonkit dev
10
+ npm run build # lessonkit build
11
+ npm run package:scorm12
13
12
  ```
14
13
 
15
- ## Notes
14
+ ## Files
15
+
16
+ - `src/App.tsx` — course UI (IDs match `lessonkit.json`)
17
+ - `lessonkit.json` — manifest for CLI and LXPack packaging
18
+
19
+ ## Docs
16
20
 
17
- - Depends on [`@lessonkit/react`](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/components-and-hooks.html).
18
- - Copied by [`@lessonkit/cli`](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) when you run `lessonkit init`.
19
- - Package for an LMS with the [packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html).
21
+ [CLI reference](https://lessonkit.readthedocs.io/en/latest/reference/cli.html) · [React quickstart](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/quickstart.html) · [Packaging guide](https://lessonkit.readthedocs.io/en/latest/guides/react-developers/packaging-and-cli.html)
@@ -15,7 +15,12 @@
15
15
  "passingScore": 1
16
16
  }
17
17
  ],
18
- "theme": { "preset": "default" }
18
+ "theme": { "preset": "default" },
19
+ "tracking": {
20
+ "xapi": {
21
+ "activityIri": "https://example.com/courses/my-course"
22
+ }
23
+ }
19
24
  },
20
25
  "paths": {
21
26
  "spaDistDir": "dist",
@@ -13,16 +13,16 @@
13
13
  "test:coverage": "vitest run --coverage --passWithNoTests=false"
14
14
  },
15
15
  "dependencies": {
16
- "@lessonkit/core": "^0.9.2",
17
- "@lessonkit/react": "^0.9.2",
18
- "@lessonkit/themes": "^0.9.2",
19
- "@lessonkit/xapi": "^0.9.2",
16
+ "@lessonkit/core": "^1.0.0",
17
+ "@lessonkit/react": "^1.0.0",
18
+ "@lessonkit/themes": "^1.0.0",
19
+ "@lessonkit/xapi": "^1.0.0",
20
20
  "react": "^18.3.1",
21
21
  "react-dom": "^18.3.1"
22
22
  },
23
23
  "devDependencies": {
24
- "@lessonkit/cli": "^0.9.2",
25
- "@lessonkit/lxpack": "^0.9.2",
24
+ "@lessonkit/cli": "^1.0.0",
25
+ "@lessonkit/lxpack": "^1.0.0",
26
26
  "@testing-library/react": "^16.3.0",
27
27
  "@types/react": "^18.3.23",
28
28
  "@types/react-dom": "^18.3.7",
@@ -1 +0,0 @@
1
- html,body{height:100%}body{margin:0;background:var(--lk-color-background);color:var(--lk-color-foreground);font-family:var(--lk-font-family);font-size:var(--lk-font-size-base);line-height:var(--lk-line-height-base)}.app-shell{margin:0 auto;padding:var(--lk-space-xl) var(--lk-space-lg);max-width:720px}section,article{border:1px solid var(--lk-color-border);border-radius:var(--lk-radius-lg);padding:var(--lk-space-lg);margin:var(--lk-space-md) 0;background:var(--lk-color-panel);box-shadow:var(--lk-shadow-md)}h1,h2{margin:0 0 var(--lk-space-sm);font-weight:var(--lk-font-weight-strong)}button{border:1px solid var(--lk-color-border);background:var(--lk-color-panel);color:var(--lk-color-foreground);border-radius:var(--lk-radius-md);padding:var(--lk-space-sm) var(--lk-space-md);font-weight:var(--lk-font-weight-strong);cursor:pointer}input[type=radio]{accent-color:var(--lk-color-primary)}label{display:block;margin:var(--lk-space-xs) 0}