@loadbare/widgets 0.10.0 → 0.11.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.
Files changed (46) hide show
  1. package/README.md +19 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/lb-mailto.browser.d.ts +2 -0
  7. package/dist/lb-mailto.browser.d.ts.map +1 -0
  8. package/dist/lb-mailto.browser.js +29 -0
  9. package/dist/lb-mailto.browser.js.map +1 -0
  10. package/dist/lb-mailto.html +6 -0
  11. package/dist/lb-tel.browser.d.ts +2 -0
  12. package/dist/lb-tel.browser.d.ts.map +1 -0
  13. package/dist/lb-tel.browser.js +40 -0
  14. package/dist/lb-tel.browser.js.map +1 -0
  15. package/dist/lb-tel.html +6 -0
  16. package/dist-cli/package-root.js +24 -0
  17. package/dist-cli/skills-cli.js +87 -0
  18. package/dist-cli/skills.js +125 -0
  19. package/docs/lb-checkbox.md +2 -2
  20. package/docs/lb-form.md +2 -2
  21. package/docs/lb-input.md +4 -2
  22. package/docs/lb-mailto.md +29 -0
  23. package/docs/lb-master.md +2 -2
  24. package/docs/lb-options.md +2 -0
  25. package/docs/lb-select.md +2 -1
  26. package/docs/lb-table.md +4 -3
  27. package/docs/lb-tel.md +32 -0
  28. package/docs/lb-unknown-page.md +2 -2
  29. package/package.json +12 -4
  30. package/skills/loadbare-widgets/SKILL.md +61 -0
  31. package/skills/loadbare-widgets/references/lb-checkbox.md +63 -0
  32. package/skills/loadbare-widgets/references/lb-confirm.md +44 -0
  33. package/skills/loadbare-widgets/references/lb-date.md +48 -0
  34. package/skills/loadbare-widgets/references/lb-form.md +35 -0
  35. package/skills/loadbare-widgets/references/lb-input.md +77 -0
  36. package/skills/loadbare-widgets/references/lb-insert-dialog.md +73 -0
  37. package/skills/loadbare-widgets/references/lb-mailto.md +29 -0
  38. package/skills/loadbare-widgets/references/lb-master.md +65 -0
  39. package/skills/loadbare-widgets/references/lb-number.md +53 -0
  40. package/skills/loadbare-widgets/references/lb-options.md +113 -0
  41. package/skills/loadbare-widgets/references/lb-picker.md +53 -0
  42. package/skills/loadbare-widgets/references/lb-select.md +70 -0
  43. package/skills/loadbare-widgets/references/lb-table.md +173 -0
  44. package/skills/loadbare-widgets/references/lb-tel.md +32 -0
  45. package/skills/loadbare-widgets/references/lb-unknown-page.md +43 -0
  46. package/docs/temp-feature-request-lb-table.md +0 -130
package/README.md CHANGED
@@ -19,6 +19,8 @@ Inventory:
19
19
  | [`lb-checkbox`](docs/lb-checkbox.md) | a checkbox and its label, as markup to style |
20
20
  | [`lb-number`](docs/lb-number.md) | a number shown by `Intl.NumberFormat` |
21
21
  | [`lb-date`](docs/lb-date.md) | a date or time shown by `Intl.DateTimeFormat` |
22
+ | [`lb-tel`](docs/lb-tel.md) | a phone number shown as a `tel:` link |
23
+ | [`lb-mailto`](docs/lb-mailto.md) | an email address shown as a `mailto:` link |
22
24
 
23
25
  ## Installing
24
26
 
@@ -39,3 +41,20 @@ Apart from the line in `src/imports.ts`, no other import statements
39
41
  are required. The loadbare builder automatically bundles the HTML and
40
42
  JS for any widget in the library that is actually used in the app.
41
43
 
44
+ ## Agent skills
45
+
46
+ The package ships an agent skill, `loadbare-widgets`, written for a coding agent working
47
+ in a project that depends on `@loadbare/widgets`. Install it with:
48
+
49
+ ```bash
50
+ npx loadbare-widgets skills install
51
+ ```
52
+
53
+ It links the skill into `.agents/skills/` and `.claude/skills/`, where agents
54
+ look for skills. `--copy` writes copies instead of links, and `--dir <path>`
55
+ installs into one directory of your choosing. The command prints the paths to
56
+ add to `.gitignore` and writes nothing there itself.
57
+
58
+ `npx loadbare-app skills install`, `npx loadbare-widgets skills install` and
59
+ `npx loadbare-db skills install` install the three loadbare skills, one per
60
+ package.
package/dist/index.d.ts CHANGED
@@ -7,4 +7,6 @@ import "./lb-confirm.browser.js";
7
7
  import "./lb-insert-dialog.browser.js";
8
8
  import "./lb-number.browser.js";
9
9
  import "./lb-date.browser.js";
10
+ import "./lb-tel.browser.js";
11
+ import "./lb-mailto.browser.js";
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,CAAC;AAC9B,OAAO,qBAAqB,CAAC;AAC7B,OAAO,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -7,4 +7,6 @@ import "./lb-confirm.browser.js";
7
7
  import "./lb-insert-dialog.browser.js";
8
8
  import "./lb-number.browser.js";
9
9
  import "./lb-date.browser.js";
10
+ import "./lb-tel.browser.js";
11
+ import "./lb-mailto.browser.js";
10
12
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,CAAC","sourcesContent":["import \"./lb-input.browser.js\";\nimport \"./lb-select.browser.js\";\nimport \"./lb-options.browser.js\";\nimport \"./lb-table.browser.js\";\nimport \"./lb-picker.browser.js\";\nimport \"./lb-confirm.browser.js\";\nimport \"./lb-insert-dialog.browser.js\";\nimport \"./lb-number.browser.js\";\nimport \"./lb-date.browser.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../widgets/index.ts"],"names":[],"mappings":"AAAA,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,yBAAyB,CAAC;AACjC,OAAO,+BAA+B,CAAC;AACvC,OAAO,wBAAwB,CAAC;AAChC,OAAO,sBAAsB,CAAC;AAC9B,OAAO,qBAAqB,CAAC;AAC7B,OAAO,wBAAwB,CAAC","sourcesContent":["import \"./lb-input.browser.js\";\nimport \"./lb-select.browser.js\";\nimport \"./lb-options.browser.js\";\nimport \"./lb-table.browser.js\";\nimport \"./lb-picker.browser.js\";\nimport \"./lb-confirm.browser.js\";\nimport \"./lb-insert-dialog.browser.js\";\nimport \"./lb-number.browser.js\";\nimport \"./lb-date.browser.js\";\nimport \"./lb-tel.browser.js\";\nimport \"./lb-mailto.browser.js\";\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lb-mailto.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-mailto.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-mailto.browser.ts"],"names":[],"mappings":""}
@@ -0,0 +1,29 @@
1
+ /// <reference lib="dom" />
2
+ import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
3
+ /**
4
+ * Shows the value that landed on it as written, in the `<a>` its definition
5
+ * holds, with a `mailto:` `href` that writes to it. Not a control: the hub
6
+ * stamps `lb-column-value` on the tag and never gathers it.
7
+ *
8
+ * The address is encoded into the `href`, so a value carrying `?` or `&`
9
+ * cannot add a subject, a body or another recipient to the message. A value
10
+ * with no `@` shows as it arrived, with no `href`.
11
+ */
12
+ class LbMailto extends HTMLElement {
13
+ static observedAttributes = [ATTR_COLUMN_VALUE];
14
+ attributeChangedCallback(_name, _old, value) {
15
+ const a = this.querySelector("a");
16
+ if (!a)
17
+ return;
18
+ a.textContent = value ?? "";
19
+ const address = value?.trim();
20
+ if (address?.includes("@")) {
21
+ a.setAttribute("href", `mailto:${encodeURIComponent(address).replace(/%40/g, "@")}`);
22
+ }
23
+ else {
24
+ a.removeAttribute("href");
25
+ }
26
+ }
27
+ }
28
+ customElements.define("lb-mailto", LbMailto);
29
+ //# sourceMappingURL=lb-mailto.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-mailto.browser.js","sourceRoot":"","sources":["../widgets/lb-mailto.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,QAAS,SAAQ,WAAW;IAChC,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEhD,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,WAAW,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;QAC9B,IAAI,OAAO,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,CAAC,CAAC,YAAY,CACZ,MAAM,EACN,UAAU,kBAAkB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAC7D,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;;AAGH,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/**\n * Shows the value that landed on it as written, in the `<a>` its definition\n * holds, with a `mailto:` `href` that writes to it. Not a control: the hub\n * stamps `lb-column-value` on the tag and never gathers it.\n *\n * The address is encoded into the `href`, so a value carrying `?` or `&`\n * cannot add a subject, a body or another recipient to the message. A value\n * with no `@` shows as it arrived, with no `href`.\n */\nclass LbMailto extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const a = this.querySelector(\"a\");\n if (!a) return;\n a.textContent = value ?? \"\";\n const address = value?.trim();\n if (address?.includes(\"@\")) {\n a.setAttribute(\n \"href\",\n `mailto:${encodeURIComponent(address).replace(/%40/g, \"@\")}`,\n );\n } else {\n a.removeAttribute(\"href\");\n }\n }\n}\n\ncustomElements.define(\"lb-mailto\", LbMailto);\n"]}
@@ -0,0 +1,6 @@
1
+ <!--
2
+ An email address as a mailto: link. The script writes the href from the
3
+ value, so the definition is an <a> with none: until a value lands it is
4
+ not a link.
5
+ -->
6
+ <a></a>
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lb-tel.browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-tel.browser.d.ts","sourceRoot":"","sources":["../widgets/lb-tel.browser.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ /// <reference lib="dom" />
2
+ import { ATTR_COLUMN_VALUE } from "@loadbare/app/constants";
3
+ /**
4
+ * Shows the value that landed on it as written, in the `<a>` its definition
5
+ * holds, with a `tel:` `href` that dials it. Not a control: the hub stamps
6
+ * `lb-column-value` on the tag and never gathers it.
7
+ *
8
+ * The `href` keeps a leading `+` and the digits, and drops the rest, so
9
+ * `(555) 123-4567` dials as `5551234567`. An extension written after `x`,
10
+ * `ext` or `extension` becomes `;ext=`, rather than digits dialed on the end
11
+ * of the number. A value with no digits shows as it arrived, with no `href`.
12
+ */
13
+ class LbTel extends HTMLElement {
14
+ static observedAttributes = [ATTR_COLUMN_VALUE];
15
+ attributeChangedCallback(_name, _old, value) {
16
+ const a = this.querySelector("a");
17
+ if (!a)
18
+ return;
19
+ a.textContent = value ?? "";
20
+ const href = value === null ? null : tel(value);
21
+ if (href)
22
+ a.setAttribute("href", href);
23
+ else
24
+ a.removeAttribute("href");
25
+ }
26
+ }
27
+ /** The `tel:` URI for a number as a person writes it, null for no number. */
28
+ function tel(value) {
29
+ const [number, ext] = value.split(EXTENSION, 2);
30
+ const digits = number.replace(/\D/g, "");
31
+ if (!digits)
32
+ return null;
33
+ const plus = number.trim().startsWith("+") ? "+" : "";
34
+ const extDigits = ext?.replace(/\D/g, "");
35
+ return `tel:${plus}${digits}${extDigits ? `;ext=${extDigits}` : ""}`;
36
+ }
37
+ /** Where a number ends and its extension begins. */
38
+ const EXTENSION = /\s*(?:extension|ext\.?|x)\s*(?=\d)/i;
39
+ customElements.define("lb-tel", LbTel);
40
+ //# sourceMappingURL=lb-tel.browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lb-tel.browser.js","sourceRoot":"","sources":["../widgets/lb-tel.browser.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAE3B,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;GASG;AACH,MAAM,KAAM,SAAQ,WAAW;IAC7B,MAAM,CAAC,kBAAkB,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAEhD,wBAAwB,CACtB,KAAa,EACb,IAAmB,EACnB,KAAoB;QAEpB,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,CAAC;YAAE,OAAO;QACf,CAAC,CAAC,WAAW,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChD,IAAI,IAAI;YAAE,CAAC,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;YAClC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;;AAGH,6EAA6E;AAC7E,SAAS,GAAG,CAAC,KAAa;IACxB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACtD,MAAM,SAAS,GAAG,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1C,OAAO,OAAO,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACvE,CAAC;AAED,oDAAoD;AACpD,MAAM,SAAS,GAAG,qCAAqC,CAAC;AAExD,cAAc,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC","sourcesContent":["/// <reference lib=\"dom\" />\n\nimport { ATTR_COLUMN_VALUE } from \"@loadbare/app/constants\";\n\n/**\n * Shows the value that landed on it as written, in the `<a>` its definition\n * holds, with a `tel:` `href` that dials it. Not a control: the hub stamps\n * `lb-column-value` on the tag and never gathers it.\n *\n * The `href` keeps a leading `+` and the digits, and drops the rest, so\n * `(555) 123-4567` dials as `5551234567`. An extension written after `x`,\n * `ext` or `extension` becomes `;ext=`, rather than digits dialed on the end\n * of the number. A value with no digits shows as it arrived, with no `href`.\n */\nclass LbTel extends HTMLElement {\n static observedAttributes = [ATTR_COLUMN_VALUE];\n\n attributeChangedCallback(\n _name: string,\n _old: string | null,\n value: string | null,\n ): void {\n const a = this.querySelector(\"a\");\n if (!a) return;\n a.textContent = value ?? \"\";\n const href = value === null ? null : tel(value);\n if (href) a.setAttribute(\"href\", href);\n else a.removeAttribute(\"href\");\n }\n}\n\n/** The `tel:` URI for a number as a person writes it, null for no number. */\nfunction tel(value: string): string | null {\n const [number, ext] = value.split(EXTENSION, 2);\n const digits = number.replace(/\\D/g, \"\");\n if (!digits) return null;\n const plus = number.trim().startsWith(\"+\") ? \"+\" : \"\";\n const extDigits = ext?.replace(/\\D/g, \"\");\n return `tel:${plus}${digits}${extDigits ? `;ext=${extDigits}` : \"\"}`;\n}\n\n/** Where a number ends and its extension begins. */\nconst EXTENSION = /\\s*(?:extension|ext\\.?|x)\\s*(?=\\d)/i;\n\ncustomElements.define(\"lb-tel\", LbTel);\n"]}
@@ -0,0 +1,6 @@
1
+ <!--
2
+ A phone number as a tel: link. The script writes the href from the value,
3
+ so the definition is an <a> with none: until a value lands it is not a
4
+ link.
5
+ -->
6
+ <a></a>
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Walk up from `dir` to the nearest ancestor holding a package.json.
3
+ *
4
+ * Needed because a caller may run from source (cli/*.ts) or compiled
5
+ * (dist-cli/*.js) — both are the same distance from the package root, but
6
+ * nothing here should assume that stays true.
7
+ */
8
+ import { readFile } from "node:fs/promises";
9
+ import path from "node:path";
10
+ export async function findPackageRoot(dir) {
11
+ for (;;) {
12
+ try {
13
+ await readFile(path.join(dir, "package.json"), "utf-8");
14
+ return dir;
15
+ }
16
+ catch {
17
+ const parent = path.dirname(dir);
18
+ if (parent === dir) {
19
+ throw new Error(`could not find a package.json above '${dir}'`);
20
+ }
21
+ dir = parent;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * loadbare-widgets — the package's one command.
4
+ *
5
+ * loadbare-widgets skills install [--copy] [--dir <path>]
6
+ *
7
+ * Installs the agent skills that ship in this package into `.agents/skills`
8
+ * and `.claude/skills`, or into `--dir` alone — see cli/skills.ts. Nothing
9
+ * goes to stdout but the usage `--help` asks for; every other line is
10
+ * progress, on stderr.
11
+ */
12
+ import { parseArgs } from "node:util";
13
+ import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+ import { findPackageRoot } from "./package-root.js";
16
+ import { installSkills, SKILL_DESTINATIONS } from "./skills.js";
17
+ const USAGE = "usage: loadbare-widgets skills install [--copy] [--dir <path>]";
18
+ async function main() {
19
+ let parsed;
20
+ try {
21
+ parsed = parseArgs({
22
+ allowPositionals: true,
23
+ options: {
24
+ copy: { type: "boolean", default: false },
25
+ dir: { type: "string" },
26
+ help: { type: "boolean", short: "h", default: false },
27
+ },
28
+ });
29
+ }
30
+ catch (err) {
31
+ console.error(`loadbare-widgets: ${err.message}`);
32
+ console.error(USAGE);
33
+ return 1;
34
+ }
35
+ const { values, positionals } = parsed;
36
+ // Asked for, the usage is the product, so it alone goes to stdout.
37
+ if (values.help) {
38
+ console.log(USAGE);
39
+ return 0;
40
+ }
41
+ if (positionals.join(" ") !== "skills install") {
42
+ console.error(USAGE);
43
+ return 1;
44
+ }
45
+ // Source and compiled output sit at different depths, so the package is
46
+ // found by walking up rather than by counting.
47
+ const packageRoot = await findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
48
+ const source = path.join(packageRoot, "skills");
49
+ const destinations = values.dir ? [values.dir] : SKILL_DESTINATIONS;
50
+ const result = installSkills({
51
+ source,
52
+ destinations,
53
+ copy: values.copy,
54
+ cwd: process.cwd(),
55
+ log: (line) => console.error(line),
56
+ });
57
+ // An install with no skills in it is a packaging fault, not a user error,
58
+ // and saying where it looked is what makes that diagnosable.
59
+ if (result.names.length === 0) {
60
+ console.error(`loadbare-widgets: this install of @loadbare/widgets carries no skills (looked in ${source})`);
61
+ return 1;
62
+ }
63
+ console.error("");
64
+ if (result.copiedInstead) {
65
+ console.error("Some links could not be created and those skills were copied instead.");
66
+ console.error("A copy does not follow the package when it is upgraded — rerun this");
67
+ console.error("command after an upgrade, or use --copy everywhere and commit the result.");
68
+ console.error("");
69
+ }
70
+ if (values.copy) {
71
+ console.error("These are copies. Commit them, and rerun this command after upgrading");
72
+ console.error("@loadbare/widgets so the skill matches the widgets.");
73
+ }
74
+ else {
75
+ console.error("These are links into node_modules. To keep them out of git:");
76
+ console.error("");
77
+ for (const destination of destinations) {
78
+ for (const name of result.names) {
79
+ console.error(` ${path.join(destination, name)}`);
80
+ }
81
+ }
82
+ }
83
+ console.error("");
84
+ console.error("Start your agent from this directory and it will find them.");
85
+ return result.refused > 0 ? 1 : 0;
86
+ }
87
+ process.exitCode = await main();
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Put the agent skills that ship in this package where a coding agent will
3
+ * find them.
4
+ *
5
+ * The same command as `loadbare-db skills install` and `loadbare-app skills
6
+ * install`, and deliberately a copy of them rather than a shared module:
7
+ * this package reaches `@loadbare/app` only through `constants` and `types`,
8
+ * and a skill installer is neither. The copies are kept identical, so a
9
+ * change to one belongs in all three.
10
+ *
11
+ * A symlink by default, because the point is that the skill tracks the
12
+ * dependency: `npm install @loadbare/widgets@next` and the skill moves with it.
13
+ * `copy` is for a team that would rather commit the text than have every
14
+ * clone depend on `node_modules` being populated.
15
+ *
16
+ * Nothing is done to `.gitignore`. Whether these paths are committed is a
17
+ * decision about the project, and the command that installs a file is not the
18
+ * one to make it.
19
+ */
20
+ import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, } from "node:fs";
21
+ import path from "node:path";
22
+ /**
23
+ * Where a coding agent looks for skills.
24
+ *
25
+ * `.agents/skills` is the vendor-neutral location, read by Codex, Cursor and
26
+ * Copilot; `.claude/skills` is Claude Code's, and Cursor reads it too. Both
27
+ * are written rather than guessing from what happens to be configured.
28
+ */
29
+ export const SKILL_DESTINATIONS = [".agents/skills", ".claude/skills"];
30
+ /** `absent` covers a dangling symlink, which `existsSync` reports as missing. */
31
+ function entryKind(p) {
32
+ let stat;
33
+ try {
34
+ stat = lstatSync(p);
35
+ }
36
+ catch {
37
+ return "absent";
38
+ }
39
+ if (stat.isSymbolicLink())
40
+ return "symlink";
41
+ if (stat.isDirectory())
42
+ return "directory";
43
+ return "other";
44
+ }
45
+ /** The `name:` a skill directory declares, or null if there is no skill there. */
46
+ function declaredSkillName(dir) {
47
+ try {
48
+ const match = /^name:\s*(\S+)\s*$/m.exec(readFileSync(path.join(dir, "SKILL.md"), "utf-8"));
49
+ return match?.[1] ?? null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * A link into `node_modules` is relative, so the project stays movable and the
57
+ * link means the same thing to everyone who checks it out. A globally
58
+ * installed package gets an absolute path, since a relative one breaks the
59
+ * moment the project moves.
60
+ */
61
+ function linkTarget(from, to, cwd) {
62
+ const inside = path.resolve(from).startsWith(path.resolve(cwd) + path.sep);
63
+ return inside
64
+ ? path.relative(path.dirname(path.resolve(cwd, to)), path.resolve(from))
65
+ : path.resolve(from);
66
+ }
67
+ /** The skills under `source`: each directory holding a `SKILL.md`. */
68
+ export function shippedSkills(source) {
69
+ if (!existsSync(source))
70
+ return [];
71
+ return readdirSync(source, { withFileTypes: true })
72
+ .filter((entry) => entry.isDirectory() &&
73
+ existsSync(path.join(source, entry.name, "SKILL.md")))
74
+ .map((entry) => entry.name)
75
+ .sort();
76
+ }
77
+ export function installSkills(options) {
78
+ const { source, destinations, copy, cwd, log } = options;
79
+ const names = shippedSkills(source);
80
+ let refused = 0;
81
+ let copiedInstead = false;
82
+ for (const destination of destinations) {
83
+ for (const name of names) {
84
+ const from = path.join(source, name);
85
+ const to = path.join(destination, name);
86
+ const at = path.resolve(cwd, to);
87
+ const kind = entryKind(at);
88
+ // A directory this command wrote says so on its own front matter, and
89
+ // is replaced. Anything else at that path belongs to someone, and a
90
+ // skill install is not permitted to be the thing that lost it.
91
+ if (kind === "directory" && declaredSkillName(at) !== name) {
92
+ log(` refused ${to} — a directory is already there and is not the ${name} skill`);
93
+ refused++;
94
+ continue;
95
+ }
96
+ if (kind === "other") {
97
+ log(` refused ${to} — a file is already there`);
98
+ refused++;
99
+ continue;
100
+ }
101
+ mkdirSync(path.resolve(cwd, destination), { recursive: true });
102
+ rmSync(at, { recursive: true, force: true });
103
+ if (copy) {
104
+ cpSync(from, at, { recursive: true });
105
+ log(` copied ${to}`);
106
+ continue;
107
+ }
108
+ try {
109
+ symlinkSync(linkTarget(from, to, cwd), at, "dir");
110
+ log(` linked ${to}`);
111
+ }
112
+ catch (error) {
113
+ // Windows refuses a symlink to an unprivileged process unless
114
+ // developer mode is on. A copy is the same skill, so the install
115
+ // succeeds and the difference is reported rather than raised.
116
+ if (error.code !== "EPERM")
117
+ throw error;
118
+ cpSync(from, at, { recursive: true });
119
+ copiedInstead = true;
120
+ log(` copied ${to}`);
121
+ }
122
+ }
123
+ }
124
+ return { names, refused, copiedInstead };
125
+ }
@@ -46,8 +46,8 @@ for now.
46
46
  ## The control
47
47
 
48
48
  The checkbox is checked when its column is anything but `false` or null, and
49
- sends `"true"` or `"false"`. See
50
- [checkboxes](../../app/docs/reference/data-binding.md#checkboxes).
49
+ sends `"true"` or `"false"`. See "Checkboxes" in the data-binding reference
50
+ of the loadbare-app skill.
51
51
 
52
52
  ## Styling
53
53
 
package/docs/lb-form.md CHANGED
@@ -26,8 +26,8 @@ Requires `exp-id`.
26
26
  A row with a key is on record, and each control saves its own column on
27
27
  `change` with `lb-request="lb-row-update"`. A row whose key is empty is a new
28
28
  one not yet entered: the hub sends no update for it, so the controls only
29
- hold what a submit will gather. See
30
- [a row not yet entered](../../app/docs/reference/data-binding.md#a-row-not-yet-entered).
29
+ hold what a submit will gather. See "A row not yet entered" in the
30
+ data-binding reference of the loadbare-app skill.
31
31
 
32
32
  A definition only, with no script of its own.
33
33
 
package/docs/lb-input.md CHANGED
@@ -43,13 +43,15 @@ off drops the attribute.
43
43
  `lb-input` is a form-associated custom element. Its `value` property gets and
44
44
  sets the value of the `<input>` inside it, and it fires `change` on itself when
45
45
  that `<input>` fires `change`. A form reset returns the `<input>` to its
46
- default.
46
+ default. A refusal's message for its column goes to the `<input>`, as HTML's
47
+ own validity.
47
48
 
48
49
  ## Inside a form
49
50
 
50
51
  An `lb-input` inside an `lb-row-insert` or `lb-row-update` form carries no
51
52
  `lb-request`. The hub gathers every `lb-column` whose form owner is the form
52
- on submit and sends one request for all of them.
53
+ on submit and sends one request for all of them, so a widget that also sent
54
+ its own would write the same edit twice.
53
55
 
54
56
  ```html
55
57
  <form lb-query="people" lb-request="lb-row-insert">
@@ -0,0 +1,29 @@
1
+ # `lb-mailto`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ An email address shown as written, as a `mailto:` link. Not a control:
6
+ write `lb-column` on the tag, and the hub lands the column on it and never
7
+ gathers it.
8
+
9
+ ```html
10
+ <lb-mailto lb-column="email"></lb-mailto>
11
+ ```
12
+
13
+ It takes no parameters. To edit the address, use
14
+ `<lb-input exp-type="email">`.
15
+
16
+ ## The value
17
+
18
+ The link's text is the value as it arrived. Its `href` is `mailto:` and the
19
+ address, encoded, so a value carrying `?` or `&` cannot add a subject, a body
20
+ or another recipient to the message.
21
+
22
+ A value with no `@` shows as it arrived, with no `href`. Null shows
23
+ nothing.
24
+
25
+ ## Styling
26
+
27
+ The definition is an `<a>`, with no `href` while there is no address, so
28
+ `lb-mailto a:not([href])` selects a value that is no link. The hub stamps
29
+ the tag with `lb-column-value`, the value as it arrived.
package/docs/lb-master.md CHANGED
@@ -46,8 +46,8 @@ Requires `exp-row`.
46
46
  | Cancel | A new row | Resets the form |
47
47
 
48
48
  Each carries `data-mode`, `on-record` or `new`, and the stylesheet shows each
49
- only in its mode. A new row is one whose key is empty, a
50
- [row not yet entered](../../app/docs/reference/data-binding.md#a-row-not-yet-entered).
49
+ only in its mode. A new row is one whose key is empty, a row not yet entered
50
+ (see that section in the data-binding reference of the loadbare-app skill).
51
51
 
52
52
  Which row shows is a query parm, and only the page knows its name, so New is
53
53
  a request the page declares. Its handler empties the parm, and the row
@@ -80,6 +80,8 @@ and `disabled` so it is never a choice.
80
80
  `lb-options` is a form-associated custom element. Its `value` property gets
81
81
  and sets the value of the `<select>` inside it, and it fires `change` on itself
82
82
  when that `<select>` fires `change`. A form reset selects the first option.
83
+ A refusal's message for its column goes to the `<select>`, as HTML's own
84
+ validity.
83
85
 
84
86
  The hub reads the element's own `lb-column` from the row around it, and lands
85
87
  the rows of its `lb-query` inside it. A value set before the option it names
package/docs/lb-select.md CHANGED
@@ -37,7 +37,8 @@ improvements through iterative use.
37
37
  `lb-select` is a form-associated custom element. Its `value` property gets
38
38
  and sets the value of the `<select>` inside it, and it fires `change` on itself
39
39
  when that `<select>` fires `change`. A form reset selects the option marked
40
- `selected`, or the first option.
40
+ `selected`, or the first option. A refusal's message for its column goes to
41
+ the `<select>`, as HTML's own validity.
41
42
 
42
43
  ## Choosing
43
44
 
package/docs/lb-table.md CHANGED
@@ -72,8 +72,9 @@ filter does not pass is hidden.
72
72
  ## The order on the heading
73
73
 
74
74
  The heading of the column the rows are sorted by carries a triangle, up for
75
- ascending and down for descending, and `aria-sort`. Only the order's
76
- leading term is shown. A heading's column is the `lb-column` in the cell
75
+ ascending and down for descending, and `aria-sort`, read from the
76
+ `lb-query-order` the hub stamps on the table. Only the order's leading term
77
+ is shown. A heading's column is the `lb-column` in the cell
77
78
  at the same position in the row template, on the cell or inside it. A cell
78
79
  naming no column, or several, gives its heading none.
79
80
 
@@ -160,7 +161,7 @@ created, or moved by changing a column the order reads.
160
161
 
161
162
  The footer is an ordinary part of the page, not a row from the table's query.
162
163
  An aggregate there covers every row showing. A figure that is not a sum of the rows,
163
- such as a budget, is an element with its own `lb-query`, a query of kind
164
+ such as a budget, is a `<tr>` with its own `lb-query`, a query of kind
164
165
  `row` declared on the server like any other.
165
166
 
166
167
  ## Styling an empty table
package/docs/lb-tel.md ADDED
@@ -0,0 +1,32 @@
1
+ # `lb-tel`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A phone number shown as written, as a `tel:` link. Not a control: write
6
+ `lb-column` on the tag, and the hub lands the column on it and never gathers
7
+ it.
8
+
9
+ ```html
10
+ <lb-tel lb-column="phone"></lb-tel>
11
+ ```
12
+
13
+ It takes no parameters. To edit the number, use
14
+ `<lb-input exp-type="tel">`.
15
+
16
+ ## The value
17
+
18
+ The link's text is the value as it arrived. Its `href` keeps a leading `+`
19
+ and the digits, and drops everything else, so `(555) 123-4567` dials
20
+ `tel:5551234567`.
21
+
22
+ An extension written after `x`, `ext`, `ext.` or `extension` goes in the
23
+ `href` as `;ext=`, so `555-123-4567 x89` dials `tel:5551234567;ext=89`.
24
+
25
+ A value with no digits shows as it arrived, with no `href`. Null shows
26
+ nothing.
27
+
28
+ ## Styling
29
+
30
+ The definition is an `<a>`, with no `href` while there is nothing to dial,
31
+ so `lb-tel a:not([href])` selects a number that is no link. The hub stamps
32
+ the tag with `lb-column-value`, the value as it arrived.
@@ -39,5 +39,5 @@ there is no page label to show.
39
39
  ## Inside the hub
40
40
 
41
41
  The hub finds the dialog inside its own subtree and nowhere else, so the tag
42
- goes inside `<lb-hub>`. See [`chrome.html`](../../app/docs/reference/chrome.md)
43
- for the same dialog written by hand.
42
+ goes inside `<lb-hub>`. The chrome reference of the loadbare-app skill shows
43
+ the same dialog written by hand in `chrome.html`.
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@loadbare/widgets",
3
- "description": "The basic widget set for @loadbare/app: input, select, options, picker, table, unknown-page, confirm, form, insert-dialog, master, checkbox, number, date",
4
- "version": "0.10.0",
3
+ "description": "The basic widget set for @loadbare/app: input, select, options, picker, table, unknown-page, confirm, form, insert-dialog, master, checkbox, number, date, tel, mailto",
4
+ "version": "0.11.0",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
8
+ "dist-cli",
8
9
  "docs",
10
+ "skills/loadbare-widgets/SKILL.md",
11
+ "skills/loadbare-widgets/references",
9
12
  "README.md"
10
13
  ],
11
14
  "publishConfig": {
@@ -17,12 +20,17 @@
17
20
  "default": "./dist/index.js"
18
21
  }
19
22
  },
23
+ "bin": {
24
+ "loadbare-widgets": "dist-cli/skills-cli.js"
25
+ },
20
26
  "loadbare": {
21
27
  "widgets": "./dist"
22
28
  },
23
29
  "scripts": {
24
- "build": "node scripts/build.mjs",
25
- "postbuild": "node ../../scripts/check-dist-imports.mjs",
30
+ "build": "node scripts/build.mjs && npm run build:cli && npm run build:skills",
31
+ "build:cli": "rm -rf dist-cli && tsc --project tsconfig.cli.json",
32
+ "build:skills": "node scripts/build-skills.mjs",
33
+ "postbuild": "node --eval \"fs.chmodSync('dist-cli/skills-cli.js', 0o755)\" --input-type=module && node ../../scripts/check-dist-imports.mjs",
26
34
  "test": "tsx --test \"tests/**/*.test.ts\"",
27
35
  "typecheck": "tsc --noEmit",
28
36
  "format": "prettier --write .",