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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@nichoth/components

v0.16.15

Published

A collection of UI components

Downloads

1,433

Readme

components

module types Socket Badge license

A collection of UI components made with preact and tonic.

See a live demo

install

npm i -S @nichoth/components

use

I recommend using this with vite + ESM, because it is easy. These are preact and tonic components; you will need to install preact or tonic.

The preact version is recommended, because some of the animations do not work well in the tonic version. In particular the radio-group, and hamburger components do not work well.

preact + htm

Use preact with tagged template literals.

[!IMPORTANT]
This is the preferred way to consume these, with htm + preact.

Import components from @nichoth/components/htm/*.

import { html } from 'htm/preact'
import { render, FunctionComponent } from 'preact'
import { TextInput } from '@nichoth/components/htm/text-input'
import '@nichoth/components/text-input.css'

const Example:FunctionComponent<{}> = function () {
    return html`
        <div>
            <h3>Text Input</h3>
            <${TextInput}
                displayName="htm text input"
                required=${true}
                minLength=${3}
                maxLength=${7}
                name=${'htm-text-input'}
            ><//>
        </div>
    `
}

render(html`<${Example}><//>`, document.getElementById('root')!)

preact + JSX

Use preact with JSX

import { render, FunctionComponent } from 'preact'
import { TextInput } from '@nichoth/components/preact/text-input'
import '@nichoth/components/text-input.css'

const Example:FunctionComponent = function () {
    return <div>
        <h3>Text Input</h3>
        <form className="example-form">
            <TextInput name="text" displayName="Input test" />
        </form>
    </div>
}

render(<Example />, document.getElementById('root')!)

tonic

Use web components with Tonic

import Tonic from '@nichoth/tonic'
import { SpinningButton } from '@nichoth/components/tonic/spinning-button'
import '@nichoth/components/text-input.css'

export class TonicExample extends Tonic {
    state = {
        isSpinning: false,
    }

    render () {
        return this.html`<div>
            <spinning-button
                isSpinning=${this.state.isSpinning}
                data-event="click-the-button"
            >
                click here
            </spinning-button>
        </div>`
    }
}

Tonic.add(SpinningButton)
Tonic.add(TonicExample)

globals

We use these CSS variables

:root {
    --transition-time: 0.2s;
    --button-outine-color: black;
    --button-primary-outline: #0077ff;
    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);
    --button-outline-disabled-ol: #0077ff5c;
    --text-input-error-border: red;
    --text-input-error-text: red;
    --hamburger-color: black;
    --fade-in-time: 0.2s;  /* for the mobile nav fade-in animation */
}

Define them in your application to customize.

API

css

Import the css variables in addition to the per-component css.

With Vite as bundler:

import '@nichoth/components/variables.css'

Switch

See ./src/htm/switch

css

Affected by the transition variable.

:root {
    --transition-time: 0.2s;
}

example

import { html } from 'htm/preact'
import { Switch } from '@nichoth/components/htm/switch'
import '@nichoth/components/switch.css'

function Example () {
    return html`<div id="switch">
        <form
            onSubmit=${ev => {
                ev.preventDefault()
                const el = ev.target.elements['test-switch']
                console.log('el.checked', el.checked)
            }}>
                <${Switch} name="test-switch" />
                <button type="submit">submit</button>
        </form>
    </div>`

checkbox

See ./src/htm/checkbox.ts

Pass in the checkbox label as child text.

Optionally Pass in a signal as the checked value, to use as a controlled input. Or do not pass in a checkedState for a non-controlled input.

const Checkbox:FunctionComponent<{
    checkedState?:Signal<boolean>;
} & JSX.HTMLAttributes<HTMLCheckbox>> = function (props)

example

import { html } from 'htm/preact'
import { Checkbox } from '@nichoth/components/htm/checkbox'
import '@nichoth/components/checkbox.css'

function Example () {
    // note we pass in the checkbox label text as a child
    return html`<form onSubmit=${ev => {
        ev.preventDefault()
        const testbox = ev.target.elements.testbox
        console.log('testbox value', testbox.checked)
    }}>
        <fieldset>
            <legend>checkbox demo</legend>
            ${/* Can pass in `checkedState` here */}
            <${Checkbox} name="testbox">Testing checkbox<//>
        </fieldset>

        <button type="submit">submit</button>
    </form>`
}

css

CSS selectors:

label.checkbox

and

input.checkbox

Accordion

Available in preact/htm only.

Accordion CSS

It is affected by the --accordion-transition-time, and --x-transition-time CSS variables

:root {
    --accordion-transition-time: 0.4s;
    --x-transition-time: 0.2s;
}

example

import { html } from 'htm/preact'
import { Accordion } from '@nichoth/components/htm/accordion'
import '@nichoth/components/accordion.css'

function Example () {
    return html`<${Accordion}>
        <summary>Trying accordion example</summary>
        <p>This is the nested paragraph element in the accordion demo</p>
    <//>`
}

Results in UI like this:

closed: Screenshot of accordion closed

open: Screenshot of accordion open

Button

Show resolving state with a spinner in the button. Either pass in a signal to use as resolving state, or just return a promise from the click event handler.

Button example

import { FunctionComponent } from 'preact'
import { html } from 'htm/preact'
import { useSignal } from '@preact/signals'
import { Button } from '@nichoth/components/preact/button'
const isResolving = useSignal(false)

const MyComponent:FunctionComponent = function () {
    return html`<Button
        isSpinning={resolving} ${/*<- note we are passing a signal, not boolean*/}
        onClick={clicker}
    >
        example
    </Button>`
}

Button Outline

If you return a promise from the onClick event handler, then the button will spin until the promise resolves.

Takes an optional signal for isSpinning. To control the spinning state of the button, eg for a form submit, pass in a signal and update its value.

Looks for a few css variables:

:root {
    --button-outine-color: black;
    --button-primary-outline: #0077ff;
    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);
    --button-outline-disabled-ol: #0077ff5c;
}
interface ButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
    isSpinning?: Signal<boolean>,
    className?: string,
    onClick?: (ev:MouseEvent) => Promise<any>
}

example

import { ButtonOutine } from '@nichoth/components/preact/button-outline'

<ButtonOutline
    type="submit"
    onClick={ev => {
        ev.preventDefault()
        console.log('click')
        // if you return a promise, then the button
        // will spin until it resolves
        return sleep(2000)
    }}
>
    example
</ButtonOutline>

ButtonLink

A link that looks like a button. Use with htm.

import { html } from 'htm/preact'
import { FunctionComponent } from 'preact'

/**
 * A link that looks like a button
 *
 * @param {HTMLAttributes<HTMLAnchorElement>} props
 * @returns {FunctionComponent}
 */
const ButtonLink:FunctionComponent<
    HTMLAttributes<HTMLAnchorElement>
> = function (props) {
    const className = [props.class, 'btn-link'].join(' ').trim()
    return html`<a href=${props.href} class=${className}>${props.children}</a>`
}

ButtonLink example

import { html } from 'htm/preact'
import { ButtonLink } from '@nichoth/components/htm/button-link'
import '@nichoth/components/button.css'

// ...

return html`<div>
    <ButtonLink href="#" class="my-button">button text</ButtonLink>
</div>`

CopyBtn

import { CopyBtn } from '@nichoth/components/preact/copy-btn'

<CopyBtn payload="copying things">copy</CopyBtn>

CopyIconBtn

import { CopyIconBtn } from '@nicohoth/components/preact/copy-icon-btn'
// ...
<span>
    Copy this text
    <CopyIconBtn payload="copy this text" />
</span>

Editable Field

interface Props extends JSX.HTMLAttributes<HTMLInputElement> {
    onSave:(value:string) => Promise<any>
    name:string
}

const EditableField:FunctionComponent<Props> = function EditableField (props)
import { EditableField } from '@nichoth/components/preact/editable-field'
// ...
<EditableField
    name="editable-field"
    value="edit this"
    onSave={saver}
/>

Text Input

This looks for 2 css variables:

:root {
    --text-input-error-border: red;
    --text-input-error-text: red;
}

Pass in an attribute title; this determines the invalid hint text that is shown below the input.

interface InputProps extends JSX.HTMLAttributes<HTMLInputElement> {
    displayName: string;
    name: string;
    className?: string;
}

const TextInput:FunctionComponent<InputProps> = function (props:InputProps)

example

import { TextInput } from '@nichoth/components/preact/text-input'
// ...
function MyElement () {
    return html`<form className="example-form">
        <${TextInput}
            displayName="htm text input"
            title="At least 3 characters, but less than 7"
            required=${true}
            minLength=${3}
            maxLength=${7}
            name=${'htm-text-input-example'}
        ><//>
    </form>`
}

PencilBtn

import { PencilBtn } from '@nichoth/components/preact/pencil-btn'
<PencilBtn onClick={(ev) => {
    // we are passed a `click` event
    ev.preventDefault()
    console.log('click')
}} />

RadioGroup

interface Props {
    id?:string
    name:string
    legend:string
    options:string[]
    required:boolean
}

const RadioGroup:FunctionComponent<Props> = function (props)
import { RadioGroup } from '@nichoth/components/preact/radio-group'

<RadioGroup
    name="test-radio"
    legend="testing radio group"
    options={['aaa', 'bbb', 'ccc']}
    required={true}
/>

NumberInput

interface Props {
    name:string;
    min:number;
    max:number;
    value:Signal<number>;
    onIncrease?:(ev:MouseEvent)=>any;
    onDecrease?:(ev:MouseEvent)=>any;
    onChange?:(ev:JSX.TargetedEvent)=>any;
}

const NumberInput:FunctionComponent<Props> = function NumberInput (props)
import { NumberInput } from '@nichoth/components/preact/number-input'

const count = useSignal(3)
// ...
<NumberInput
    min={0}
    max={7}
    name="test-input"
    value={count}
    onIncrease={() => {
        console.log('increase')
    }}
    onDecrease={() => {
        console.log('decrease')
    }}
/>

ReactiveForm

A form element that uses HTML attributes to check validity, and enables or disables the submit button as appropriate.

type Props = {
    onInput?:(event:InputEvent)=>any;
    onSubmit:(event:SubmitEvent)=>any;
    controls?:boolean;
    buttonText?:string;
} & Readonly<Attributes & {
    children?: ComponentChildren
}>

const ReactiveForm:FunctionComponent<Props> = function (props:Props)
import { ReactiveForm } from '@nichoth/components/preact/reactive-form'

<ReactiveForm
    onSubmit={async (ev:SubmitEvent) => {
        ev.preventDefault()
        const text = ((ev.target as HTMLFormElement)
            .elements
            .namedItem('text') as HTMLInputElement)

        await sleep(2000)

        console.log('resolved...', text.value)
    }}
>
    <TextInput
        required={true}
        displayName="text input"
        name="text"
    />
</ReactiveForm>

example

import { render } from 'preact'
import { useSignal } from '@preact/signals'
import HamburgerWrapper from '@nichoth/components/preact/hamburger'
import MobileNav from '@nichoth/components/preact/mobile-nav-menu'
import { CopyBtn, CopyIconBtn } from '@nichoth/components/preact/copy-btn'
import '@nichoth/components/variables.css'
import '@nichoth/components/copy-btn.css'
import '@nichoth/components/hamburger.css'
import '@nichoth/components/mobile-nav-menu.css'
import '@nichoth/components/z-index.css'

const App = function App () {
    const isOpen = useSignal(false)

    function mobileNavHandler (ev) {
        ev.preventDefault()
        isOpen.value = !isOpen.value
    }

    return <div class="app">
        <HamburgerWrapper isOpen={isOpen} onClick={mobileNavHandler} />
        <MobileNav isOpen={isOpen}>
            <a href="/baloney">baloney</a>
            <a href="/test">testing</a>
        <//>

        <CopyBtn payload="hurray">copy something</CopyBtn>

        <p>Copy this <CopyIconBtn payload="Copy this" /></p>
    </div>
}

const el = document.getElementById('root')
if (el) render(<App />, el)

css

We look for a css variable --hamburger-color, or by default use a black color.

/* in your css file */
:root {
    --hamburger-color: #FAFAFA;
}

see also