npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@e280/sly

v0.3.7

Published

web shadow views

Downloads

2,473

Readme

🦝 sly

npm install lit @e280/sly @e280/strata @e280/stz

@e280's lit-based web library for reactive light or shadow views

  • 🎭 #views, reactive lit views, light-dom or shadow-dom
  • 🪝 #hooks, react-like composable hooks
  • #spinners, display async operations with animations
  • 💅 #spa, tiny router for hashy little single-page-apps
  • 🪙 #loot, drag-and-drop facilities
  • 🪄 #dom, the "it's not jquery" multitool
  • 🧪 https://sly.e280.org/ sly's testing page

🎭 views

reactive lit-html views

  • 🔮 see codepen demo, plain html (no build!)
  • 🌗 light or shadow, render nakedly on the page, or within a cozy shadow bubble
  • 🪝 hooks-based, familiar react-style hooks
  • auto-reactive, views magically rerender on strata-compatible state changes
  • 🪶 no compile step, just god's honest javascript via lit-html tagged-templates
  • 🧩 not web components, no dom registration needed, just vibes and good typings
import {html} from "lit"
import {light, shadow, dom} from "@e280/sly"

export const MyLightView = light(() => html`<p>blinded by the light</p>`)

export const MyShadowView = shadow(() => html`<p>shrouded in darkness</p>`)

🌞 light views

lit, signals, hooks — life is joyous again

  • define a light view
    import {html} from "lit"
    import {light, useSignal} from "@e280/sly"
    
    export const MyCounter = light((start: number) => {
      const $count = useSignal(start)
      const increment = () => $count.value++
    
      return html`
        <button @click="${increment}">${$count.value}</button>
      `
    })
  • render it into the dom
    dom.render(dom(".demo"), html`
      <h1>my cool counter demo</h1>
      ${MyCounter(123)}
    `)
  • remember, light views are naked.
    so they don't have a containing host element,
    and they can't have their own styles.

🌚 shadow views

each shadow view gets its own cozy shadow-dom bubble, which scopes local css, and also supports slotting

  • define a shadow view
    import {css, html} from "lit"
    import {shadow, useName, useCss, useSignal} from "@e280/sly"
    
    export const MyShadowCounter = shadow((start: number) => {
      useName("counter")
      useCss(css`button { color: cyan }`)
    
      const $count = useSignal(start)
      const increment = () => $count.value++
    
      return html`
        <button @click="${increment}">${$count.value}</button>
        <slot></slot>
      `
    })
  • render it into the dom
    dom.render(dom(".demo"), html`
      <h1>my cool counter demo</h1>
      ${MyShadowCounter(234)}
    `)
    • shadow views have a host element, rendered output looks like:
      <h1>my cool counter demo</h1>
      <sly-shadow view="counter"></sly-shadow>
  • .with to nest children or set attrs
    dom.render(dom(".demo"), html`
      <h1>my cool counter demo</h1>
    
      ${MyShadowCounter.with({
        props: [234],
        attrs: {"data-whatever": 555},
        children: html`
          <p>woah, slotting support!</p>
        `,
      })}
    `)
  • you can do custom shadow setup if needed (default shown)
    import {SlyShadow} from "@e280/sly"
    
    const customShadow = shadow.setup(() => {
      SlyShadow.register()
      const host = document.createElement("sly-shadow")
      const shadow = host.attachShadow({mode: "open"})
      return {host, shadow}
    })
    
    const MyShadowView = customShadow(() => html`<p>shrouded in darkness</p>`)

🍨 web components

web-native custom elements

  • they use hooks like the views, but they don't take props
    import {html} from "lit"
    import {lightElement, shadowElement} from "@e280/sly"
    
    const MyLight = lightElement(() => html`hello`)
    const MyShadow = shadowElement(() => html`hello`)
    
    dom.register({MyLight, MyShadow})
    <my-light></my-light>
    <my-shadow></my-shadow>

🪝 hooks

composable view state and utilities

👮 follow the hooks rules, or you go to hooks jail

just like react hooks, the execution order of hooks seriously matters.

you must not call these hooks under if-conditionals, or for-loops, or inside callback functions, or after a conditional return statement, or anything like that.. otherwise, heed my warning: weird bad stuff will happen..

🌚 shadow-only hooks

  • useName, set the "view" attribute value
    useName("squarepants")
      // <sly-shadow view="squarepants">
  • useCss, attach stylesheets (use lit's css!) to the shadow root
    useCss(css1, css2, css3)
  • useHost, get the host element
    const host = useHost()
  • useShadow, get the shadow root
    const shadow = useShadow()
  • useAttrs, access host element attributes (and rerender on attr changes)
    const attrs = useAttrs({
      name: String,
      count: Number,
      active: Boolean,
    })
    
    attrs.count = 123 // set the attr

🌞 universal hooks

  • useState, react-like hook to create some reactive state (we prefer signals)
    const [count, setCount] = useState(0)
    
    const increment = () => setCount(n => n + 1)
  • useRef, react-like hook to make a non-reactive box for a value
    const ref = useRef(0)
    
    ref.current // 0
    ref.current = 1 // does not trigger rerender
  • useSignal, create a strata signal
    const $count = useSignal(1)
    
    // read the signal
    $count()
    
    // write the signal
    $count(2)
  • useDerived, create a strata derived signal
    const $product = useDerived(() => $count() * $whatever())
  • useEffect, run a fn whenever strata state changes
    useEffect(() => console.log($count))
  • useOnce, run fn at initialization, and return a value
    const whatever = useOnce(() => {
      console.log("happens one time")
      return 123
    })
    
    whatever // 123
  • useMount, setup mount/unmount lifecycle
    useMount(() => {
      console.log("mounted")
      return () => console.log("unmounted")
    })
  • useWake, run fn each time mounted, and return value
    const whatever = useWake(() => {
      console.log("mounted")
      return 123
    })
    
    whatever // 123
  • useLifecycle, mount/unmount lifecycle, but also return a value
    const whatever = useLifecycle(() => {
      console.log("mounted")
      const value = 123
      return [value, () => console.log("unmounted")]
    })
    
    whatever // 123
  • useRender, returns a fn to rerender the view (debounced)
    const render = useRender()
    
    render().then(() => console.log("render done"))
  • useRendered, get a promise that resolves after the next render
    useRendered().then(() => console.log("rendered"))
  • useWait, start loading a strata#wait signal
    const $wait = useWait(async() => {
      await nap(2000)
      return 123
    })
    • look at the current Wait state
      $wait()
        // {done: true, ok: true, value: 123}
    • await for when the value is ready
      await $wait.ready
        // 123
  • useWaitResult, start a strata#wait, but with a formal stz#ok ok/err result
    const $wait = useWaitResult(async() => {
      await nap(2000)
      return (Math.random() > 0.5)
        ? ok(123)
        : err("uh oh")
    })

🧑‍🍳 happy hooks recipes

  • make a ticker, mount, cycle, and nap
    import {cycle, nap} from "@e280/stz"
    const $seconds = useSignal(0)
    
    useMount(() => cycle(async() => {
      await nap(1000)
      $seconds.value++
    }))
  • wake + rendered, to do something after each mount's first render
    const rendered = useRendered()
    
    useWake(() => rendered.then(() => {
      console.log("after first render")
    }))

⏳️ spinners

animated loading spinners

⏳️ stuff you'll need

  • from stz#ok — ok/err, formal error handling
    import {ok, err, nap} from "@e280/stz"
  • from strata#wait — wait, async operation state
    import {wait} from "@e280/strata"

⏳️ ui jumpstart

  • okay, so let's just do a loading spinner example
    import {html} from "lit"
    import {shadow, useWait, spinner} from "@e280/sly"
    
    const MyView = shadow(() => {
    
      // ⏳️ create a $wait signal
      const $wait = useWait(async() => {
        await nap(2000) // contrived async job
        return 123 // return a value
      })
    
      // ⏳️ ui display for the changing $wait signal
      return spinner($wait(), value => html`
        <p>done, the value is ${value}</p>
      `)
    })
    • while the async fn is running, an animated spinner will be shown
    • when the async fn resolves, our little <p> tag will render
    • if the async fn errors out, the error message will be displayed in red
  • stock spinners for your convenience (earth is my favorite)
    import {spinner, dotsSpinner, waveSpinner, earthSpinner, moonSpinner} from "@e280/sly"

⏳️ make your own spinners

  • it's easy
    import {makeSpinner, makeAsciiAnim, ErrorDisplay} from "@e280/sly"
    
    export const pieSpinner = makeSpinner(
      makeAsciiAnim(10, ["◷", "◶", "◵", "◴"]),
      ErrorDisplay,
    )
    • so makeSpinner accepts two views, one for the loading state, and one for the error state
    • feel free to make your own views

💅 spa

tiny router for cozy single page apps

import {derived} from "@e280/strata"
import {router, norm, hashNav, hashSignal} from "@e280/sly"

the spa router is agnostic about whether you're routing location.hash or location.pathname or otherwise.

  • router
    const route = router({
      "": () => "home", // 💁 routes can return anything
      "settings": () => "settings",
    
      // 🧩 params use braces
      "user/{id}": params => `user ${params.id}`,
    
      // 🔀 subpath with {*}
      "user/{id}/{*}": (params, subpath) => `user ${params.id} ${subpath}`,
    })
    you get a fn that resolves the path you give it
    route("")
      // "home"
    
    route("settings")
      // "settings"
    
    route("user/123/profile")
      // "user 123 profile"
    
    route("unknown/whatever")
      // undefined
  • norm fn chops off leading slashes and/or hash chars
    route(norm(location.hash))
      // "#/settings" -> "settings"
    route(norm(location.pathname))
      // "/settings" -> "settings"
  • subrouting pattern
    // here's a subrouter
    const user = (params: {id: string}) => router({
      "profile": () => `user ${params.id} profile`,
      "invites": () => `user ${params.id} invites`,
    })
    
    // here's the main router, where we can nest the subrouter
    const route = router({
    
      // this {*} captures the rest of the string, we pass it to the subrouter
      "user/{id}/{*}": (params, subpath) => user(params)(subpath),
    })
    route("user/123/profile")
      // "user 123 profile"

now, if you want to setup location.hash routing, you might want these primitives.

  • hashNav fn to trigger navigations
    const go = hashNav({
      home: () => ``,
      settings: () => `settings`,
      user: (id: string) => `user/${id}`,
      userProfile: (id: string) => `user/${id}/profile`,
      userInvites: (id: string) => `user/${id}/invites`,
    })
    
    go.settings()
      // navigates to "#/settings"
    
    go.user("123")
      // navigates to "#/user/123"
  • hashSignal create a strata signal for the current normalized location.hash
    const $hash = hashSignal()
    $hash.value
      // "user/123/profile"
    • the signal value auto-updates whenever the hash changes
    • the value is run through the norm fn to chop off the leading #/
    • whenever the hash changes, it runs cleanHash fn which aesthetically converts e280.org/#/ to just e280.org/ in the address bar
  • you should setup a derived signal that routes whenever that hash signal changes
    const $content = derived(() => route($hash()))
      // "user 123 profile"
    then you can plop that content into your lit html
    html`
      <div>
        ${$content()}
      </div>
    `

🪙 loot

drag-and-drop facilities

import {loot, view, dom} from "@e280/sly"
import {ev} from "@e280/stz"

🪙 loot.Drops

accept the user dropping stuff like files onto the page

  • setup drops
    const drops = new loot.Drops({
      predicate: loot.hasFiles,
      acceptDrop: event => {
        const files = loot.files(event)
        console.log("files dropped", files)
      },
    })
  • attach event listeners to your dropzone, one of these ways:
    • view example
      light(() => html`
        <div
          ?data-indicator="${drops.$indicator()}"
          @dragover="${drops.dragover}"
          @dragleave="${drops.dragleave}"
          @drop="${drops.drop}">
            my dropzone
        </div>
      `)
    • vanilla-js whole-page example
      // attach listeners to the body
      ev(document.body, {
        dragover: drops.dragover,
        dragleave: drops.dragleave,
        drop: drops.drop,
      })
      
      // sly attribute handler for the body
      const attrs = dom.attrs(document.body).spec({
        "data-indicator": Boolean,
      })
      
      // sync the data-indicator attribute
      drops.$indicator.on(bool => attrs["data-indicator"] = bool)
  • flashy css indicator for the dropzone, so the user knows your app is eager to accept the drop
    [data-indicator] {
      border: 0.5em dashed cyan;
    }

🪙 loot.DragAndDrops

setup drag-and-drops between items within your page

  • declare types for your draggy and droppy things
    // money that can be picked up and dragged
    type Money = {value: number}
      // dnd will call this a "draggy"
    
    // bag that money can be dropped into
    type Bag = {id: number}
      // dnd will call this a "droppy"
  • make your dnd
    const dnd = new loot.DragAndDrops<Money, Bag>({
      acceptDrop: (event, money, bag) => {
        console.log("drop!", {money, bag})
      },
    })
  • attach dragzone listeners (there can be many dragzones...)
    light(() => {
      const money = useOnce((): Money => ({value: 280}))
      const dragzone = useOnce(() => dnd.dragzone(() => money))
    
      return html`
        <div
          draggable="${dragzone.draggable}"
          @dragstart="${dragzone.dragstart}"
          @dragend="${dragzone.dragend}">
            money ${money.value}
        </div>
      `
    })
  • attach dropzone listeners (there can be many dropzones...)
    light(() => {
      const bag = useOnce((): Bag => ({id: 1}))
      const dropzone = useOnce(() => dnd.dropzone(() => bag))
      const indicator = !!(dnd.dragging && dnd.hovering === bag)
    
      return html`
        <div
          ?data-indicator="${indicator}"
          @dragenter="${dropzone.dragenter}"
          @dragleave="${dropzone.dragleave}"
          @dragover="${dropzone.dragover}"
          @drop="${dropzone.drop}">
            bag ${bag.id}
        </div>
      `
    })

🪙 loot helpers

  • loot.hasFiles(event) — return true if DragEvent contains any files (useful in predicate)
  • loot.files(event) — returns an array of files in a drop's DragEvent (useful in acceptDrop)

🪄 dom

the "it's not jquery!" multitool

import {dom} from "@e280/sly"

🪄 dom queries

  • require an element
    dom(".demo")
      // HTMLElement (or throws)
    // alias
    dom.require(".demo")
      // HTMLElement (or throws)
  • maybe get an element
    dom.maybe(".demo")
      // HTMLElement | undefined
  • all matching elements in an array
    dom.all(".demo ul li")
      // HTMLElement[]

🪄 dom.in scope

  • make a scope
    dom.in(".demo") // selector
      // Dom instance
    dom.in(demoElement) // element
      // Dom instance
  • run queries in that scope
    dom.in(demoElement).require(".button")
    dom.in(demoElement).maybe(".button")
    dom.in(demoElement).all("ol li")

🪄 dom utilities

  • dom.register web components
    dom.register({MyComponent, AnotherCoolComponent})
      // <my-component>
      // <another-cool-component>
    • dom.register automatically dashes the tag names (MyComponent becomes <my-component>)
  • dom.render content into an element
    dom.render(element, html`<p>hello world</p>`)
    dom.in(".demo").render(html`<p>hello world</p>`)
  • dom.el little element builder
    const div = dom.el("div", {"data-whatever": 123, "data-active": true})
      // <div data-whatever="123" data-active></div>
  • dom.elmer make an element with a fluent chain
    const div = dom.elmer("div")
      .attr("data-whatever", 123)
      .attr("data-active")
      .children("hello world")
      .done()
        // HTMLElement
  • dom.mk make an element with a lit template (returns the first)
    const div = dom.mk(html`
      <div data-whatever="123" data-active>
        hello world
      </div>
    `) // HTMLElement
  • dom.events to attach event listeners
    const detach = dom.events(element, {
      keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
      keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
    })
    const detach = dom.in(".demo").events({
      keydown: (e: KeyboardEvent) => console.log("keydown", e.code),
      keyup: (e: KeyboardEvent) => console.log("keyup", e.code),
    })
    // unattach those event listeners when you're done
    detach()
  • dom.attrs to setup a type-happy html attribute helper
    const attrs = dom.attrs(element).spec({
      name: String,
      count: Number,
      active: Boolean,
    })
    const attrs = dom.in(".demo").attrs.spec({
      name: String,
      count: Number,
      active: Boolean,
    })
    attrs.name // "chase"
    attrs.count // 123
    attrs.active // true
    attrs.name = "zenky"
    attrs.count = 124
    attrs.active = false // removes html attr
    attrs.name = undefined // removes the attr
    attrs.count = undefined // removes the attr
    or if you wanna be more loosey-goosey, skip the spec
    const {attrs} = dom.in(".demo")
    attrs.strings.name = "pimsley"
    attrs.numbers.count = 125
    attrs.booleans.active = true

🧑‍💻 sly is by e280

reward us with github stars
build with us at https://e280.org/ but only if you're cool