@loadbare/widgets 0.10.1 → 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 CHANGED
@@ -41,3 +41,20 @@ Apart from the line in `src/imports.ts`, no other import statements
41
41
  are required. The loadbare builder automatically bundles the HTML and
42
42
  JS for any widget in the library that is actually used in the app.
43
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.
@@ -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">
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
@@ -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
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.10.1",
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 .",
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: loadbare-widgets
3
+ description: The basic widget set for Loadbare/app, shipped as `@loadbare/widgets`. Covers each widget's parameters, the attributes it reads, and what a change sends; `lb-input`, `lb-select`, `lb-options`, `lb-picker`, `lb-table`, `lb-unknown-page`, `lb-confirm`, `lb-form`, `lb-insert-dialog`, `lb-master`, `lb-checkbox`, `lb-number`, `lb-date`, `lb-tel` and `lb-mailto`. Use whenever a task writes or changes markup using one of these tags, or decides whether a page needs a custom element of its own. The pages, queries and requests the widgets sit in are the loadbare-app skill's.
4
+ license: Apache-2.0
5
+ metadata:
6
+ package: "@loadbare/widgets"
7
+ homepage: https://gitlab.com/kendowns/loadbare
8
+ ---
9
+
10
+ # Working with @loadbare/widgets
11
+
12
+ Every widget is an ordinary custom element for `@loadbare/app`: markup with
13
+ `lb-*` attributes and `exp-*` parameters, landed and gathered by the hub like
14
+ any other element. How pages, queries, requests and custom elements work is
15
+ in the loadbare-app skill. This skill covers only what each widget takes and
16
+ does.
17
+
18
+ Install the package and list it in the application's `src/imports.ts`:
19
+
20
+ ```ts
21
+ export default ["@loadbare/widgets"];
22
+ ```
23
+
24
+ The builder then bundles the definition and script of each widget a page
25
+ uses, and nothing else. The application's own definition of a tag
26
+ overrides the package's.
27
+
28
+ `lb-input`, `lb-select`, `lb-options` and `lb-picker` are controls:
29
+ form-associated custom elements with a `value` property that fire `change`.
30
+ Each carries `lb-column` and `lb-request` itself, the way an `<input>` does.
31
+ Inside a form that sends one request for all its controls, leave
32
+ `lb-request` off.
33
+
34
+ ## Reference material
35
+
36
+ One page per widget. They ship with the package, so no network is needed.
37
+
38
+ | Widget | Is |
39
+ | --- | --- |
40
+ | [`lb-input`](references/lb-input.md) | a control wrapping an `<input>` |
41
+ | [`lb-select`](references/lb-select.md) | a control wrapping a `<select>` whose options the page writes |
42
+ | [`lb-options`](references/lb-options.md) | a control wrapping a `<select>` whose options come from a query |
43
+ | [`lb-picker`](references/lb-picker.md) | `lb-options` with the row template built in |
44
+ | [`lb-table`](references/lb-table.md) | a `<table>` supplying its own scaffolding and a ghost row |
45
+ | [`lb-unknown-page`](references/lb-unknown-page.md) | the chrome's dialog for a URL that names no page |
46
+ | [`lb-confirm`](references/lb-confirm.md) | a button that asks before it sends |
47
+ | [`lb-form`](references/lb-form.md) | a form over one row, which saves per control once the row is on record |
48
+ | [`lb-insert-dialog`](references/lb-insert-dialog.md) | a row entered in a modal dialog, chosen in the picker that opened it |
49
+ | [`lb-master`](references/lb-master.md) | a set, one of its rows, and the buttons that act on the set |
50
+ | [`lb-checkbox`](references/lb-checkbox.md) | a checkbox and its label, as markup to style |
51
+ | [`lb-number`](references/lb-number.md) | a number shown by `Intl.NumberFormat` |
52
+ | [`lb-date`](references/lb-date.md) | a date or time shown by `Intl.DateTimeFormat` |
53
+ | [`lb-tel`](references/lb-tel.md) | a phone number shown as a `tel:` link |
54
+ | [`lb-mailto`](references/lb-mailto.md) | an email address shown as a `mailto:` link |
55
+
56
+ ## The other loadbare skills
57
+
58
+ `npx loadbare-widgets skills install` puts this skill where a coding agent
59
+ finds it. `npx loadbare-app skills install` and
60
+ `npx loadbare-db skills install` do the same for the loadbare-app and
61
+ loadbare-db skills.
@@ -0,0 +1,63 @@
1
+ # `lb-checkbox`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A checkbox and its label as one element, for an application to style. A
6
+ definition only, with no script: the hub treats a native checkbox as a
7
+ control, so the widget adds nothing but the markup.
8
+
9
+ Requires `exp-column` and `exp-label`.
10
+
11
+ ```html
12
+ <div lb-query="people">
13
+ <template>
14
+ <div>
15
+ <lb-checkbox exp-column="active" exp-label="Active" exp-request="lb-row-update"></lb-checkbox>
16
+ </div>
17
+ </template>
18
+ </div>
19
+ ```
20
+
21
+ ## Expansion Attributes
22
+
23
+ | Parameter | Fills |
24
+ | ------------- | ----------------------------------------------- |
25
+ | `exp-column` | the checkbox's `lb-column` |
26
+ | `exp-label` | the label's text |
27
+ | `exp-request` | the checkbox's `lb-request`, none when absent |
28
+
29
+ The column and request go on the checkbox inside, since it is the control,
30
+ so they are expansion attributes here rather than `lb-column` and
31
+ `lb-request` on the tag.
32
+
33
+ ## Why it is not like `lb-input`
34
+
35
+ `lb-input` has a script that makes the tag itself the control, so the page
36
+ writes `lb-column` on the tag. `lb-checkbox` cannot do the same. A value
37
+ that lands before a widget's script has loaded reaches it only as the
38
+ `lb-column-value` stamp, a string, and a checkbox's state is a boolean. A
39
+ script `lb-checkbox` would read the stamp `"false"` as checked, since no
40
+ string is read. The native checkbox has no such gap: the hub sets `checked`
41
+ on it directly.
42
+
43
+ `lb-input` could equally be markup alone; its script is a convention, kept
44
+ for now.
45
+
46
+ ## The control
47
+
48
+ The checkbox is checked when its column is anything but `false` or null, and
49
+ sends `"true"` or `"false"`. See "Checkboxes" in the data-binding reference
50
+ of the loadbare-app skill.
51
+
52
+ ## Styling
53
+
54
+ The definition is a `<label>` holding the `<input type="checkbox">` and a
55
+ `<span>` with the text:
56
+
57
+ ```css
58
+ lb-checkbox label {
59
+ display: inline-flex;
60
+ gap: 0.5em;
61
+ align-items: center;
62
+ }
63
+ ```
@@ -0,0 +1,44 @@
1
+ # `lb-confirm`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A button that asks before it sends. The button opens a dialog holding the
6
+ message the page writes inside the tag, a Cancel button, and a confirm button
7
+ carrying the request.
8
+
9
+ Requires `exp-label` and `exp-request`.
10
+
11
+ ```html
12
+ <template>
13
+ <tr>
14
+ <td lb-column="name"></td>
15
+ <td>
16
+ <lb-confirm exp-label="Delete" exp-request="lb-row-delete">
17
+ Delete <b lb-column="name"></b>?
18
+ </lb-confirm>
19
+ </td>
20
+ </tr>
21
+ </template>
22
+ ```
23
+
24
+ ## Expansion Attributes
25
+
26
+ | Parameter | Fills |
27
+ | ------------- | --------------------------------------------- |
28
+ | `exp-label` | the text of the button that opens the dialog |
29
+ | `exp-request` | the confirm button's `lb-request` |
30
+ | `exp-confirm` | the confirm button's text, `Confirm` when absent |
31
+
32
+ ## The request
33
+
34
+ The request is on the dialog's confirm button, never on the tag, so nothing
35
+ reaches the hub until the user says yes. The dialog sits in the row the tag
36
+ sits in, so the request carries that row's query and key, and the message
37
+ can show the row's columns with `lb-column`.
38
+
39
+ Cancel comes first and takes focus, so Enter on a dialog opened by accident
40
+ sends nothing. The widget closes the dialog when the request succeeds. A refused request leaves it open, with the hub's
41
+ `lb-request-error` on the confirm button.
42
+
43
+ The dialog is found by its place in the tag, not by id, so the tag works in a
44
+ row template, where the hub clones it once per row.
@@ -0,0 +1,48 @@
1
+ # `lb-date`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A date or a time shown by the browser's `Intl.DateTimeFormat`. Not a
6
+ control: write `lb-column` on the tag, and the hub lands the column on it
7
+ and never gathers it.
8
+
9
+ ```html
10
+ <lb-date lb-column="due" exp-date-style="medium"></lb-date>
11
+ ```
12
+
13
+ ## Expansion Attributes
14
+
15
+ Each parameter is one of `Intl.DateTimeFormat`'s options, in kebab-case. A
16
+ parameter not given leaves Intl's default.
17
+
18
+ | Parameter | `Intl.DateTimeFormat` option |
19
+ | -------------------- | ------------------------------------- |
20
+ | `exp-locale` | the locale, the browser's when absent |
21
+ | `exp-date-style` | `dateStyle` |
22
+ | `exp-time-style` | `timeStyle` |
23
+ | `exp-time-zone` | `timeZone` |
24
+ | `exp-time-zone-name` | `timeZoneName` |
25
+ | `exp-hour-cycle` | `hourCycle` |
26
+ | `exp-weekday` | `weekday` |
27
+ | `exp-era` | `era` |
28
+ | `exp-year` | `year` |
29
+ | `exp-month` | `month` |
30
+ | `exp-day` | `day` |
31
+ | `exp-hour` | `hour` |
32
+ | `exp-minute` | `minute` |
33
+ | `exp-second` | `second` |
34
+
35
+ Intl refuses `exp-date-style` or `exp-time-style` beside any of
36
+ `exp-weekday` through `exp-second`.
37
+
38
+ ## The value
39
+
40
+ The widget reads the value as JavaScript's `Date` does. A date alone, as
41
+ `YYYY-MM-DD`, shows as that day in every time zone. A value that is no
42
+ date shows as it arrived. Null shows nothing.
43
+
44
+ ## Styling
45
+
46
+ The definition is a `<time>` whose text is the formatted date and whose
47
+ `datetime` is the value as it arrived. The hub stamps the tag with
48
+ `lb-column-value`, the value as it arrived.
@@ -0,0 +1,35 @@
1
+ # `lb-form`
2
+
3
+ > Status: a barebones starter. Iterative use will drive improvements.
4
+
5
+ A `<form>` over one row's controls, in one of two modes the row decides.
6
+
7
+ Requires `exp-id`.
8
+
9
+ ```html
10
+ <section lb-query="account">
11
+ <lb-form exp-id="account-form">
12
+ <lb-input lb-column="name" exp-label="Name" lb-request="lb-row-update"></lb-input>
13
+ </lb-form>
14
+ <button type="submit" form="account-form" lb-request="lb-row-insert">Save new</button>
15
+ </section>
16
+ ```
17
+
18
+ ## Expansion Attributes
19
+
20
+ | Parameter | Fills |
21
+ | --------- | ------------------ |
22
+ | `exp-id` | the form's `id` |
23
+
24
+ ## Two modes
25
+
26
+ A row with a key is on record, and each control saves its own column on
27
+ `change` with `lb-request="lb-row-update"`. A row whose key is empty is a new
28
+ one not yet entered: the hub sends no update for it, so the controls only
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
+
32
+ A definition only, with no script of its own.
33
+
34
+ A button that acts on the row as a whole, such as one adding it to the set,
35
+ belongs to whatever holds the set, and reaches the form by `form="<id>"`.