@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/AGENT.md +5 -5
- package/CATALOG.md +66 -2
- package/CHANGELOG.md +49 -0
- package/README.md +54 -5
- package/catalog.json +412 -1
- package/lib/blocks/booking.d.ts +2 -0
- package/lib/blocks/booking.js +117 -0
- package/lib/blocks/hours.d.ts +2 -0
- package/lib/blocks/hours.js +97 -0
- package/lib/blocks/menu.d.ts +2 -0
- package/lib/blocks/menu.js +93 -0
- package/lib/blocks/product.d.ts +2 -0
- package/lib/blocks/product.js +92 -0
- package/lib/blocks/reviews.d.ts +2 -0
- package/lib/blocks/reviews.js +96 -0
- package/lib/generate.d.ts +19 -2
- package/lib/generate.js +43 -3
- package/lib/index.d.ts +3 -1
- package/lib/index.js +2 -0
- package/lib/islands/hours.d.ts +1 -0
- package/lib/islands/hours.js +69 -0
- package/lib/registry.js +9 -4
- package/lib/templates.d.ts +37 -0
- package/lib/templates.js +450 -0
- package/lib/verticals.d.ts +40 -0
- package/lib/verticals.js +117 -0
- package/package.json +2 -1
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.
|
|
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,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
|
+
};
|