@bytesbrains/weblocks 0.2.0 → 0.3.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/AGENT.md CHANGED
@@ -46,16 +46,17 @@ its exact config schema:
46
46
  - `catalogPrompt()` — a compact text menu for a system prompt.
47
47
  - `CATALOG.md` — the same, human-readable.
48
48
 
49
- The 37 block types, by group:
49
+ The 39 block types, by group:
50
50
 
51
51
  - **Chrome/app-shell:** `nav`, `app-shell`, `sidebar`, `announcement-bar`, `footer`
52
52
  - **Heroes:** `hero`, `hero-app`
53
53
  - **Content:** `features`, `about`, `rich-text`, `split`, `steps`, `stats`,
54
54
  `services-catalogue`, `pricing`, `logos`, `team`
55
55
  - **Media:** `gallery`, `carousel`, `video`, `map`
56
+ - **Location:** `directions` (deep links to open the visitor’s map app)
56
57
  - **Structured:** `timeline`, `tabs`, `accordion`, `testimonials`, `faq`
57
58
  - **Collections:** `blog-list`, `blog-post`, `feed`
58
- - **Dynamic (powered):** `contact-form`, `newsletter`, `auth`
59
+ - **Dynamic (powered):** `contact-form`, `newsletter`, `search`, `auth`
59
60
  - **Conversion/rhythm:** `cta`, `social-links`, `contact-details`, `divider`, `spacer`
60
61
 
61
62
  ## Composing a manifest
@@ -152,8 +153,8 @@ cheap:
152
153
 
153
154
  ## Powered (dynamic) blocks
154
155
 
155
- `contact-form`, `newsletter`, and `auth` need a backend. You still only fill their
156
- typed config (fields, labels, providers). The block declares the runtime
156
+ `contact-form`, `newsletter`, `search`, and `auth` need a backend. You still only
157
+ fill their typed config (fields, labels, providers, placeholder). The block declares the runtime
157
158
  capability it needs; the **host** wires the endpoint. If no runtime is wired, the
158
159
  block renders inert-but-valid — that is expected, not an error. Never put secrets,
159
160
  endpoints, captcha keys, or backend logic in the config.
package/CATALOG.md CHANGED
@@ -1,8 +1,8 @@
1
- # @bytesbrains/weblocks — Block Catalog (v0.2.0)
1
+ # @bytesbrains/weblocks — Block Catalog (v0.3.0)
2
2
 
3
3
  The AI composes a `SiteManifest` (`{ meta, design, blocks[] }`) using **only** the block types below, then the engine validates + renders it to static HTML. This file is generated from the code (`npm run emit:catalog`) — do not edit by hand.
4
4
 
5
- **Block types:** `app-shell` · `nav` · `announcement-bar` · `sidebar` · `hero` · `hero-app` · `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` · `gallery` · `carousel` · `video` · `map` · `timeline` · `tabs` · `accordion` · `testimonials` · `faq` · `blog-list` · `blog-post` · `feed` · `contact-form` · `newsletter` · `auth` · `cta` · `social-links` · `contact-details` · `divider` · `spacer` · `footer`
5
+ **Block types:** `app-shell` · `nav` · `announcement-bar` · `sidebar` · `hero` · `hero-app` · `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` · `gallery` · `carousel` · `video` · `map` · `timeline` · `tabs` · `accordion` · `testimonials` · `faq` · `blog-list` · `blog-post` · `feed` · `contact-form` · `newsletter` · `search` · `auth` · `cta` · `social-links` · `contact-details` · `directions` · `divider` · `spacer` · `footer`
6
6
 
7
7
  ## `app-shell`
8
8
 
@@ -315,6 +315,19 @@ An email-capture form that posts to a host-provided runtime; renders inert until
315
315
  | `submitLabel` | string | | |
316
316
  | `successMessage` | string | | |
317
317
 
318
+ ## `search`
319
+
320
+ A site search rendered as a full search bar or a compact expanding icon button; queries a host-provided search runtime.
321
+
322
+ | field | type | required | notes |
323
+ |---|---|---|---|
324
+ | `layout` | undefined (bar\|icon) | | |
325
+ | `placeholder` | string | | |
326
+ | `label` | string | | |
327
+ | `buttonLabel` | string | | |
328
+ | `name` | string | | |
329
+ | `align` | undefined (start\|center\|end) | | |
330
+
318
331
  ## `auth`
319
332
 
320
333
  A provider-agnostic sign in / sign up panel: social provider buttons and an optional email form that start auth via a host runtime.
@@ -358,6 +371,21 @@ A contact info block: address, phone, email, and opening hours (each optional).
358
371
  | `email` | string | | |
359
372
  | `hours` | string | | |
360
373
 
374
+ ## `directions`
375
+
376
+ A location card with deep links that open the visitor’s map app (Google/Apple Maps) for directions, from an address, GPS coordinates, or a pasted map link.
377
+
378
+ | field | type | required | notes |
379
+ |---|---|---|---|
380
+ | `title` | string | | |
381
+ | `place` | string | | |
382
+ | `address` | string | | |
383
+ | `lat` | string | | |
384
+ | `lng` | string | | |
385
+ | `mapUrl` | string | | |
386
+ | `directionsLabel` | string | | |
387
+ | `appleMaps` | boolean | | |
388
+
361
389
  ## `divider`
362
390
 
363
391
  A visual section break rendered as a thin line, a dotted rule, or a gradient bar.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ follows [semantic versioning](https://semver.org): the **block catalog** and the
5
5
  **`SiteManifest` shape** are the public contract — additive block/field changes
6
6
  are minor, breaking changes to either are major.
7
7
 
8
+ ## 0.3.0 — 2026-07-16
9
+
10
+ Additive and non-breaking — every `0.2.0` manifest still validates and renders
11
+ identically. Catalog 37 → 39.
12
+
13
+ ### Added
14
+ - **`search` block** — site search rendered as a full **search bar** or a
15
+ compact **expanding icon button** (CSS-only, no JavaScript). A powered block
16
+ that queries a host `search.query` runtime; degrades to an inert, valid GET
17
+ form when unwired.
18
+ - **`directions` block** — a location card with **deep links that open the
19
+ visitor’s map app for directions** (Google Maps universal URL + optional Apple
20
+ Maps), built from an address, GPS coordinates, or a pasted map link. Static,
21
+ no runtime. Catalog now 39 blocks.
22
+
8
23
  ## 0.2.0 — 2026-07-16
9
24
 
10
25
  Rich, app-like PWAs. Additive and **non-breaking**: every `0.1.0` manifest still
package/README.md CHANGED
@@ -139,7 +139,7 @@ import catalogJson from '@bytesbrains/weblocks/catalog.json' with { type: 'json'
139
139
 
140
140
  ## Block catalog
141
141
 
142
- **37 typed blocks.** Full field reference in [`CATALOG.md`](./CATALOG.md).
142
+ **39 typed blocks.** Full field reference in [`CATALOG.md`](./CATALOG.md).
143
143
 
144
144
  | Group | Blocks |
145
145
  |---|---|
@@ -147,9 +147,10 @@ import catalogJson from '@bytesbrains/weblocks/catalog.json' with { type: 'json'
147
147
  | Heroes | `hero` · `hero-app` |
148
148
  | Content | `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` |
149
149
  | Media | `gallery` · `carousel` · `video` · `map` |
150
+ | Location | `directions` (deep links to the visitor’s map app) |
150
151
  | Structured | `timeline` · `tabs` · `accordion` · `testimonials` · `faq` |
151
152
  | Collections | `blog-list` · `blog-post` · `feed` |
152
- | Dynamic (powered) | `contact-form` · `newsletter` · `auth` |
153
+ | Dynamic (powered) | `contact-form` · `newsletter` · `search` · `auth` |
153
154
  | Conversion / rhythm | `cta` · `social-links` · `contact-details` · `divider` · `spacer` |
154
155
 
155
156
  `rich-text` and `blog-post` carry **typed** content nodes (headings, paragraphs,
package/catalog.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "package": "@bytesbrains/weblocks",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Block catalog for @bytesbrains/weblocks. Send this to the model as its API reference; it composes a SiteManifest ({ meta, design, blocks[] }) using ONLY these block types. The engine then validates and renders it.",
6
6
  "blockTypes": [
7
7
  "app-shell",
@@ -34,10 +34,12 @@
34
34
  "feed",
35
35
  "contact-form",
36
36
  "newsletter",
37
+ "search",
37
38
  "auth",
38
39
  "cta",
39
40
  "social-links",
40
41
  "contact-details",
42
+ "directions",
41
43
  "divider",
42
44
  "spacer",
43
45
  "footer"
@@ -1581,6 +1583,56 @@
1581
1583
  ]
1582
1584
  }
1583
1585
  },
1586
+ {
1587
+ "type": "search",
1588
+ "description": "A site search rendered as a full search bar or a compact expanding icon button; queries a host-provided search runtime.",
1589
+ "schema": {
1590
+ "type": "object",
1591
+ "properties": {
1592
+ "layout": {
1593
+ "enum": [
1594
+ "bar",
1595
+ "icon"
1596
+ ],
1597
+ "default": "bar"
1598
+ },
1599
+ "placeholder": {
1600
+ "type": "string",
1601
+ "maxLength": 80,
1602
+ "default": "Search…"
1603
+ },
1604
+ "label": {
1605
+ "type": "string",
1606
+ "maxLength": 60,
1607
+ "default": "Search"
1608
+ },
1609
+ "buttonLabel": {
1610
+ "type": "string",
1611
+ "maxLength": 40,
1612
+ "default": "Search"
1613
+ },
1614
+ "name": {
1615
+ "type": "string",
1616
+ "maxLength": 40,
1617
+ "default": "q"
1618
+ },
1619
+ "align": {
1620
+ "enum": [
1621
+ "start",
1622
+ "center",
1623
+ "end"
1624
+ ],
1625
+ "default": "center"
1626
+ }
1627
+ },
1628
+ "additionalProperties": false
1629
+ },
1630
+ "runtime": {
1631
+ "capabilities": [
1632
+ "search.query"
1633
+ ]
1634
+ }
1635
+ },
1584
1636
  {
1585
1637
  "type": "auth",
1586
1638
  "description": "A provider-agnostic sign in / sign up panel: social provider buttons and an optional email form that start auth via a host runtime.",
@@ -1761,6 +1813,55 @@
1761
1813
  "additionalProperties": false
1762
1814
  }
1763
1815
  },
1816
+ {
1817
+ "type": "directions",
1818
+ "description": "A location card with deep links that open the visitor’s map app (Google/Apple Maps) for directions, from an address, GPS coordinates, or a pasted map link.",
1819
+ "schema": {
1820
+ "type": "object",
1821
+ "properties": {
1822
+ "title": {
1823
+ "type": "string",
1824
+ "maxLength": 120,
1825
+ "default": ""
1826
+ },
1827
+ "place": {
1828
+ "type": "string",
1829
+ "maxLength": 120,
1830
+ "default": ""
1831
+ },
1832
+ "address": {
1833
+ "type": "string",
1834
+ "maxLength": 240,
1835
+ "default": ""
1836
+ },
1837
+ "lat": {
1838
+ "type": "string",
1839
+ "maxLength": 24,
1840
+ "default": ""
1841
+ },
1842
+ "lng": {
1843
+ "type": "string",
1844
+ "maxLength": 24,
1845
+ "default": ""
1846
+ },
1847
+ "mapUrl": {
1848
+ "type": "string",
1849
+ "maxLength": 500,
1850
+ "default": ""
1851
+ },
1852
+ "directionsLabel": {
1853
+ "type": "string",
1854
+ "maxLength": 40,
1855
+ "default": "Get directions"
1856
+ },
1857
+ "appleMaps": {
1858
+ "type": "boolean",
1859
+ "default": true
1860
+ }
1861
+ },
1862
+ "additionalProperties": false
1863
+ }
1864
+ },
1764
1865
  {
1765
1866
  "type": "divider",
1766
1867
  "description": "A visual section break rendered as a thin line, a dotted rule, or a gradient bar.",
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const directions: BlockSpec;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `directions` — a location card with **deep links that open the visitor's map
3
+ * app for directions**. Unlike `map` (an inline embed), this is lightweight and
4
+ * link-based: give it an address, GPS coordinates, and/or a pasted map link, and
5
+ * it builds cross-platform "Get directions" links.
6
+ *
7
+ * - Google Maps universal URL (`.../maps/dir/?api=1&destination=…`) opens the
8
+ * Google Maps app on iOS/Android when installed, else the web.
9
+ * - An optional Apple Maps link (`maps.apple.com/?daddr=…`) for iOS users.
10
+ * - GPS coordinates, when valid, are used as a precise destination.
11
+ *
12
+ * Static brick — no runtime, no JS. Links are built with `encodeURIComponent`;
13
+ * a pasted `mapUrl` is scheme-sanitized.
14
+ */
15
+ import { escapeAttr, escapeHtml, sanitizeUrl } from '../schema.js';
16
+ const PIN = '<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s-7-6.5-7-11a7 7 0 0 1 14 0c0 4.5-7 11-7 11z"></path><circle cx="12" cy="10" r="2.5"></circle></svg>';
17
+ const schema = {
18
+ title: { kind: 'string', default: '', max: 120 },
19
+ place: { kind: 'string', default: '', max: 120 }, // name of the location
20
+ address: { kind: 'string', default: '', max: 240 }, // shown + query fallback
21
+ lat: { kind: 'string', default: '', max: 24 }, // GPS latitude (e.g. 38.7223)
22
+ lng: { kind: 'string', default: '', max: 24 }, // GPS longitude (e.g. -9.1393)
23
+ mapUrl: { kind: 'string', default: '', max: 500 }, // a pasted Google/other map link
24
+ directionsLabel: { kind: 'string', default: 'Get directions', max: 40 },
25
+ appleMaps: { kind: 'boolean', default: true }, // also offer an Apple Maps link
26
+ };
27
+ const css = `
28
+ .blk-directions{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
29
+ .blk-directions .wrap{max-width:640px;margin:0 auto}
30
+ .blk-directions h2{font-size:var(--fs-xl);margin:0 0 var(--space);font-weight:800;text-align:center}
31
+ .blk-directions .card{display:flex;gap:1em;align-items:flex-start;background:var(--surface);border:1px solid color-mix(in srgb,var(--text) 12%,transparent);border-radius:var(--radius);padding:calc(var(--space)*1.2)}
32
+ .blk-directions .pin{flex:0 0 auto;display:inline-flex;color:var(--primary);line-height:0;padding-top:.15em}
33
+ .blk-directions .info{flex:1;min-width:0}
34
+ .blk-directions .place{margin:0;font-weight:700;font-size:var(--fs-lg)}
35
+ .blk-directions .addr{margin:.25em 0 0;color:var(--muted)}
36
+ .blk-directions .coords{margin:.25em 0 0;color:var(--muted);font-size:var(--fs-base);font-variant-numeric:tabular-nums}
37
+ .blk-directions .actions{display:flex;flex-wrap:wrap;gap:.5em;margin-top:calc(var(--space)*.9)}
38
+ .blk-directions .btn{display:inline-flex;align-items:center;gap:.4em;text-decoration:none;font-weight:600;font-size:var(--fs-base);padding:.55em 1em;border-radius:var(--radius);border:1px solid var(--primary);color:var(--primary);transition:opacity var(--motion),background var(--motion)}
39
+ .blk-directions .btn.primary{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}
40
+ .blk-directions .btn:hover{opacity:.9}
41
+ @media(max-width:440px){.blk-directions .card{flex-direction:column}.blk-directions .actions .btn{flex:1;justify-content:center}}
42
+ `.trim();
43
+ /** Parse a coordinate string to a valid lat/lng pair, or null. */
44
+ function coords(latRaw, lngRaw) {
45
+ const latStr = String(latRaw ?? '').trim();
46
+ const lngStr = String(lngRaw ?? '').trim();
47
+ if (!latStr || !lngStr)
48
+ return null; // empty → not set (Number('') is 0, so guard first)
49
+ const lat = Number(latStr);
50
+ const lng = Number(lngStr);
51
+ if (!Number.isFinite(lat) || !Number.isFinite(lng))
52
+ return null;
53
+ if (Math.abs(lat) > 90 || Math.abs(lng) > 180)
54
+ return null;
55
+ return { lat, lng };
56
+ }
57
+ function render(config) {
58
+ const title = config.title;
59
+ const place = config.place;
60
+ const address = config.address;
61
+ const gps = coords(config.lat, config.lng);
62
+ const mapUrl = sanitizeUrl(config.mapUrl);
63
+ const hasMapUrl = mapUrl !== '#' && !!config.mapUrl;
64
+ const directionsLabel = config.directionsLabel || 'Get directions';
65
+ const appleMaps = config.appleMaps !== false;
66
+ // Precise coords win as the destination; else the address; else the place name.
67
+ const dest = gps ? `${gps.lat},${gps.lng}` : (address || place);
68
+ const enc = dest ? encodeURIComponent(dest) : '';
69
+ const gDir = enc ? `https://www.google.com/maps/dir/?api=1&destination=${enc}` : '';
70
+ const aDir = enc ? `https://maps.apple.com/?daddr=${enc}` : '';
71
+ const btn = (href, label, primary = false) => `<a class="btn${primary ? ' primary' : ''}" href="${escapeAttr(href)}" target="_blank" rel="noopener">${escapeHtml(label)}</a>`;
72
+ const actions = [
73
+ gDir && btn(gDir, directionsLabel, true),
74
+ appleMaps && aDir && btn(aDir, 'Apple Maps'),
75
+ hasMapUrl && btn(mapUrl, 'Open map'),
76
+ ].filter(Boolean).join('\n ');
77
+ return `<section class="blk-directions" aria-label="${escapeAttr(title || place || 'Directions')}">
78
+ <div class="wrap">
79
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
80
+ <div class="card">
81
+ <span class="pin" aria-hidden="true">${PIN}</span>
82
+ <div class="info">
83
+ ${place ? `<p class="place">${escapeHtml(place)}</p>` : ''}
84
+ ${address ? `<p class="addr">${escapeHtml(address)}</p>` : ''}
85
+ ${gps ? `<p class="coords">${escapeHtml(`${gps.lat}, ${gps.lng}`)}</p>` : ''}
86
+ ${actions ? `<div class="actions">\n ${actions}\n </div>` : ''}
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </section>`;
91
+ }
92
+ export const directions = {
93
+ type: 'directions',
94
+ description: 'A location card with deep links that open the visitor’s map app (Google/Apple Maps) for directions, from an address, GPS coordinates, or a pasted map link.',
95
+ schema,
96
+ css,
97
+ render,
98
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const search: BlockSpec;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `search` — site search as either a full **bar** or a compact **icon** button
3
+ * that expands on focus (CSS-only, no JavaScript). Powered brick (§6): the query
4
+ * target is resolved by the host via the `search.query` capability. With no
5
+ * runtime wired it degrades to a harmless same-page GET form, keeping the
6
+ * `data-wl-*` hooks so a host can enhance it client-side.
7
+ */
8
+ import { escapeAttr, escapeHtml } from '../schema.js';
9
+ import { NOOP_RUNTIME, safeMethod } from '../runtime.js';
10
+ const CAPABILITY = 'search.query';
11
+ // Inline magnifier — self-contained, inherits colour via currentColor.
12
+ const ICON = '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><line x1="20" y1="20" x2="16.65" y2="16.65"></line></svg>';
13
+ const schema = {
14
+ layout: { kind: 'enum', values: ['bar', 'icon'], default: 'bar' },
15
+ placeholder: { kind: 'string', default: 'Search…', max: 80 },
16
+ label: { kind: 'string', default: 'Search', max: 60 }, // accessible name
17
+ buttonLabel: { kind: 'string', default: 'Search', max: 40 }, // submit text (bar layout)
18
+ name: { kind: 'string', default: 'q', max: 40 }, // query param name
19
+ align: { kind: 'enum', values: ['start', 'center', 'end'], default: 'center' },
20
+ };
21
+ const css = `
22
+ .blk-search{padding:var(--space) var(--space);background:var(--bg);color:var(--text);display:flex}
23
+ .blk-search.align-start{justify-content:flex-start}
24
+ .blk-search.align-center{justify-content:center}
25
+ .blk-search.align-end{justify-content:flex-end}
26
+ .blk-search .field{display:flex;align-items:center;background:var(--surface);border:1px solid color-mix(in srgb,var(--text) 16%,transparent);border-radius:var(--radius);transition:border-color var(--motion),box-shadow var(--motion)}
27
+ .blk-search .field:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 18%,transparent)}
28
+ .blk-search .ico{display:inline-flex;align-items:center;justify-content:center;padding:.5em .2em .5em .65em;color:var(--muted);cursor:text;line-height:0}
29
+ .blk-search .input{font:inherit;color:inherit;background:transparent;border:0;outline:0;padding:.55em .5em;min-width:0}
30
+ .blk-search .input::placeholder{color:var(--muted)}
31
+ .blk-search .go{display:inline-flex;align-items:center;font:inherit;font-weight:700;cursor:pointer;border:0;border-radius:var(--radius);white-space:nowrap}
32
+ .blk-search .vh{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
33
+ /* Bar: input always open, text submit button. */
34
+ .blk-search.layout-bar .field{width:min(520px,100%)}
35
+ .blk-search.layout-bar .input{flex:1}
36
+ .blk-search.layout-bar .go{background:var(--primary);color:var(--on-primary);padding:.5em 1em;margin:3px}
37
+ .blk-search.layout-bar .go:hover{opacity:.9}
38
+ /* Icon: collapsed magnifier that expands on focus-within; Enter submits. */
39
+ .blk-search.layout-icon .field{border-radius:999px}
40
+ .blk-search.layout-icon .input{width:0;padding-left:0;padding-right:0;opacity:0;transition:width var(--motion),opacity var(--motion),padding var(--motion)}
41
+ .blk-search.layout-icon .field:focus-within .input{width:min(240px,60vw);opacity:1;padding:.55em .5em .55em .2em}
42
+ .blk-search.layout-icon .go{display:none}
43
+ @media(max-width:480px){.blk-search.layout-icon .field:focus-within .input{width:52vw}}
44
+ `.trim();
45
+ function render(config, _tokens, ctx) {
46
+ const id = ctx?.id ?? 'search';
47
+ const runtime = ctx?.runtime ?? NOOP_RUNTIME;
48
+ const layout = config.layout === 'icon' ? 'icon' : 'bar';
49
+ const align = ['start', 'center', 'end'].includes(config.align) ? config.align : 'center';
50
+ const label = config.label || 'Search';
51
+ const placeholder = config.placeholder;
52
+ const buttonLabel = config.buttonLabel || 'Search';
53
+ const qname = String(config.name ?? 'q').replace(/[^A-Za-z0-9_-]/g, '') || 'q';
54
+ const fid = `s-${String(id).replace(/[^A-Za-z0-9_-]/g, '') || 'search'}`;
55
+ const action = runtime.resolve(CAPABILITY, id);
56
+ // Search is a query, so GET by default; the host adapter may override.
57
+ const method = action ? safeMethod(action.method) : 'get';
58
+ return `<section class="blk-search layout-${layout} align-${align}" aria-label="${escapeAttr(label)}">
59
+ <form class="wrap" role="search" method="${method}" action="${action ? escapeAttr(action.url) : '#'}" data-wl-capability="${CAPABILITY}" data-wl-block="${escapeAttr(id)}"${action ? '' : ' data-wl-inert="true"'}>
60
+ <div class="field">
61
+ <label class="ico" for="${escapeAttr(fid)}" aria-hidden="true">${ICON}</label>
62
+ <input class="input" id="${escapeAttr(fid)}" type="search" name="${escapeAttr(qname)}" placeholder="${escapeAttr(placeholder)}" aria-label="${escapeAttr(label)}" autocomplete="off">
63
+ <button class="go" type="submit"><span class="go-label">${escapeHtml(buttonLabel)}</span></button>
64
+ </div>
65
+ </form>
66
+ </section>`;
67
+ }
68
+ export const search = {
69
+ type: 'search',
70
+ description: 'A site search rendered as a full search bar or a compact expanding icon button; queries a host-provided search runtime.',
71
+ schema,
72
+ css,
73
+ render,
74
+ runtime: { capabilities: [CAPABILITY] },
75
+ };
package/lib/registry.js CHANGED
@@ -26,10 +26,12 @@ import { blogPost } from './blocks/blogPost.js';
26
26
  import { feed } from './blocks/feed.js';
27
27
  import { contactForm } from './blocks/contactForm.js';
28
28
  import { newsletter } from './blocks/newsletter.js';
29
+ import { search } from './blocks/search.js';
29
30
  import { auth } from './blocks/auth.js';
30
31
  import { cta } from './blocks/cta.js';
31
32
  import { socialLinks } from './blocks/socialLinks.js';
32
33
  import { contactDetails } from './blocks/contactDetails.js';
34
+ import { directions } from './blocks/directions.js';
33
35
  import { divider } from './blocks/divider.js';
34
36
  import { spacer } from './blocks/spacer.js';
35
37
  import { appShell } from './blocks/appShell.js';
@@ -52,9 +54,9 @@ const SPECS = [
52
54
  // collections
53
55
  blogList, blogPost, feed,
54
56
  // dynamic / powered
55
- contactForm, newsletter, auth,
57
+ contactForm, newsletter, search, auth,
56
58
  // conversion / contact
57
- cta, socialLinks, contactDetails,
59
+ cta, socialLinks, contactDetails, directions,
58
60
  // rhythm
59
61
  divider, spacer,
60
62
  footer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytesbrains/weblocks",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Block engine for AI-composable web apps — the AI composes a SiteManifest from a fixed block catalog; the engine validates and renders it to static HTML. Snap-together \"Lego\" web building blocks, shareable across repos.",
5
5
  "keywords": [
6
6
  "ai",
@@ -18,7 +18,9 @@
18
18
  "license": "MIT",
19
19
  "author": "bytesbrains",
20
20
  "type": "module",
21
- "engines": { "node": ">=20" },
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
22
24
  "sideEffects": false,
23
25
  "main": "./lib/index.js",
24
26
  "module": "./lib/index.js",
@@ -47,8 +49,12 @@
47
49
  "url": "git+https://github.com/bytesbrains/weblocks.git"
48
50
  },
49
51
  "homepage": "https://github.com/bytesbrains/weblocks#readme",
50
- "bugs": { "url": "https://github.com/bytesbrains/weblocks/issues" },
51
- "publishConfig": { "access": "public" },
52
+ "bugs": {
53
+ "url": "https://github.com/bytesbrains/weblocks/issues"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
52
58
  "scripts": {
53
59
  "build": "tsc -p tsconfig.json",
54
60
  "emit:catalog": "node scripts/emit-catalog.mjs",