@instafy/cli 0.2.2 → 0.2.4
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 +60 -3
- package/dist/bind/LICENSE +661 -0
- package/dist/bind/bin/instafy-bind.mjs +542 -0
- package/dist/bind/client/bind-client.js +1219 -0
- package/dist/bind/src/apply.mjs +293 -0
- package/dist/bind/src/bound.mjs +202 -0
- package/dist/bind/src/budget.mjs +58 -0
- package/dist/bind/src/check.mjs +176 -0
- package/dist/bind/src/config.mjs +180 -0
- package/dist/bind/src/controller.mjs +244 -0
- package/dist/bind/src/executors/agent.mjs +336 -0
- package/dist/bind/src/executors/script.mjs +163 -0
- package/dist/bind/src/handler-kit.mjs +150 -0
- package/dist/bind/src/init.mjs +238 -0
- package/dist/bind/src/jobs.mjs +316 -0
- package/dist/bind/src/net.mjs +281 -0
- package/dist/bind/src/page-fetch.mjs +43 -0
- package/dist/bind/src/preview-host.mjs +114 -0
- package/dist/bind/src/protocol.mjs +103 -0
- package/dist/bind/src/records-schema.mjs +580 -0
- package/dist/bind/src/records.mjs +161 -0
- package/dist/bind/src/save-owner.mjs +492 -0
- package/dist/bind/src/schema-lite.mjs +212 -0
- package/dist/bind/src/server.mjs +908 -0
- package/dist/bind/src/sse.mjs +119 -0
- package/dist/bind/src/starters/blank/app.js +17 -0
- package/dist/bind/src/starters/blank/index.html +22 -0
- package/dist/bind/src/starters/checklist/app.js +171 -0
- package/dist/bind/src/starters/checklist/bound.json +29 -0
- package/dist/bind/src/starters/checklist/data.json +26 -0
- package/dist/bind/src/starters/checklist/index.html +23 -0
- package/dist/bind/src/starters/compare/app.js +316 -0
- package/dist/bind/src/starters/compare/bound.json +26 -0
- package/dist/bind/src/starters/compare/data.json +170 -0
- package/dist/bind/src/starters/compare/index.html +23 -0
- package/dist/bind/src/starters/handlers/read_offer.mjs +55 -0
- package/dist/bind/src/starters/handlers/set_check.mjs +41 -0
- package/dist/bind/src/starters/ranked/app.js +166 -0
- package/dist/bind/src/starters/ranked/bound.json +26 -0
- package/dist/bind/src/starters/ranked/data.json +142 -0
- package/dist/bind/src/starters/ranked/index.html +23 -0
- package/dist/bind/src/starters/site.js +888 -0
- package/dist/bind/src/starters/theme.css +492 -0
- package/dist/bind/src/starters/timeline/app.js +143 -0
- package/dist/bind/src/starters/timeline/data.json +41 -0
- package/dist/bind/src/starters/timeline/index.html +22 -0
- package/dist/bind/src/static.mjs +141 -0
- package/dist/bind/src/tokens.mjs +102 -0
- package/dist/bind/src/try.mjs +139 -0
- package/dist/bind/src/web.mjs +191 -0
- package/dist/cli.js +5031 -2446
- package/package.json +5 -4
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* Ranked starter: a top-N list ("best X", "top 10"). One layout at every width; the ranking is the order.
|
|
2
|
+
* You design the content in data.json; keep theme.css and site.js. Restyle only when asked.
|
|
3
|
+
*
|
|
4
|
+
* data.json:
|
|
5
|
+
* title, lede the page title and its one-line answer (the verdict, and the biggest unknown if there is one)
|
|
6
|
+
* basis how it is ranked, one line: "Ranked by safety reviews, then price."
|
|
7
|
+
* filter (only for a constraint the person stated): {key: "price", max: 300, label: "Under $300"}
|
|
8
|
+
* items: [{ref: "option:a", rank, name, why (one short line), badge?, specs: [fact] (at most 3), price?: fact,
|
|
9
|
+
* store?: {name, url, readable}, details?: [{label, fact}], sources?: [{name, url}]}]
|
|
10
|
+
* badge: only for a label the person must notice. "Our pick" only when it is your own pick and "why" says why;
|
|
11
|
+
* a source's pick or award for a row is that row's badge, saying whose ("BabyGearLab's top pick",
|
|
12
|
+
* "BabyGearLab: best budget"), never a "Details" toggle. Nothing is added by itself.
|
|
13
|
+
* price: the price as a fact (not in specs), with "asof" when the source dates it; a live check replaces it. A
|
|
14
|
+
* review site's price is its list price: "list": true, so the row says "list price" and never reads as the
|
|
15
|
+
* store's price now. When every price comes from one source on one date, one line under the list names the
|
|
16
|
+
* source once ("List prices on BabyGearLab ↗ · seen 5 Aug"), and "Sources" does not name it again.
|
|
17
|
+
* details: only facts the row does not already show.
|
|
18
|
+
* unchecked: ["what was not checked", …]
|
|
19
|
+
* fact: {"v": "$145", "src": "Example Store", "url": "https://…", "date": "2026-10-17", "est": false}
|
|
20
|
+
* The item shape is the compare starter's, so the same items work there ("Compare these", only when asked).
|
|
21
|
+
* Query: ?show=all&open=<slug>. Rows: id="row-<slug>", data-ref="<ref>".
|
|
22
|
+
* SPDX-License-Identifier: AGPL-3.0-only */
|
|
23
|
+
(function () {
|
|
24
|
+
"use strict";
|
|
25
|
+
var h = site.h;
|
|
26
|
+
|
|
27
|
+
site.start({
|
|
28
|
+
data: "data.json",
|
|
29
|
+
state: function (data) { return { show: data.filter ? "fit" : "all", open: "" }; },
|
|
30
|
+
select: function (ref) { return { open: site.slug(ref) }; },
|
|
31
|
+
render: render
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function priceOf(item) { return site.live(item.ref, "price") || item.price || null; }
|
|
35
|
+
function valueFor(item, key) {
|
|
36
|
+
if (key === "price") return priceOf(item);
|
|
37
|
+
var d = (item.details || []).filter(function (x) { return x.key === key; })[0];
|
|
38
|
+
return d ? d.fact : null;
|
|
39
|
+
}
|
|
40
|
+
function fits(data, item) {
|
|
41
|
+
var f = data.filter;
|
|
42
|
+
if (!f) return true;
|
|
43
|
+
var n = site.num(valueFor(item, f.key));
|
|
44
|
+
if (n == null) return true;
|
|
45
|
+
return (f.max == null || n <= f.max) && (f.min == null || n >= f.min);
|
|
46
|
+
}
|
|
47
|
+
function render(data, st) {
|
|
48
|
+
site.head(data); // title, lede and the basis line ("Ranked by …") come first, before any button
|
|
49
|
+
var items = (data.items || []).slice().sort(function (a, b) { return (a.rank || 0) - (b.rank || 0); });
|
|
50
|
+
var shown = st.show === "all" ? items : items.filter(function (i) { return fits(data, i); });
|
|
51
|
+
var hidden = items.length - shown.length;
|
|
52
|
+
var priced = items.some(function (i) { return i.price || i.store; });
|
|
53
|
+
var shared = sharedPrice(items);
|
|
54
|
+
|
|
55
|
+
var bar = document.getElementById("bar");
|
|
56
|
+
bar.replaceChildren();
|
|
57
|
+
if (data.filter) {
|
|
58
|
+
bar.appendChild(site.seg("show", "Show", [{ value: "all", label: "All" }, { value: "fit", label: data.filter.label }], st.show,
|
|
59
|
+
function (v) { site.setState({ show: v }); }));
|
|
60
|
+
}
|
|
61
|
+
if (priced) site.append(bar, site.checkAll(items));
|
|
62
|
+
bar.hidden = !bar.firstChild;
|
|
63
|
+
|
|
64
|
+
var list = document.getElementById("list");
|
|
65
|
+
list.replaceChildren();
|
|
66
|
+
if (!items.length) {
|
|
67
|
+
list.appendChild(site.empty("Nothing ranked yet.", null, "Ask in chat to add some."));
|
|
68
|
+
} else {
|
|
69
|
+
var wrap = h("div", { "class": "stack" });
|
|
70
|
+
var stores = priced ? site.priceNotice(items) : null; // the reason a store can't be read, said once
|
|
71
|
+
// every store refuses checks from the start and one line already names the list prices: that line says where
|
|
72
|
+
// today's price is ("Open a store for today's price."), instead of a box about checks nobody was offered
|
|
73
|
+
var inline = !!(shared && stores && site.storesBlocked(items));
|
|
74
|
+
if (inline) stores = null;
|
|
75
|
+
wrap.appendChild(h("ol", { "class": "list ranked" }, shown.map(function (item) { return row(item, st, priced, shared); })));
|
|
76
|
+
if (hidden > 0) {
|
|
77
|
+
wrap.appendChild(h("p", { "class": "cap" }, hidden + " hidden · ",
|
|
78
|
+
h("button", { "class": "link-btn", text: "Show all", attrs: { type: "button", "data-key": "show-all" }, on: { click: function () { site.setState({ show: "all" }); } } })));
|
|
79
|
+
}
|
|
80
|
+
if (shared) wrap.appendChild(sharedLine(shared, inline));
|
|
81
|
+
if (stores) wrap.appendChild(stores);
|
|
82
|
+
list.appendChild(wrap);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var sources = [];
|
|
86
|
+
items.forEach(function (i) {
|
|
87
|
+
sources = sources.concat(site.sourcesOf((i.specs || []).concat([i.price])));
|
|
88
|
+
(i.sources || []).forEach(function (s) { sources.push(s); });
|
|
89
|
+
});
|
|
90
|
+
// the line under the list already names the prices' source: "Sources" does not say it a second time
|
|
91
|
+
if (shared) sources = sources.filter(function (s) { return s.name !== shared.src && !(site.host(s.url) && site.host(s.url) === site.host(shared.url)); });
|
|
92
|
+
// the footer belongs to a list: an empty page has nothing to confirm
|
|
93
|
+
site.notes(items.length ? data.unchecked : [], items.length ? sources : []);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The price fact every priced row shares (same source and date), else null. */
|
|
97
|
+
function sharedPrice(items) {
|
|
98
|
+
var facts = items.map(function (i) { return i.price; }).filter(Boolean);
|
|
99
|
+
if (facts.length < 2) return null;
|
|
100
|
+
var key = function (f) { return f && f.src && !f.est && f.v != null ? [f.src, f.asof || f.date || "", f.list ? "list" : ""].join("|") : null; };
|
|
101
|
+
var k = key(facts[0]);
|
|
102
|
+
return k && facts.every(function (f) { return key(f) === k; }) ? facts[0] : null;
|
|
103
|
+
}
|
|
104
|
+
/** "List prices on BabyGearLab ↗ · seen 5 Aug" ("Prices: Store ↗ · …" for a store's own), said once under the
|
|
105
|
+
* list (" · may have changed" when it is old; "Open a store for today's price." when no store can be checked, as its own sentence, so no wrapped line starts with a dot). */
|
|
106
|
+
function sharedLine(f, openStore) {
|
|
107
|
+
var when = site.seen(f);
|
|
108
|
+
return h("p", { "class": "cap shared-line" }, f.list ? "List prices on " : "Prices: ", site.link(f.url, f.src), when ? [" · ", h("span", { "class": "nowrap", text: when })] : "",
|
|
109
|
+
site.old(f, true) ? " · may have changed" : "", openStore ? ". Open a store for today's price." : "");
|
|
110
|
+
}
|
|
111
|
+
/** Details holds only what the row does not already show, and a source link once: a page on a site the row's
|
|
112
|
+
* facts already link to is not repeated. */
|
|
113
|
+
function extras(item) {
|
|
114
|
+
var onRow = {};
|
|
115
|
+
(item.specs || []).concat([item.price]).forEach(function (f) { var v = site.valueText(f); if (v) onRow[v.toLowerCase()] = 1; });
|
|
116
|
+
var facts = (item.details || []).filter(function (d) { var v = site.valueText(d.fact); return v && !onRow[v.toLowerCase()]; }).slice(0, 8);
|
|
117
|
+
var linked = {};
|
|
118
|
+
(item.specs || []).concat([item.price], facts.map(function (d) { return d.fact; })).forEach(function (f) {
|
|
119
|
+
if (f && f.url) { linked[f.url] = 1; if (site.host(f.url)) linked["@" + site.host(f.url)] = 1; }
|
|
120
|
+
});
|
|
121
|
+
var sources = (item.sources || []).filter(function (s) { return s && !linked[s.url] && !linked["@" + site.host(s.url)]; });
|
|
122
|
+
return { facts: facts, sources: sources };
|
|
123
|
+
}
|
|
124
|
+
function hasMore(item) { var x = extras(item); return !!(x.facts.length || x.sources.length); }
|
|
125
|
+
|
|
126
|
+
function row(item, st, priced, shared) {
|
|
127
|
+
var open = st.open === site.slug(item.ref);
|
|
128
|
+
// a badge only when the data names one ("Our pick" when the reason is in "why"; "BabyGearLab's top pick" when a
|
|
129
|
+
// source picked it). Never added by itself.
|
|
130
|
+
var badge = item.badge || null;
|
|
131
|
+
var specs = (item.specs || []).slice(0, 3).map(function (f) {
|
|
132
|
+
var v = site.valueText(f);
|
|
133
|
+
return v && !f.src && !f.est ? v + ", not checked" : v;
|
|
134
|
+
}).filter(Boolean);
|
|
135
|
+
var live = site.live(item.ref, "price");
|
|
136
|
+
var details = hasMore(item) ? h("button", {
|
|
137
|
+
"class": "btn quiet more-btn",
|
|
138
|
+
text: open ? "Hide details" : "Details",
|
|
139
|
+
attrs: { type: "button", "aria-expanded": open ? "true" : "false", "aria-label": (open ? "Hide details for " : "Details for ") + item.name, "data-key": "details:" + item.ref },
|
|
140
|
+
on: { click: function () { site.setState({ open: open ? "" : site.slug(item.ref) }); } }
|
|
141
|
+
}) : null;
|
|
142
|
+
return h("li", { "class": "row", attrs: site.rowAttrs(item.ref) },
|
|
143
|
+
h("span", { "class": "rank", text: item.rank, attrs: { "aria-hidden": "true" } }),
|
|
144
|
+
h("div", { "class": "row-body" },
|
|
145
|
+
// "Details" stays at the end of the first line even when a badge wraps under the name
|
|
146
|
+
h("div", { "class": "name-line" },
|
|
147
|
+
h("div", { "class": "name-wrap" }, h("span", { "class": "name" }, h("span", { "class": "sr-only", text: "Number " + item.rank + ": " }), item.name),
|
|
148
|
+
badge ? h("span", { "class": "badge", text: badge }) : null), details),
|
|
149
|
+
item.why ? h("p", { "class": "why", text: item.why }) : null,
|
|
150
|
+
specs.length ? h("p", { "class": "cap", text: specs.join(" · ") }) : null,
|
|
151
|
+
// every priced row has the same price line, with or without a store
|
|
152
|
+
priced ? site.trust(live || item.price || null, { label: "Price", price: true, ref: item.ref, intent: "read_offer", store: item.store, seed: item.price || null, shared: !live && !!shared && !!item.price }) : null,
|
|
153
|
+
open && hasMore(item) ? more(item) : null));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function more(item) {
|
|
157
|
+
var x = extras(item);
|
|
158
|
+
return h("div", { "class": "more" },
|
|
159
|
+
x.facts.length ? h("dl", { "class": "facts" }, x.facts.map(function (d) {
|
|
160
|
+
return h("div", null, h("dt", { text: d.label }), h("dd", null, site.trust(d.fact)));
|
|
161
|
+
})) : null,
|
|
162
|
+
x.sources.length ? h("p", { "class": "cap" }, "Sources: ", x.sources.map(function (s, i) {
|
|
163
|
+
return [i ? ", " : "", site.link(s.url, s.name)];
|
|
164
|
+
})) : null);
|
|
165
|
+
}
|
|
166
|
+
})();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"title": "Top three options",
|
|
4
|
+
"intents": {
|
|
5
|
+
"read_offer": {
|
|
6
|
+
"label": "Check price",
|
|
7
|
+
"describe": "Read one option's price from its store page (schema.org data). The URL comes from data.json, never from the click.",
|
|
8
|
+
"executor": "script",
|
|
9
|
+
"handler": "handlers/read_offer.mjs",
|
|
10
|
+
"params": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"required": ["ref"],
|
|
13
|
+
"additionalProperties": false,
|
|
14
|
+
"properties": {
|
|
15
|
+
"ref": { "type": "string", "maxLength": 80, "pattern": "^[a-z][a-z0-9-]{0,23}:[a-z0-9-]{1,50}$" }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"target": { "records": "data/records/read_offer.json" },
|
|
19
|
+
"lanes": ["local"],
|
|
20
|
+
"pending": "item",
|
|
21
|
+
"cooldown_s": 10,
|
|
22
|
+
"timeout_s": 45
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"results": { "files": ["data/records/read_offer.json"], "poll_s": 3, "sse": true }
|
|
26
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Top three options",
|
|
3
|
+
"lede": "Option A is the best all-round pick. Option C costs least, but its price was not checked.",
|
|
4
|
+
"basis": "Ranked by review scores, then price.",
|
|
5
|
+
"items": [
|
|
6
|
+
{
|
|
7
|
+
"ref": "option:option-a",
|
|
8
|
+
"rank": 1,
|
|
9
|
+
"name": "Option A",
|
|
10
|
+
"why": "Best reviewed, and the lightest",
|
|
11
|
+
"specs": [
|
|
12
|
+
{
|
|
13
|
+
"v": "8.9 oz",
|
|
14
|
+
"src": "Maker",
|
|
15
|
+
"url": "https://example.com/option-a/specs",
|
|
16
|
+
"date": "2026-10-17"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"v": "2-year warranty",
|
|
20
|
+
"src": "Maker",
|
|
21
|
+
"url": "https://example.com/option-a/warranty",
|
|
22
|
+
"date": "2026-10-17"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"price": {
|
|
26
|
+
"v": "$145",
|
|
27
|
+
"src": "Example Store",
|
|
28
|
+
"url": "https://example.com/option-a",
|
|
29
|
+
"date": "2026-10-17"
|
|
30
|
+
},
|
|
31
|
+
"store": {
|
|
32
|
+
"name": "Example Store",
|
|
33
|
+
"url": "https://example.com/option-a",
|
|
34
|
+
"readable": true
|
|
35
|
+
},
|
|
36
|
+
"details": [
|
|
37
|
+
{
|
|
38
|
+
"label": "Review score",
|
|
39
|
+
"fact": {
|
|
40
|
+
"v": "4.6 of 5",
|
|
41
|
+
"src": "Example Reviews",
|
|
42
|
+
"url": "https://example.com/reviews/option-a",
|
|
43
|
+
"date": "2026-10-16"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"label": "Colours",
|
|
48
|
+
"fact": {
|
|
49
|
+
"v": "Black, white",
|
|
50
|
+
"src": "Maker",
|
|
51
|
+
"url": "https://example.com/option-a/specs",
|
|
52
|
+
"date": "2026-10-17"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"sources": [
|
|
57
|
+
{
|
|
58
|
+
"name": "Example Reviews",
|
|
59
|
+
"url": "https://example.com/reviews/option-a"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"badge": "Our pick"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"ref": "option:option-b",
|
|
66
|
+
"rank": 2,
|
|
67
|
+
"name": "Option B",
|
|
68
|
+
"why": "Nearly as good, a little heavier",
|
|
69
|
+
"specs": [
|
|
70
|
+
{
|
|
71
|
+
"v": "about 9.5 oz",
|
|
72
|
+
"n": 9.5,
|
|
73
|
+
"est": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"v": "1-year warranty",
|
|
77
|
+
"src": "Maker",
|
|
78
|
+
"url": "https://example.com/option-b/warranty",
|
|
79
|
+
"date": "2026-10-17"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"price": {
|
|
83
|
+
"v": "$139",
|
|
84
|
+
"src": "Example Store",
|
|
85
|
+
"url": "https://example.com/option-b",
|
|
86
|
+
"date": "2026-10-17"
|
|
87
|
+
},
|
|
88
|
+
"store": {
|
|
89
|
+
"name": "Example Store",
|
|
90
|
+
"url": "https://example.com/option-b",
|
|
91
|
+
"readable": true
|
|
92
|
+
},
|
|
93
|
+
"details": [
|
|
94
|
+
{
|
|
95
|
+
"label": "Review score",
|
|
96
|
+
"fact": {
|
|
97
|
+
"v": "4.4 of 5",
|
|
98
|
+
"src": "Example Reviews",
|
|
99
|
+
"url": "https://example.com/reviews/option-b",
|
|
100
|
+
"date": "2026-10-16"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"ref": "option:option-c",
|
|
107
|
+
"rank": 3,
|
|
108
|
+
"name": "Option C",
|
|
109
|
+
"why": "The lowest price, fewer reviews",
|
|
110
|
+
"specs": [
|
|
111
|
+
{
|
|
112
|
+
"v": "9.8 oz",
|
|
113
|
+
"src": "Maker",
|
|
114
|
+
"url": "https://example.com/option-c/specs",
|
|
115
|
+
"date": "2026-10-17"
|
|
116
|
+
}
|
|
117
|
+
],
|
|
118
|
+
"price": {
|
|
119
|
+
"note": "the store blocks price checks"
|
|
120
|
+
},
|
|
121
|
+
"store": {
|
|
122
|
+
"name": "Other Store",
|
|
123
|
+
"url": "https://example.org/option-c",
|
|
124
|
+
"readable": false
|
|
125
|
+
},
|
|
126
|
+
"details": [
|
|
127
|
+
{
|
|
128
|
+
"label": "Review score",
|
|
129
|
+
"fact": {
|
|
130
|
+
"v": "4.1 of 5",
|
|
131
|
+
"src": "Example Reviews",
|
|
132
|
+
"url": "https://example.com/reviews/option-c",
|
|
133
|
+
"date": "2026-10-16"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
],
|
|
139
|
+
"unchecked": [
|
|
140
|
+
"Check Option B's weight on the maker's spec sheet before you buy"
|
|
141
|
+
]
|
|
142
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
|
7
|
+
<meta name="theme-color" content="#141414" media="(prefers-color-scheme: dark)">
|
|
8
|
+
<title>Top picks</title>
|
|
9
|
+
<link rel="stylesheet" href="theme.css">
|
|
10
|
+
<!-- Restyle only when asked: override --if-* tokens in a <style> here, after theme.css. -->
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<main class="page">
|
|
14
|
+
<header class="head"><h1 class="title" id="title">Top picks</h1><p class="lede" id="lede"></p></header>
|
|
15
|
+
<div class="bar" id="bar" hidden></div>
|
|
16
|
+
<section id="list"></section>
|
|
17
|
+
<footer class="notes" id="notes" hidden></footer>
|
|
18
|
+
</main>
|
|
19
|
+
<script src="__bind/client.js"></script>
|
|
20
|
+
<script src="site.js"></script>
|
|
21
|
+
<script src="app.js"></script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|