@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.
- package/README.md +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lb-mailto.browser.d.ts +2 -0
- package/dist/lb-mailto.browser.d.ts.map +1 -0
- package/dist/lb-mailto.browser.js +29 -0
- package/dist/lb-mailto.browser.js.map +1 -0
- package/dist/lb-mailto.html +6 -0
- package/dist/lb-tel.browser.d.ts +2 -0
- package/dist/lb-tel.browser.d.ts.map +1 -0
- package/dist/lb-tel.browser.js +40 -0
- package/dist/lb-tel.browser.js.map +1 -0
- package/dist/lb-tel.html +6 -0
- package/dist-cli/package-root.js +24 -0
- package/dist-cli/skills-cli.js +87 -0
- package/dist-cli/skills.js +125 -0
- package/docs/lb-checkbox.md +2 -2
- package/docs/lb-form.md +2 -2
- package/docs/lb-input.md +4 -2
- package/docs/lb-mailto.md +29 -0
- package/docs/lb-master.md +2 -2
- package/docs/lb-options.md +2 -0
- package/docs/lb-select.md +2 -1
- package/docs/lb-table.md +4 -3
- package/docs/lb-tel.md +32 -0
- package/docs/lb-unknown-page.md +2 -2
- package/package.json +12 -4
- package/skills/loadbare-widgets/SKILL.md +61 -0
- package/skills/loadbare-widgets/references/lb-checkbox.md +63 -0
- package/skills/loadbare-widgets/references/lb-confirm.md +44 -0
- package/skills/loadbare-widgets/references/lb-date.md +48 -0
- package/skills/loadbare-widgets/references/lb-form.md +35 -0
- package/skills/loadbare-widgets/references/lb-input.md +77 -0
- package/skills/loadbare-widgets/references/lb-insert-dialog.md +73 -0
- package/skills/loadbare-widgets/references/lb-mailto.md +29 -0
- package/skills/loadbare-widgets/references/lb-master.md +65 -0
- package/skills/loadbare-widgets/references/lb-number.md +53 -0
- package/skills/loadbare-widgets/references/lb-options.md +113 -0
- package/skills/loadbare-widgets/references/lb-picker.md +53 -0
- package/skills/loadbare-widgets/references/lb-select.md +70 -0
- package/skills/loadbare-widgets/references/lb-table.md +173 -0
- package/skills/loadbare-widgets/references/lb-tel.md +32 -0
- package/skills/loadbare-widgets/references/lb-unknown-page.md +43 -0
- 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
package/dist/index.d.ts.map
CHANGED
|
@@ -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
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 @@
|
|
|
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 @@
|
|
|
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"]}
|
package/dist/lb-tel.html
ADDED
|
@@ -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
|
+
}
|
package/docs/lb-checkbox.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/docs/lb-options.md
CHANGED
|
@@ -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
|
|
76
|
-
|
|
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
|
|
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.
|
package/docs/lb-unknown-page.md
CHANGED
|
@@ -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>`.
|
|
43
|
-
|
|
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.
|
|
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
|
-
"
|
|
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 .",
|