FeatureList
Features in alternating rows of words and a picture (or code), with ticked points and a link each.
Demo
Write
The page is the program
A route returns HTML; a view is a function that makes some. There is no template language to learn beside the one you write.
- JSX-like markup, escaped by default
- Views compose like tags
- Styles scoped to the component
Watch
The page follows the data
A live query keeps the page current as rows change, over one connection, with nothing to wire up.
- Live queries
- Streams and channels
Ship
One binary, one file
Copy them to a server and run. It serves, watches, reloads and logs.
sh scp app.qwe box:~ && ssh box quillo run app.qwe --port 80
Words first, then the picture; the next row the other way round. On a phone each row stacks.
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/feature_list.qweimport ui/theme.qwe use theme_cssuse ui/feature_list.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
let feature_list_demo_shot = (a: str, b: str) => `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 240'%3E%3Crect width='400' height='240' rx='12' fill='${a}'/%3E%3Crect x='30' y='30' width='340' height='28' rx='6' fill='${b}'/%3E%3Crect x='30' y='78' width='220' height='16' rx='4' fill='${b}'/%3E%3Crect x='30' y='106' width='300' height='16' rx='4' fill='${b}'/%3E%3Crect x='30' y='150' width='340' height='60' rx='8' fill='${b}'/%3E%3C/svg%3E`view FeatureListDemo() = <div.demo-stack> <FeatureList id="feature-list-demo" features={[ { eyebrow: "Write", title: "The page is the program", text: "A route returns HTML; a view is a function that makes some. There is no template language to learn beside the one you write.", image: feature_list_demo_shot("%23ecebfd", "%23b9a8ff"), image_alt: "A page sketched as grey blocks", points: ["JSX-like markup, escaped by default", "Views compose like tags", "Styles scoped to the component"], href: "#feature_list", link: "About views" }, { eyebrow: "Watch", title: "The page follows the data", text: "A live query keeps the page current as rows change, over one connection, with nothing to wire up.", image: feature_list_demo_shot("%23e3f5ec", "%234cc38a"), image_alt: "A list sketched as green blocks", points: ["Live queries", "Streams and channels"] }, { eyebrow: "Ship", title: "One binary, one file", text: "Copy them to a server and run. It serves, watches, reloads and logs.", side: <CodeBlock code="scp app.qwe box:~ && ssh box quillo run app.qwe --port 80" language="sh"/> }, ]}/> <p.demo-note>Words first, then the picture; the next row the other way round. On a phone each row stacks.</p> </div>Source
// FeatureList: a product's features one by one, each a row of words beside a// picture, the sides swapping from row to row.//// <FeatureList features={[// { title: "Write the page", text: "HTML is a value: a view returns it.", image: "/img/write.png",// points: ["JSX-like markup", "Escaped by default"], href: "/docs/views", link: "About views" },// { title: "Watch it change", text: "A live query keeps the page current.", image: "/img/live.png" },// ]}/>//// features: a list of { title, text?, image, image_alt?, points?, href?, link?,// eyebrow? }. image: the picture's URL; side: any HTML instead (code, a video).// points: short lines listed with ticks under the text. href and link: a link// after them ("Learn more" by default). eyebrow: a small line above the title.// start: which side the first row's picture is on, "end" (default: the words// first) or "start". On a phone each row stacks, the picture under the words.import ui/theme.qwe use ui_clslet feature_list_css = css`.ui-feature-list { display: grid; gap: 56px; margin: 0; padding: 0; list-style: none; font: 15px/1.55 var(--ui-font); color: var(--ui-fg); }.ui-feature-row { display: grid; gap: 32px 48px; align-items: center; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }.ui-feature-row-flip .ui-feature-row-words { order: 2; }.ui-feature-row-words { display: grid; gap: 10px; justify-items: start; min-width: 0; }.ui-feature-row-eyebrow { margin: 0; color: var(--ui-accent); font-size: 13px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase; }.ui-feature-row-title { margin: 0; font-size: clamp(22px, 3vw, 28px); font-weight: 700; line-height: 1.2; letter-spacing: -.015em; }.ui-feature-row-text { margin: 0; color: var(--ui-muted); font-size: 16px; }.ui-feature-row-points { display: grid; gap: 6px; margin: 4px 0 0; padding: 0; list-style: none; }.ui-feature-row-points li { display: flex; gap: 8px; align-items: baseline; }.ui-feature-row-points svg { flex: none; width: 14px; height: 14px; margin-top: 2px; color: var(--ui-success); }.ui-feature-row-link { margin-top: 4px; color: var(--ui-accent); font-weight: 600; text-decoration: none; border-radius: 3px; }.ui-feature-row-link:hover { text-decoration: underline; }.ui-feature-row-link:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-feature-row-side { min-width: 0; }.ui-feature-row-side > img { display: block; width: 100%; height: auto; border-radius: var(--ui-radius); border: 1px solid var(--ui-border); }.ui-feature-row-side > :first-child { margin-top: 0; }@media (max-width: 760px) { .ui-feature-list { gap: 40px; } .ui-feature-row { grid-template-columns: minmax(0, 1fr); } .ui-feature-row-flip .ui-feature-row-words { order: 0; }}`view FeatureList(features: list, start: str = "end", id: str? = null, class: str? = null) { style global feature_list_css <ul id={id} class={ui_cls(["ui-feature-list", class])}> {for [i, f] in enumerate(features) => <li class={ui_cls(["ui-feature-row", (i % 2 == 0) == (start == "start") && "ui-feature-row-flip"])}> <div.ui-feature-row-words> {f.eyebrow != null && <p.ui-feature-row-eyebrow>{f.eyebrow}</p>} <h3.ui-feature-row-title>{f.title}</h3> {f.text != null && <p.ui-feature-row-text>{f.text}</p>} {f.points != null && len(f.points) > 0 && <ul.ui-feature-row-points> {for p in f.points => <li> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8.5l3 3 7-7"/></svg> <span>{p}</span> </li>} </ul>} {f.href != null && <a.ui-feature-row-link href={f.href}>{f.link ?? "Learn more"} →</a>} </div> {(f.side != null || f.image != null) && <div.ui-feature-row-side> {f.side ?? <img src={f.image} alt={f.image_alt ?? ""} loading="lazy"/>} </div>} </li>} </ul>}Related
Thread 0 posts
On FeatureList: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.