@bytesbrains/weblocks 0.6.2 → 0.7.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/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.6.2",
4
+ "version": "0.7.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",
@@ -20,6 +20,8 @@
20
20
  "steps",
21
21
  "stats",
22
22
  "services-catalogue",
23
+ "menu",
24
+ "product",
23
25
  "pricing",
24
26
  "logos",
25
27
  "team",
@@ -32,10 +34,12 @@
32
34
  "tabs",
33
35
  "accordion",
34
36
  "testimonials",
37
+ "reviews",
35
38
  "faq",
36
39
  "blog-list",
37
40
  "blog-post",
38
41
  "feed",
42
+ "booking",
39
43
  "contact-form",
40
44
  "newsletter",
41
45
  "search",
@@ -43,6 +47,7 @@
43
47
  "cta",
44
48
  "social-links",
45
49
  "contact-details",
50
+ "hours",
46
51
  "directions",
47
52
  "legal",
48
53
  "divider",
@@ -927,6 +932,174 @@
927
932
  "additionalProperties": false
928
933
  }
929
934
  },
935
+ {
936
+ "type": "menu",
937
+ "description": "A food/drink menu: named sections, each with items that have an optional price, dietary/allergen tags (e.g. V, VG, GF), and a 0–3 spice level. Use for restaurants and cafés.",
938
+ "schema": {
939
+ "type": "object",
940
+ "properties": {
941
+ "title": {
942
+ "type": "string",
943
+ "maxLength": 120,
944
+ "default": "Menu"
945
+ },
946
+ "subtitle": {
947
+ "type": "string",
948
+ "maxLength": 240,
949
+ "default": ""
950
+ },
951
+ "sections": {
952
+ "type": "array",
953
+ "items": {
954
+ "type": "object",
955
+ "properties": {
956
+ "name": {
957
+ "type": "string",
958
+ "maxLength": 80,
959
+ "default": "Section"
960
+ },
961
+ "note": {
962
+ "type": "string",
963
+ "maxLength": 200,
964
+ "default": ""
965
+ },
966
+ "items": {
967
+ "type": "array",
968
+ "items": {
969
+ "type": "object",
970
+ "properties": {
971
+ "name": {
972
+ "type": "string",
973
+ "maxLength": 100,
974
+ "default": "Item"
975
+ },
976
+ "description": {
977
+ "type": "string",
978
+ "maxLength": 300,
979
+ "default": ""
980
+ },
981
+ "price": {
982
+ "type": "string",
983
+ "maxLength": 40,
984
+ "default": ""
985
+ },
986
+ "tags": {
987
+ "type": "array",
988
+ "items": {
989
+ "type": "string",
990
+ "maxLength": 12,
991
+ "default": ""
992
+ },
993
+ "maxItems": 8
994
+ },
995
+ "spice": {
996
+ "type": "integer",
997
+ "minimum": 0,
998
+ "maximum": 3,
999
+ "default": 0
1000
+ }
1001
+ },
1002
+ "required": [
1003
+ "name"
1004
+ ],
1005
+ "additionalProperties": false
1006
+ },
1007
+ "maxItems": 40
1008
+ }
1009
+ },
1010
+ "required": [
1011
+ "name"
1012
+ ],
1013
+ "additionalProperties": false
1014
+ },
1015
+ "maxItems": 20
1016
+ }
1017
+ },
1018
+ "additionalProperties": false
1019
+ }
1020
+ },
1021
+ {
1022
+ "type": "product",
1023
+ "description": "A shoppable product grid: each item has an image, price (with optional \"was\" price and badge), and a buy/enquire link. Use for retail and e-commerce; the cart/checkout is the host's.",
1024
+ "schema": {
1025
+ "type": "object",
1026
+ "properties": {
1027
+ "title": {
1028
+ "type": "string",
1029
+ "maxLength": 120,
1030
+ "default": ""
1031
+ },
1032
+ "subtitle": {
1033
+ "type": "string",
1034
+ "maxLength": 240,
1035
+ "default": ""
1036
+ },
1037
+ "columns": {
1038
+ "type": "integer",
1039
+ "enum": [
1040
+ 2,
1041
+ 3,
1042
+ 4
1043
+ ],
1044
+ "default": 3
1045
+ },
1046
+ "items": {
1047
+ "type": "array",
1048
+ "items": {
1049
+ "type": "object",
1050
+ "properties": {
1051
+ "name": {
1052
+ "type": "string",
1053
+ "maxLength": 100,
1054
+ "default": "Product"
1055
+ },
1056
+ "description": {
1057
+ "type": "string",
1058
+ "maxLength": 240,
1059
+ "default": ""
1060
+ },
1061
+ "price": {
1062
+ "type": "string",
1063
+ "maxLength": 40,
1064
+ "default": ""
1065
+ },
1066
+ "was": {
1067
+ "type": "string",
1068
+ "maxLength": 40,
1069
+ "default": ""
1070
+ },
1071
+ "image": {
1072
+ "type": "string",
1073
+ "maxLength": 500,
1074
+ "default": ""
1075
+ },
1076
+ "badge": {
1077
+ "type": "string",
1078
+ "maxLength": 24,
1079
+ "default": ""
1080
+ },
1081
+ "ctaLabel": {
1082
+ "type": "string",
1083
+ "maxLength": 40,
1084
+ "default": "Buy"
1085
+ },
1086
+ "ctaHref": {
1087
+ "type": "string",
1088
+ "maxLength": 500,
1089
+ "default": "#"
1090
+ }
1091
+ },
1092
+ "required": [
1093
+ "name"
1094
+ ],
1095
+ "additionalProperties": false
1096
+ },
1097
+ "maxItems": 48
1098
+ }
1099
+ },
1100
+ "additionalProperties": false
1101
+ }
1102
+ },
930
1103
  {
931
1104
  "type": "pricing",
932
1105
  "description": "A row of pricing/tier cards, each with a name, price, feature list, and a call-to-action button; one plan can be highlighted.",
@@ -1539,6 +1712,87 @@
1539
1712
  "additionalProperties": false
1540
1713
  }
1541
1714
  },
1715
+ {
1716
+ "type": "reviews",
1717
+ "description": "Star-rated customer reviews (1–5 stars) each with an optional author, date, and source (Google, Yelp, …), plus an optional aggregate score. Richer than plain testimonial quotes.",
1718
+ "schema": {
1719
+ "type": "object",
1720
+ "properties": {
1721
+ "title": {
1722
+ "type": "string",
1723
+ "maxLength": 120,
1724
+ "default": ""
1725
+ },
1726
+ "subtitle": {
1727
+ "type": "string",
1728
+ "maxLength": 240,
1729
+ "default": ""
1730
+ },
1731
+ "average": {
1732
+ "type": "string",
1733
+ "maxLength": 8,
1734
+ "default": ""
1735
+ },
1736
+ "count": {
1737
+ "type": "string",
1738
+ "maxLength": 24,
1739
+ "default": ""
1740
+ },
1741
+ "source": {
1742
+ "type": "string",
1743
+ "maxLength": 40,
1744
+ "default": ""
1745
+ },
1746
+ "items": {
1747
+ "type": "array",
1748
+ "items": {
1749
+ "type": "object",
1750
+ "properties": {
1751
+ "rating": {
1752
+ "type": "integer",
1753
+ "minimum": 1,
1754
+ "maximum": 5,
1755
+ "default": 5
1756
+ },
1757
+ "quote": {
1758
+ "type": "string",
1759
+ "maxLength": 500,
1760
+ "default": ""
1761
+ },
1762
+ "author": {
1763
+ "type": "string",
1764
+ "maxLength": 80,
1765
+ "default": ""
1766
+ },
1767
+ "date": {
1768
+ "type": "string",
1769
+ "maxLength": 40,
1770
+ "default": ""
1771
+ },
1772
+ "source": {
1773
+ "enum": [
1774
+ "google",
1775
+ "yelp",
1776
+ "facebook",
1777
+ "trustpilot",
1778
+ "tripadvisor",
1779
+ "app-store",
1780
+ "other"
1781
+ ],
1782
+ "default": "other"
1783
+ }
1784
+ },
1785
+ "required": [
1786
+ "quote"
1787
+ ],
1788
+ "additionalProperties": false
1789
+ },
1790
+ "maxItems": 24
1791
+ }
1792
+ },
1793
+ "additionalProperties": false
1794
+ }
1795
+ },
1542
1796
  {
1543
1797
  "type": "faq",
1544
1798
  "description": "An accordion of question/answer pairs. Expands/collapses natively (no JS).",
@@ -1773,6 +2027,99 @@
1773
2027
  "additionalProperties": false
1774
2028
  }
1775
2029
  },
2030
+ {
2031
+ "type": "booking",
2032
+ "description": "An appointment/reservation request form (service, date, time, contact) that posts to a host-provided runtime. For booking-driven businesses — salons, clinics, gyms, hotels, events. No raw HTML.",
2033
+ "schema": {
2034
+ "type": "object",
2035
+ "properties": {
2036
+ "title": {
2037
+ "type": "string",
2038
+ "maxLength": 120,
2039
+ "default": "Book an appointment"
2040
+ },
2041
+ "intro": {
2042
+ "type": "string",
2043
+ "maxLength": 280,
2044
+ "default": ""
2045
+ },
2046
+ "submitLabel": {
2047
+ "type": "string",
2048
+ "maxLength": 40,
2049
+ "default": "Request booking"
2050
+ },
2051
+ "successMessage": {
2052
+ "type": "string",
2053
+ "maxLength": 200,
2054
+ "default": "Thanks — we'll confirm your booking shortly."
2055
+ },
2056
+ "serviceLabel": {
2057
+ "type": "string",
2058
+ "maxLength": 60,
2059
+ "default": "Service"
2060
+ },
2061
+ "services": {
2062
+ "type": "array",
2063
+ "items": {
2064
+ "type": "object",
2065
+ "properties": {
2066
+ "name": {
2067
+ "type": "string",
2068
+ "maxLength": 80,
2069
+ "default": "Service"
2070
+ },
2071
+ "duration": {
2072
+ "type": "string",
2073
+ "maxLength": 40,
2074
+ "default": ""
2075
+ },
2076
+ "price": {
2077
+ "type": "string",
2078
+ "maxLength": 40,
2079
+ "default": ""
2080
+ }
2081
+ },
2082
+ "required": [
2083
+ "name"
2084
+ ],
2085
+ "additionalProperties": false
2086
+ },
2087
+ "maxItems": 40
2088
+ },
2089
+ "askDate": {
2090
+ "type": "boolean",
2091
+ "default": true
2092
+ },
2093
+ "askTime": {
2094
+ "type": "boolean",
2095
+ "default": true
2096
+ },
2097
+ "askParty": {
2098
+ "type": "boolean",
2099
+ "default": false
2100
+ },
2101
+ "askPhone": {
2102
+ "type": "boolean",
2103
+ "default": true
2104
+ },
2105
+ "askNotes": {
2106
+ "type": "boolean",
2107
+ "default": true
2108
+ },
2109
+ "notesLabel": {
2110
+ "type": "string",
2111
+ "maxLength": 80,
2112
+ "default": "Anything we should know?"
2113
+ }
2114
+ },
2115
+ "additionalProperties": false
2116
+ },
2117
+ "runtime": {
2118
+ "capabilities": [
2119
+ "booking.request"
2120
+ ]
2121
+ }
2122
+ },
1776
2123
  {
1777
2124
  "type": "contact-form",
1778
2125
  "description": "A configurable contact form with typed fields (text, email, tel, textarea, select, checkbox) that posts to a host-provided runtime. No raw HTML.",
@@ -2174,6 +2521,70 @@
2174
2521
  "additionalProperties": false
2175
2522
  }
2176
2523
  },
2524
+ {
2525
+ "type": "hours",
2526
+ "description": "Structured weekly opening hours (24h times per day, split shifts allowed) rendered as an accessible table with a live \"open now / closed\" badge. Use for shops, cafés, salons, and clinics.",
2527
+ "schema": {
2528
+ "type": "object",
2529
+ "properties": {
2530
+ "title": {
2531
+ "type": "string",
2532
+ "maxLength": 120,
2533
+ "default": "Opening hours"
2534
+ },
2535
+ "timezone": {
2536
+ "type": "string",
2537
+ "maxLength": 60,
2538
+ "default": ""
2539
+ },
2540
+ "note": {
2541
+ "type": "string",
2542
+ "maxLength": 200,
2543
+ "default": ""
2544
+ },
2545
+ "days": {
2546
+ "type": "array",
2547
+ "items": {
2548
+ "type": "object",
2549
+ "properties": {
2550
+ "day": {
2551
+ "enum": [
2552
+ "mon",
2553
+ "tue",
2554
+ "wed",
2555
+ "thu",
2556
+ "fri",
2557
+ "sat",
2558
+ "sun"
2559
+ ],
2560
+ "default": "mon"
2561
+ },
2562
+ "open": {
2563
+ "type": "string",
2564
+ "maxLength": 5,
2565
+ "default": ""
2566
+ },
2567
+ "close": {
2568
+ "type": "string",
2569
+ "maxLength": 5,
2570
+ "default": ""
2571
+ },
2572
+ "closed": {
2573
+ "type": "boolean",
2574
+ "default": false
2575
+ }
2576
+ },
2577
+ "required": [
2578
+ "day"
2579
+ ],
2580
+ "additionalProperties": false
2581
+ },
2582
+ "maxItems": 21
2583
+ }
2584
+ },
2585
+ "additionalProperties": false
2586
+ }
2587
+ },
2177
2588
  {
2178
2589
  "type": "directions",
2179
2590
  "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.",
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const booking: BlockSpec;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * `booking` — an appointment/reservation request form. Powered brick (§6): the
3
+ * submit target is resolved by the host via the `booking.request` capability.
4
+ * With no runtime wired it renders inert-but-valid (disabled submit + a note)
5
+ * while keeping the `data-wl-*` hooks so a host can enhance it client-side.
6
+ *
7
+ * This is the home for the booking-driven verticals (salon, clinic, gym, hotel,
8
+ * events) — `contact-form` is generic and `auth` is login; neither fits. The
9
+ * safety-critical parts (availability, double-booking, delivery, spam) live in
10
+ * the host's vetted runtime, never in anything the AI emits. Works without JS
11
+ * via a native form post; a host `booking` island can enhance it (live slots).
12
+ */
13
+ import { escapeAttr, escapeHtml } from '../schema.js';
14
+ import { NOOP_RUNTIME, safeMethod } from '../runtime.js';
15
+ const CAPABILITY = 'booking.request';
16
+ const schema = {
17
+ title: { kind: 'string', default: 'Book an appointment', max: 120 },
18
+ intro: { kind: 'string', default: '', max: 280 },
19
+ submitLabel: { kind: 'string', default: 'Request booking', max: 40 },
20
+ successMessage: { kind: 'string', default: "Thanks — we'll confirm your booking shortly.", max: 200 },
21
+ serviceLabel: { kind: 'string', default: 'Service', max: 60 },
22
+ services: {
23
+ kind: 'array', max: 40,
24
+ of: {
25
+ kind: 'object',
26
+ fields: {
27
+ name: { kind: 'string', required: true, default: 'Service', max: 80 },
28
+ duration: { kind: 'string', default: '', max: 40 },
29
+ price: { kind: 'string', default: '', max: 40 },
30
+ },
31
+ },
32
+ },
33
+ askDate: { kind: 'boolean', default: true },
34
+ askTime: { kind: 'boolean', default: true },
35
+ askParty: { kind: 'boolean', default: false },
36
+ askPhone: { kind: 'boolean', default: true },
37
+ askNotes: { kind: 'boolean', default: true },
38
+ notesLabel: { kind: 'string', default: 'Anything we should know?', max: 80 },
39
+ };
40
+ const css = `
41
+ .blk-booking{padding:var(--space-lg) var(--space);background:var(--bg);color:var(--text)}
42
+ .blk-booking .wrap{max-width:620px;margin:0 auto}
43
+ .blk-booking h2{font-size:var(--fs-xl);margin:0 0 .2em;font-weight:800}
44
+ .blk-booking .intro{color:var(--muted);margin:0 0 var(--space)}
45
+ .blk-booking form{display:grid;gap:calc(var(--space)*.9)}
46
+ .blk-booking .grid2{display:grid;gap:calc(var(--space)*.9);grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}
47
+ .blk-booking label{display:grid;gap:.35em;font-weight:600}
48
+ .blk-booking input,.blk-booking textarea,.blk-booking select{font:inherit;color:inherit;background:var(--surface);border:1px solid color-mix(in srgb,var(--text) 18%,transparent);border-radius:var(--radius);padding:.6em .7em;width:100%}
49
+ .blk-booking textarea{min-height:6em;resize:vertical}
50
+ .blk-booking button{font:inherit;font-weight:700;cursor:pointer;border:0;border-radius:var(--radius);padding:.7em 1.2em;background:var(--primary);color:var(--on-primary);transition:opacity var(--motion)}
51
+ .blk-booking button[disabled]{opacity:.55;cursor:not-allowed}
52
+ .blk-booking .note{margin:.4em 0 0;font-size:var(--fs-base);color:var(--muted)}
53
+ `.trim();
54
+ function serviceLabel(s) {
55
+ const meta = [s.duration, s.price].filter(Boolean).join(' · ');
56
+ return meta ? `${s.name} (${meta})` : s.name;
57
+ }
58
+ function render(config, _tokens, ctx) {
59
+ const id = ctx?.id ?? 'booking';
60
+ const runtime = ctx?.runtime ?? NOOP_RUNTIME;
61
+ const title = config.title;
62
+ const intro = config.intro;
63
+ const submitLabel = config.submitLabel || 'Request booking';
64
+ const services = (config.services ?? []).filter((s) => s && s.name);
65
+ const action = runtime.resolve(CAPABILITY, id);
66
+ const base = `bk-${id}`;
67
+ const serviceField = services.length
68
+ ? `<label for="${escapeAttr(base)}-service">${escapeHtml(config.serviceLabel)}
69
+ <select id="${escapeAttr(base)}-service" name="service" required>
70
+ ${services.map((s) => `<option value="${escapeAttr(s.name)}">${escapeHtml(serviceLabel(s))}</option>`).join('\n ')}
71
+ </select></label>`
72
+ : '';
73
+ const dateTime = (config.askDate || config.askTime)
74
+ ? `<div class="grid2">
75
+ ${config.askDate ? `<label for="${escapeAttr(base)}-date">Date<input id="${escapeAttr(base)}-date" type="date" name="date" required></label>` : ''}
76
+ ${config.askTime ? `<label for="${escapeAttr(base)}-time">Time<input id="${escapeAttr(base)}-time" type="time" name="time" required></label>` : ''}
77
+ </div>`
78
+ : '';
79
+ const party = config.askParty
80
+ ? `<label for="${escapeAttr(base)}-party">Party size<input id="${escapeAttr(base)}-party" type="number" name="party" min="1" inputmode="numeric"></label>`
81
+ : '';
82
+ const contact = `<div class="grid2">
83
+ <label for="${escapeAttr(base)}-name">Your name<input id="${escapeAttr(base)}-name" type="text" name="name" required></label>
84
+ <label for="${escapeAttr(base)}-email">Email<input id="${escapeAttr(base)}-email" type="email" name="email" required></label>
85
+ </div>`;
86
+ const phone = config.askPhone
87
+ ? `<label for="${escapeAttr(base)}-phone">Phone<input id="${escapeAttr(base)}-phone" type="tel" name="phone"></label>`
88
+ : '';
89
+ const notes = config.askNotes
90
+ ? `<label for="${escapeAttr(base)}-notes">${escapeHtml(config.notesLabel)}<textarea id="${escapeAttr(base)}-notes" name="notes"></textarea></label>`
91
+ : '';
92
+ return `<section class="blk-booking" aria-label="${escapeAttr(title || 'Booking')}">
93
+ <div class="wrap">
94
+ ${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
95
+ ${intro ? `<p class="intro">${escapeHtml(intro)}</p>` : ''}
96
+ <form method="${safeMethod(action?.method)}" action="${action ? escapeAttr(action.url) : '#'}" data-wl-capability="${CAPABILITY}" data-wl-block="${escapeAttr(id)}"${action ? '' : ' data-wl-inert="true"'}>
97
+ ${serviceField}
98
+ ${dateTime}
99
+ ${party}
100
+ ${contact}
101
+ ${phone}
102
+ ${notes}
103
+ <button type="submit"${action ? '' : ' disabled'}>${escapeHtml(submitLabel)}</button>
104
+ ${action ? '' : '<p class="note">This booking form needs a runtime to be wired before it can send.</p>'}
105
+ </form>
106
+ </div>
107
+ </section>`;
108
+ }
109
+ export const booking = {
110
+ type: 'booking',
111
+ description: 'An appointment/reservation request form (service, date, time, contact) that posts to a host-provided runtime. For booking-driven businesses — salons, clinics, gyms, hotels, events. No raw HTML.',
112
+ schema,
113
+ css,
114
+ render,
115
+ island: 'booking',
116
+ runtime: { capabilities: [CAPABILITY] },
117
+ };
@@ -0,0 +1,2 @@
1
+ import type { BlockSpec } from '../registry.js';
2
+ export declare const hours: BlockSpec;