{"id":20181751,"url":"https://github.com/nichoth/components","last_synced_at":"2025-04-10T05:10:19.427Z","repository":{"id":77399981,"uuid":"582460542","full_name":"nichoth/components","owner":"nichoth","description":"A collection of browser components","archived":false,"fork":false,"pushed_at":"2025-03-13T19:04:47.000Z","size":511,"stargazers_count":1,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-24T06:22:12.725Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://nichoth.github.io/components/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nichoth.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-26T22:55:58.000Z","updated_at":"2025-03-13T19:04:49.000Z","dependencies_parsed_at":"2024-01-31T09:52:39.389Z","dependency_job_id":"4a5a0c12-a36b-4726-a252-4169c2920420","html_url":"https://github.com/nichoth/components","commit_stats":null,"previous_names":[],"tags_count":77,"template":false,"template_full_name":"nichoth/template-js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fcomponents","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fcomponents/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fcomponents/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nichoth%2Fcomponents/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nichoth","download_url":"https://codeload.github.com/nichoth/components/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248161274,"owners_count":21057555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-14T02:36:39.368Z","updated_at":"2025-04-10T05:10:19.391Z","avatar_url":"https://github.com/nichoth.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# components\n![module](https://img.shields.io/badge/module-ESM-blue)\n![types](https://img.shields.io/npm/types/@nichoth/components)\n[![Socket Badge](https://socket.dev/api/badge/npm/package/@nichoth/components)](https://socket.dev/npm/package/@nichoth/components)\n![license](https://img.shields.io/badge/license-MIT-brightgreen)\n\nA collection of UI components made with [preact](https://www.npmjs.com/package/preact) and [tonic](https://tonicframework.dev/).\n\n[See a live demo](https://nichoth.github.io/components/)\n\n## install\n```\nnpm i -S @nichoth/components\n```\n\n## use\nI recommend using this with [vite](https://www.npmjs.com/package/vite) + ESM, because it is easy. These are [preact](https://www.npmjs.com/package/preact) and [tonic](https://tonicframework.dev/) components; you will need to install `preact` or `tonic`.\n\nThe `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.\n\n### preact + htm\nUse preact with tagged template literals.\n\n\u003e [!IMPORTANT]  \n\u003e This is the preferred way to consume these, with `htm` + preact.\n\nImport components from `@nichoth/components/htm/*`.\n\n```ts\nimport { html } from 'htm/preact'\nimport { render, FunctionComponent } from 'preact'\nimport { TextInput } from '@nichoth/components/htm/text-input'\nimport '@nichoth/components/text-input.css'\n\nconst Example:FunctionComponent\u003c{}\u003e = function () {\n    return html`\n        \u003cdiv\u003e\n            \u003ch3\u003eText Input\u003c/h3\u003e\n            \u003c${TextInput}\n                displayName=\"htm text input\"\n                required=${true}\n                minLength=${3}\n                maxLength=${7}\n                name=${'htm-text-input'}\n            \u003e\u003c//\u003e\n        \u003c/div\u003e\n    `\n}\n\nrender(html`\u003c${Example}\u003e\u003c//\u003e`, document.getElementById('root')!)\n```\n\n### preact + JSX\nUse preact with JSX\n\n```tsx\nimport { render, FunctionComponent } from 'preact'\nimport { TextInput } from '@nichoth/components/preact/text-input'\nimport '@nichoth/components/text-input.css'\n\nconst Example:FunctionComponent = function () {\n    return \u003cdiv\u003e\n        \u003ch3\u003eText Input\u003c/h3\u003e\n        \u003cform className=\"example-form\"\u003e\n            \u003cTextInput name=\"text\" displayName=\"Input test\" /\u003e\n        \u003c/form\u003e\n    \u003c/div\u003e\n}\n\nrender(\u003cExample /\u003e, document.getElementById('root')!)\n```\n\n### tonic\nUse web components with [Tonic](https://tonicframework.dev/)\n\n```js\nimport Tonic from '@nichoth/tonic'\nimport { SpinningButton } from '@nichoth/components/tonic/spinning-button'\nimport '@nichoth/components/text-input.css'\n\nexport class TonicExample extends Tonic {\n    state = {\n        isSpinning: false,\n    }\n\n    render () {\n        return this.html`\u003cdiv\u003e\n            \u003cspinning-button\n                isSpinning=${this.state.isSpinning}\n                data-event=\"click-the-button\"\n            \u003e\n                click here\n            \u003c/spinning-button\u003e\n        \u003c/div\u003e`\n    }\n}\n\nTonic.add(SpinningButton)\nTonic.add(TonicExample)\n```\n\n## globals\nWe use these CSS variables\n\n```css\n:root {\n    --transition-time: 0.2s;\n    --button-outine-color: black;\n    --button-primary-outline: #0077ff;\n    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);\n    --button-outline-disabled-ol: #0077ff5c;\n    --text-input-error-border: red;\n    --text-input-error-text: red;\n    --hamburger-color: black;\n    --fade-in-time: 0.2s;  /* for the mobile nav fade-in animation */\n}\n```\n\nDefine them in your application to customize.\n\n## API\n\n### css\nImport the css variables in addition to the per-component css.\n\nWith Vite as bundler:\n```js\nimport '@nichoth/components/variables.css'\n```\n\n### Switch\nSee [./src/htm/switch](./src/htm/switch.ts)\n\n#### css\nAffected by the transition variable.\n```css\n:root {\n    --transition-time: 0.2s;\n}\n```\n\n#### example\n```js\nimport { html } from 'htm/preact'\nimport { Switch } from '@nichoth/components/htm/switch'\nimport '@nichoth/components/switch.css'\n\nfunction Example () {\n    return html`\u003cdiv id=\"switch\"\u003e\n        \u003cform\n            onSubmit=${ev =\u003e {\n                ev.preventDefault()\n                const el = ev.target.elements['test-switch']\n                console.log('el.checked', el.checked)\n            }}\u003e\n                \u003c${Switch} name=\"test-switch\" /\u003e\n                \u003cbutton type=\"submit\"\u003esubmit\u003c/button\u003e\n        \u003c/form\u003e\n    \u003c/div\u003e`\n```\n\n### checkbox\nSee [./src/htm/checkbox.ts](./src/htm/checkbox.ts)\n\nPass in the checkbox label as child text.\n\nOptionally 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.\n\n```js\nconst Checkbox:FunctionComponent\u003c{\n    checkedState?:Signal\u003cboolean\u003e;\n} \u0026 JSX.HTMLAttributes\u003cHTMLCheckbox\u003e\u003e = function (props)\n```\n\n#### example\n```js\nimport { html } from 'htm/preact'\nimport { Checkbox } from '@nichoth/components/htm/checkbox'\nimport '@nichoth/components/checkbox.css'\n\nfunction Example () {\n    // note we pass in the checkbox label text as a child\n    return html`\u003cform onSubmit=${ev =\u003e {\n        ev.preventDefault()\n        const testbox = ev.target.elements.testbox\n        console.log('testbox value', testbox.checked)\n    }}\u003e\n        \u003cfieldset\u003e\n            \u003clegend\u003echeckbox demo\u003c/legend\u003e\n            ${/* Can pass in `checkedState` here */}\n            \u003c${Checkbox} name=\"testbox\"\u003eTesting checkbox\u003c//\u003e\n        \u003c/fieldset\u003e\n\n        \u003cbutton type=\"submit\"\u003esubmit\u003c/button\u003e\n    \u003c/form\u003e`\n}\n```\n\n#### css\nCSS selectors:\n\n```css\nlabel.checkbox\n```\nand\n```css\ninput.checkbox\n```\n\n\n### Accordion\nAvailable in `preact/htm` only.\n\n#### Accordion CSS\nIt is affected by the `--accordion-transition-time`, and `--x-transition-time` CSS variables\n\n```css\n:root {\n    --accordion-transition-time: 0.4s;\n    --x-transition-time: 0.2s;\n}\n```\n\n#### example\n```js\nimport { html } from 'htm/preact'\nimport { Accordion } from '@nichoth/components/htm/accordion'\nimport '@nichoth/components/accordion.css'\n\nfunction Example () {\n    return html`\u003c${Accordion}\u003e\n        \u003csummary\u003eTrying accordion example\u003c/summary\u003e\n        \u003cp\u003eThis is the nested paragraph element in the accordion demo\u003c/p\u003e\n    \u003c//\u003e`\n}\n```\n\nResults in UI like this:\n\n__closed:__\n![Screenshot of accordion closed](image-2.png)\n\n__open:__\n![Screenshot of accordion open](image-1.png)\n\n### Button\nShow resolving state with a spinner in the button. Either pass in a signal to\nuse as resolving state, or just return a promise from the `click` event handler.\n\n### Button example\n```ts\nimport { FunctionComponent } from 'preact'\nimport { html } from 'htm/preact'\nimport { useSignal } from '@preact/signals'\nimport { Button } from '@nichoth/components/preact/button'\nconst isResolving = useSignal(false)\n\nconst MyComponent:FunctionComponent = function () {\n    return html`\u003cButton\n        isSpinning={resolving} ${/*\u003c- note we are passing a signal, not boolean*/}\n        onClick={clicker}\n    \u003e\n        example\n    \u003c/Button\u003e`\n}\n```\n\n### Button Outline\nIf you return a promise from the `onClick` event handler, then the button will spin until the promise resolves.\n\nTakes 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.\n\nLooks for a few css variables:\n```css\n:root {\n    --button-outine-color: black;\n    --button-primary-outline: #0077ff;\n    --button-outine-primary-bg-hover: rgb(0 255 255 / 28%);\n    --button-outline-disabled-ol: #0077ff5c;\n}\n```\n\n```ts\ninterface ButtonProps extends JSX.HTMLAttributes\u003cHTMLButtonElement\u003e {\n    isSpinning?: Signal\u003cboolean\u003e,\n    className?: string,\n    onClick?: (ev:MouseEvent) =\u003e Promise\u003cany\u003e\n}\n```\n\n#### example\n\n```js\nimport { ButtonOutine } from '@nichoth/components/preact/button-outline'\n\n\u003cButtonOutline\n    type=\"submit\"\n    onClick={ev =\u003e {\n        ev.preventDefault()\n        console.log('click')\n        // if you return a promise, then the button\n        // will spin until it resolves\n        return sleep(2000)\n    }}\n\u003e\n    example\n\u003c/ButtonOutline\u003e\n```\n\n### ButtonLink\nA link that looks like a button. Use with `htm`.\n\n```js\nimport { html } from 'htm/preact'\nimport { FunctionComponent } from 'preact'\n\n/**\n * A link that looks like a button\n *\n * @param {HTMLAttributes\u003cHTMLAnchorElement\u003e} props\n * @returns {FunctionComponent}\n */\nconst ButtonLink:FunctionComponent\u003c\n    HTMLAttributes\u003cHTMLAnchorElement\u003e\n\u003e = function (props) {\n    const className = [props.class, 'btn-link'].join(' ').trim()\n    return html`\u003ca href=${props.href} class=${className}\u003e${props.children}\u003c/a\u003e`\n}\n```\n\n#### ButtonLink example\n```js\nimport { html } from 'htm/preact'\nimport { ButtonLink } from '@nichoth/components/htm/button-link'\nimport '@nichoth/components/button.css'\n\n// ...\n\nreturn html`\u003cdiv\u003e\n    \u003cButtonLink href=\"#\" class=\"my-button\"\u003ebutton text\u003c/ButtonLink\u003e\n\u003c/div\u003e`\n```\n\n### CopyBtn\n```jsx\nimport { CopyBtn } from '@nichoth/components/preact/copy-btn'\n\n\u003cCopyBtn payload=\"copying things\"\u003ecopy\u003c/CopyBtn\u003e\n```\n\n### CopyIconBtn\n```js\nimport { CopyIconBtn } from '@nicohoth/components/preact/copy-icon-btn'\n// ...\n\u003cspan\u003e\n    Copy this text\n    \u003cCopyIconBtn payload=\"copy this text\" /\u003e\n\u003c/span\u003e\n```\n\n### Editable Field\n```ts\ninterface Props extends JSX.HTMLAttributes\u003cHTMLInputElement\u003e {\n    onSave:(value:string) =\u003e Promise\u003cany\u003e\n    name:string\n}\n\nconst EditableField:FunctionComponent\u003cProps\u003e = function EditableField (props)\n```\n\n```js\nimport { EditableField } from '@nichoth/components/preact/editable-field'\n// ...\n\u003cEditableField\n    name=\"editable-field\"\n    value=\"edit this\"\n    onSave={saver}\n/\u003e\n```\n\n### Text Input\nThis looks for 2 css variables:\n```css\n:root {\n    --text-input-error-border: red;\n    --text-input-error-text: red;\n}\n```\n\nPass in an attribute `title`; this determines the invalid hint text that is shown below the input.\n\n```ts\ninterface InputProps extends JSX.HTMLAttributes\u003cHTMLInputElement\u003e {\n    displayName: string;\n    name: string;\n    className?: string;\n}\n\nconst TextInput:FunctionComponent\u003cInputProps\u003e = function (props:InputProps)\n```\n\n#### example\n```js\nimport { TextInput } from '@nichoth/components/preact/text-input'\n// ...\nfunction MyElement () {\n    return html`\u003cform className=\"example-form\"\u003e\n        \u003c${TextInput}\n            displayName=\"htm text input\"\n            title=\"At least 3 characters, but less than 7\"\n            required=${true}\n            minLength=${3}\n            maxLength=${7}\n            name=${'htm-text-input-example'}\n        \u003e\u003c//\u003e\n    \u003c/form\u003e`\n}\n```\n\n### PencilBtn\n```js\nimport { PencilBtn } from '@nichoth/components/preact/pencil-btn'\n\u003cPencilBtn onClick={(ev) =\u003e {\n    // we are passed a `click` event\n    ev.preventDefault()\n    console.log('click')\n}} /\u003e\n```\n\n### RadioGroup\n```ts\ninterface Props {\n    id?:string\n    name:string\n    legend:string\n    options:string[]\n    required:boolean\n}\n\nconst RadioGroup:FunctionComponent\u003cProps\u003e = function (props)\n```\n\n```js\nimport { RadioGroup } from '@nichoth/components/preact/radio-group'\n\n\u003cRadioGroup\n    name=\"test-radio\"\n    legend=\"testing radio group\"\n    options={['aaa', 'bbb', 'ccc']}\n    required={true}\n/\u003e\n```\n\n### NumberInput\n```ts\ninterface Props {\n    name:string;\n    min:number;\n    max:number;\n    value:Signal\u003cnumber\u003e;\n    onIncrease?:(ev:MouseEvent)=\u003eany;\n    onDecrease?:(ev:MouseEvent)=\u003eany;\n    onChange?:(ev:JSX.TargetedEvent)=\u003eany;\n}\n\nconst NumberInput:FunctionComponent\u003cProps\u003e = function NumberInput (props)\n```\n\n```js\nimport { NumberInput } from '@nichoth/components/preact/number-input'\n\nconst count = useSignal(3)\n// ...\n\u003cNumberInput\n    min={0}\n    max={7}\n    name=\"test-input\"\n    value={count}\n    onIncrease={() =\u003e {\n        console.log('increase')\n    }}\n    onDecrease={() =\u003e {\n        console.log('decrease')\n    }}\n/\u003e\n```\n\n### ReactiveForm\nA `form` element that uses HTML attributes to check validity, and enables or\ndisables the submit button as appropriate.\n\n```ts\ntype Props = {\n    onInput?:(event:InputEvent)=\u003eany;\n    onSubmit:(event:SubmitEvent)=\u003eany;\n    controls?:boolean;\n    buttonText?:string;\n} \u0026 Readonly\u003cAttributes \u0026 {\n    children?: ComponentChildren\n}\u003e\n\nconst ReactiveForm:FunctionComponent\u003cProps\u003e = function (props:Props)\n```\n\n```js\nimport { ReactiveForm } from '@nichoth/components/preact/reactive-form'\n\n\u003cReactiveForm\n    onSubmit={async (ev:SubmitEvent) =\u003e {\n        ev.preventDefault()\n        const text = ((ev.target as HTMLFormElement)\n            .elements\n            .namedItem('text') as HTMLInputElement)\n\n        await sleep(2000)\n\n        console.log('resolved...', text.value)\n    }}\n\u003e\n    \u003cTextInput\n        required={true}\n        displayName=\"text input\"\n        name=\"text\"\n    /\u003e\n\u003c/ReactiveForm\u003e\n```\n\n## example\n\n```js\nimport { render } from 'preact'\nimport { useSignal } from '@preact/signals'\nimport HamburgerWrapper from '@nichoth/components/preact/hamburger'\nimport MobileNav from '@nichoth/components/preact/mobile-nav-menu'\nimport { CopyBtn, CopyIconBtn } from '@nichoth/components/preact/copy-btn'\nimport '@nichoth/components/variables.css'\nimport '@nichoth/components/copy-btn.css'\nimport '@nichoth/components/hamburger.css'\nimport '@nichoth/components/mobile-nav-menu.css'\nimport '@nichoth/components/z-index.css'\n\nconst App = function App () {\n    const isOpen = useSignal(false)\n\n    function mobileNavHandler (ev) {\n        ev.preventDefault()\n        isOpen.value = !isOpen.value\n    }\n\n    return \u003cdiv class=\"app\"\u003e\n        \u003cHamburgerWrapper isOpen={isOpen} onClick={mobileNavHandler} /\u003e\n        \u003cMobileNav isOpen={isOpen}\u003e\n            \u003ca href=\"/baloney\"\u003ebaloney\u003c/a\u003e\n            \u003ca href=\"/test\"\u003etesting\u003c/a\u003e\n        \u003c//\u003e\n\n        \u003cCopyBtn payload=\"hurray\"\u003ecopy something\u003c/CopyBtn\u003e\n\n        \u003cp\u003eCopy this \u003cCopyIconBtn payload=\"Copy this\" /\u003e\u003c/p\u003e\n    \u003c/div\u003e\n}\n\nconst el = document.getElementById('root')\nif (el) render(\u003cApp /\u003e, el)\n```\n\n### css\nWe look for a css variable `--hamburger-color`, or by default use a black color. \n\n```css\n/* in your css file */\n:root {\n    --hamburger-color: #FAFAFA;\n}\n```\n\n### see also\n\n* [You don't need JavaScript for that](https://www.htmhell.dev/adventcalendar/2023/2/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnichoth%2Fcomponents","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnichoth%2Fcomponents","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnichoth%2Fcomponents/lists"}