Marketing

FeatureList

Features in alternating rows of words and a picture (or code), with ticked points and a link each.

#marketing#section#list#image#no-js

Demo

Preview
  • 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
    About views →
    A page sketched as grey blocks
  • 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
    A list sketched as green blocks
  • 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.qwe
main.qwe
import 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.

demos/feature_list.qwe
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

ui/feature_list.qwe
// 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>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On FeatureList: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.