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

wini-web-components

v8.5.15

Published

A simple ui library for React

Downloads

1,831

Readme

wini-web-components

A modern, lightweight React TypeScript UI component library with 35+ ready-to-use components, responsive layout utilities, design token theming, and optional Wini backend integration for dynamic form/table/page rendering.

npm version License: MIT


Table of Contents


Installation

npm install wini-web-components

These provide:

  • root.css — CSS design token variables (colors, borders, shadows) through https://cdn.jsdelivr.net/gh/WiniGit/web-component@latest/src/skin/root.css
  • typography.css — text utility classes (heading-1heading-8, body-1body-3, etc.) through https://cdn.jsdelivr.net/gh/WiniGit/web-component@latest/src/skin/typography.css
  • layout.cssrow, col, and responsive grid classes through https://cdn.jsdelivr.net/gh/WiniGit/web-component@latest/src/skin/layout.css

Setup: Wrap Your App with WiniProvider

WiniProvider is the root provider. It sets up routing, toasts, dialogs, auth token handling, and optionally loads design tokens and i18n from a Wini backend.

// main.tsx
import WiniProvider from 'wini-web-components'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <WiniProvider
    pid="your-project-id"
    url="https://your-wini-api.com/"
    fileUrl="https://your-file-server.com/"
    imgUrlId="https://your-cdn.com/"
    theme="light"              // "light" | "dark"
    loadResources={true}       // set false to skip backend token/i18n loading
  >
    <Route path="/" element={<HomePage />} />
    <Route path="/about" element={<AboutPage />} />
  </WiniProvider>
)

Note: WiniProvider wraps BrowserRouter internally — do not add another BrowserRouter in your app.

Access Global Context

import { useWiniContext } from 'wini-web-components'

function MyComponent() {
  const { theme, setTheme, userData, setUserData, globalData, setGlobalData, i18n } = useWiniContext()
  return <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle theme</button>
}

Layout System

Use row and col classes for flex layouts. These are available globally after importing the skin CSS.

// Horizontal layout (flex-direction: row, align-items: center)
<div className="row" style={{ gap: 12 }}>
  <span>Left</span>
  <span className="remain">This stretches to fill remaining space</span>
  <span>Right</span>
</div>

// Vertical layout (flex-direction: column)
<div className="col" style={{ gap: 8 }}>
  <span>Top</span>
  <span>Bottom</span>
</div>

remain inside a row or col expands to fill available space (flex: 1).


Components

Button

A versatile button supporting sizes, color variants, icons, links, tooltips, and keyboard Enter on submit type.

import { Button, Winicon } from 'wini-web-components'

// Basic
<Button label="Click me" onClick={() => alert('clicked!')} />

// Size variants (default: size32)
<Button label="Large" className="size48 button-primary" onClick={...} />

// As a link
<Button label="Go to docs" linkTo="https://example.com" target="_blank" className="size40 button-neutral" />

// With prefix icon
<Button
  label="Save"
  prefix={<Winicon src="outline/files/save-2" size={16} />}
  className="size40 button-primary"
  onClick={...}
/>

// With tooltip
<Button label="Info" tooltip={{ message: 'More details', position: 'top' }} />

// Submit type — also triggers on Enter key
<Button label="Submit" type="submit" className="size40 button-primary" onClick={...} />

Size classes: size24 size32(default) size40 size48 size56 size64

Color classes: button-primary button-grey button-neutral button-black button-white button-infor button-warning button-error button-success button-infor-main button-warning-main button-error-main button-success-main

SimpleButton is a variant that auto-disables itself during the async onClick to prevent double-clicks.


Winicon

SVG icon component loaded from the WiniGit icon library CDN. Icons are cached in the browser after first load.

import { Winicon } from 'wini-web-components'

<Winicon src="outline/user/user" size={24} color="#287CF0" />
<Winicon src="fill/actions/trash" size={20} onClick={() => handleDelete()} className="icon-button" />

// With tooltip
<Winicon src="outline/essential/info-circle" size={20} tooltip={{ message: 'Help', position: 'bottom' }} />

Size classes: size24 size32 size40 size48 size56 size64 Style classes: icon-button (adds hover/click interactivity styles), light


TextField

Styled text input with optional prefix/suffix slots, helper text, and react-hook-form support.

import { TextField } from 'wini-web-components'

// Basic
<TextField placeholder="Enter name" onChange={(e) => setName(e.target.value)} />

// With prefix icon and helper text
<TextField
  prefix={<Winicon src="outline/user/user" size={16} />}
  placeholder="Email"
  type="email"
  helperText="Invalid email"        // shown below the field in red
  className="size40 body-3"
/>

// With react-hook-form
const { register } = useForm()
<TextField placeholder="Username" register={register('username', { required: true })} />

Size classes: size24 size32 size40(default) size48


TextArea

Multi-line text input.

import { TextArea } from 'wini-web-components'

<TextArea placeholder="Write something..." onChange={(e) => setText(e.target.value)} />

Select1

Single-value dropdown. Supports static options, async lazy-loading, hierarchical (parent/child) options, and custom render.

import { Select1 } from 'wini-web-components'

const options = [
  { id: '1', name: 'Option A' },
  { id: '2', name: 'Option B' },
  { id: '3', name: 'Option C', disabled: true },
]

// Static options
<Select1
  value={selected}
  options={options}
  placeholder="Choose one"
  onChange={(item) => setSelected(item?.id)}
/>

// Async options (loaded on open / search)
<Select1
  options={[]}
  getOptions={async ({ length, search }) => {
    const res = await fetchItems({ page: Math.floor(length / 10) + 1, search })
    return { data: res.items, totalCount: res.total }
  }}
  onChange={(item) => setSelected(item?.id)}
/>

Checkbox

import { Checkbox } from 'wini-web-components'

<Checkbox value={isChecked} onChange={(val) => setIsChecked(val)} />
<Checkbox value={null} />   {/* null = indeterminate state */}

Switch

Toggle switch with customizable colors and size.

import { Switch } from 'wini-web-components'

<Switch value={isOn} onChange={(val) => setIsOn(val)} />
<Switch value={isOn} size="2.4rem" onBackground="#287CF0" offBackground="#D7D7DB" onChange={...} />

RadioButton

import { RadioButton } from 'wini-web-components'

<RadioButton value={selected === 'a'} onChange={() => setSelected('a')} label="Option A" />
<RadioButton value={selected === 'b'} onChange={() => setSelected('b')} label="Option B" />

DateTimePicker

Date and/or time picker.

import { DateTimePicker } from 'wini-web-components'

<DateTimePicker value={date} onChange={(val) => setDate(val)} placeholder="Pick a date" />

NumberPicker

Numeric input with increment/decrement controls.

import { NumberPicker } from 'wini-web-components'

<NumberPicker value={count} onChange={(val) => setCount(val)} min={0} max={100} />

Slider

Range slider input.

import { Slider } from 'wini-web-components'

<Slider value={volume} min={0} max={100} onChange={(val) => setVolume(val)} />

Rating

Star rating input.

import { Rating } from 'wini-web-components'

<Rating value={3} onChange={(val) => setRating(val)} />

InputOtp

OTP / PIN code input with configurable digit count.

import { InputOtp } from 'wini-web-components'

<InputOtp length={6} onComplete={(code) => verifyOtp(code)} />

ColorPicker

Hex/RGBA color picker input.

import { ColorPicker } from 'wini-web-components'

<ColorPicker value={color} onChange={(hex) => setColor(hex)} />

SelectMultiple

Multi-value dropdown with tag display.

import { SelectMultiple } from 'wini-web-components'

<SelectMultiple
  value={['1', '2']}
  options={[{ id: '1', name: 'React' }, { id: '2', name: 'TypeScript' }]}
  onChange={(items) => setSelected(items.map(i => i.id))}
/>

Tag

Small label/badge component with color support.

import { Tag } from 'wini-web-components'

<Tag>Active</Tag>
<Tag style={{ backgroundColor: '#B7D3FA', color: '#287CF0' }}>Info</Tag>

Pagination

Page navigation component.

import { Pagination } from 'wini-web-components'

<Pagination
  currentPage={page}
  total={totalItems}
  pageSize={20}
  onChange={(newPage) => setPage(newPage)}
/>

InfiniteScroll

Trigger a callback when the user scrolls to the bottom of a container.

import { InfiniteScroll } from 'wini-web-components'

<InfiniteScroll onLoadMore={() => fetchNextPage()} isLoading={loading} hasMore={hasMore}>
  {items.map(item => <div key={item.id}>{item.name}</div>)}
</InfiniteScroll>

Dialog

A global imperative dialog (confirm/alert modal). Rendered by WiniProvider — no extra setup needed.

import { showDialog, DialogAlignment, ComponentStatus } from 'wini-web-components'

showDialog({
  title: 'Delete item?',
  content: 'This action cannot be undone.',
  status: ComponentStatus.ERROR,
  alignment: DialogAlignment.center,
  onSubmit: () => handleDelete(),
  submitTitle: 'Delete',
  cancelTitle: 'Cancel',
})

Status options: ComponentStatus.INFOR ComponentStatus.WARNING ComponentStatus.ERROR ComponentStatus.SUCCESS


Popup

A flexible fullscreen/overlay modal managed via ref.

import { Popup, showPopup, closePopup } from 'wini-web-components'
import { useRef } from 'react'

const popupRef = useRef<Popup>()

// Trigger open
showPopup({
  ref: popupRef,
  heading: <h3>My Popup</h3>,
  body: <p>Custom content here</p>,
  footer: <Button label="Close" onClick={() => closePopup(popupRef)} />,
  clickOverlayClosePopup: true,
})

// Place once in your JSX
<Popup ref={popupRef} />

ToastMessage

Imperative toast notifications. Rendered by WiniProvider — no extra setup needed.

import { ToastMessage } from 'wini-web-components'

ToastMessage.success('Saved successfully!')
ToastMessage.errors('Something went wrong.')
ToastMessage.warn('Check your input.')
ToastMessage.infor('New update available.')

ProgressBar

import { ProgressBar } from 'wini-web-components'

<ProgressBar value={72} max={100} />

ProgressCircle

import { ProgressCircle } from 'wini-web-components'

<ProgressCircle value={72} max={100} />

Calendar

Standalone date calendar view.

import { Calendar } from 'wini-web-components'

<Calendar value={date} onChange={(val) => setDate(val)} />

Carousel

Image/content slideshow powered by react-awesome-slider.

import { Carousel } from 'wini-web-components'

<Carousel>
  <div data-src="/image1.jpg" />
  <div data-src="/image2.jpg" />
</Carousel>

VideoPlayer / AudioPlayer / IframePlayer

Media player components for video, audio, and embedded iframes.

import { VideoPlayer, AudioPlayer, IframePlayer } from 'wini-web-components'

<VideoPlayer src="https://example.com/video.mp4" />
<AudioPlayer src="https://example.com/audio.mp3" />
<IframePlayer src="https://www.youtube.com/embed/dQw4w9WgXcQ" />

ImportFile / UploadFiles

File import input and multi-file uploader.

import { ImportFile, UploadFiles } from 'wini-web-components'

// Single file pick (no upload)
<ImportFile onChange={(file) => setFile(file)} accept=".pdf,.docx" />

// Upload to server
<UploadFiles
  onComplete={(urls) => setAttachments(urls)}
  maxFiles={5}
/>

CustomCkEditor5

Rich text editor wrapper for CKEditor 5. Requires ckeditor5 and @ckeditor/ckeditor5-react as peer dependencies.

import { CustomCkEditor5 } from 'wini-web-components'

<CustomCkEditor5 value={html} onChange={(html) => setHtml(html)} />

WiniEditor

Lightweight built-in rich text editor (no peer dependency required).

import { WiniEditor } from 'wini-web-components'

<WiniEditor value={content} onChange={(val) => setContent(val)} />

IconPicker

Searchable icon selector from the Wini icon library.

import { IconPicker } from 'wini-web-components'

<IconPicker value={icon} onChange={(iconName) => setIcon(iconName)} />


Utility Functions (Util class)

The Util class provides a comprehensive collection of static helper methods for date/time, number formatting, color conversion, string manipulation, browser storage, cookies, and more.

import { Util, formatNumberConvert, randomGID } from 'wini-web-components'

📅 Date & Time

Util.dateTime_stringToDecimal(stringDate: string): number

Converts a date string to a Unix timestamp in seconds.

Util.dateTime_stringToDecimal('2024-01-15T10:30:00')
// => 1705311000

| Param | Type | Description | |-------|------|-------------| | stringDate | string | Any valid JS date string | | Returns | number | Unix timestamp in seconds |


Util.dateDefault

Static constant — the timestamp of 01/01/2021. Useful as a fallback or baseline.

Util.dateDefault  // => 1609459200000 (milliseconds)

Util.calculateAge(birthdate: string): number

Calculates a person's current age from a dd/MM/yyyy birthdate string.

Util.calculateAge('15/06/1995')  // => 30 (as of 2026)
Util.calculateAge('invalid')     // => 0

| Param | Type | Description | |-------|------|-------------| | birthdate | string | Date in dd/MM/yyyy format | | Returns | number | Age in full years, or 0 if invalid |


Util.getStringDateNow(): string

Returns today's date as a formatted string DD -MM -YYYY.

Util.getStringDateNow()  // => "06 -04 -2026"

Util.stringToDate(_date, _format?, _delimiter?): Date

Parses a date string into a Date object with flexible format and optional time support.

Util.stringToDate('15/06/2024')
// => Date: June 15, 2024

Util.stringToDate('15/06/2024 14:30:00', 'dd/mm/yyyy HH:mm:ss')
// => Date: June 15, 2024 at 14:30:00

Util.stringToDate('2024-06-15', 'yyyy-mm-dd', '-')
// => Date: June 15, 2024

| Param | Type | Default | Description | |-------|------|---------|-------------| | _date | string | — | The date string to parse | | _format | string | "dd/mm/yyyy" | Format pattern. Date parts: dd, mm, yyyy. Time parts: HH/hh, mm, ss | | _delimiter | string | "/" | Delimiter between date parts | | Returns | Date | | Parsed Date object |


Util.dateToString(x?, y?): string

Converts a Date object (or timestamp number) to a formatted string. Supports date-only, time-only, date+time, and time+date formats.

Util.dateToString(new Date(), 'dd/mm/yyyy')
// => "06/04/2026"

Util.dateToString(new Date(), 'yyyy-mm-dd hh:mm:ss')
// => "2026-04-06 14:30:00"

Util.dateToString(new Date(), 'hh:mm dd/mm/yyyy')
// => "14:30 06/04/2026"

Util.dateToString(new Date(), 'mm/yyyy')
// => "04/2026"

Util.dateToString(new Date(), 'dd/mm')
// => "06/04"

Util.dateToString(1712419200000, 'dd/mm/yyyy')
// => "06/04/2024"

| Param | Type | Default | Description | |-------|------|---------|-------------| | x | Date \| number | new Date() | Date object or timestamp in ms | | y | string | "dd/mm/yyyy" | Format: dd/mm/yyyy, yyyy-mm-dd, hh:mm:ss, dd/mm/yyyy hh:mm, etc. | | Returns | string | | Formatted date/time string, or "" if input is falsy |

Supported format tokens: dd, mm, yyyy, hh, mm (minute), ss
Supported separators: / and - for dates, : for time


Util.timeSince(dateCreate, translate?): string

Returns a human-readable relative time string (e.g. "5 minutes ago"). Defaults to Vietnamese; pass a translate function for other languages.

// Vietnamese (default)
Util.timeSince(Date.now() - 3600000)       // => "1 giờ trước"
Util.timeSince(Date.now() - 120000)        // => "2 phút trước"
Util.timeSince(Date.now() - 86400000)      // => "hôm qua"
Util.timeSince(Date.now() - 500)           // => "vừa xong"

// English (with translate function)
const t = (key: string) => ({
  year: 'year', years: 'years',
  month: 'month', months: 'months',
  day: 'day', days: 'days',
  hour: 'hour', hours: 'hours',
  minute: 'minute', minutes: 'minutes',
  yesterday: 'yesterday',
  ago: 'ago', now: 'just now'
}[key] || key)

Util.timeSince(Date.now() - 7200000, t)   // => "2 hours ago"

| Param | Type | Description | |-------|------|-------------| | dateCreate | number | Timestamp in milliseconds | | translate | (key: string) => string | Optional i18n function. Keys: year, years, month, months, day, days, hour, hours, minute, minutes, yesterday, ago, now | | Returns | string | Relative time string |


💰 Number & Currency Formatting

Util.formatCurrency(amount, currency?, options?): string

Formats a number into a currency string with the correct symbol, position, and locale formatting. Supports 5 currencies out of the box.

// Vietnamese Dong (default)
Util.formatCurrency(1500000)                    // => "1,500,000.00 ₫"
Util.formatCurrency(1500000, 'VND')             // => "1,500,000.00 ₫"

// US Dollar
Util.formatCurrency(1999.99, 'USD')             // => "$1,999.99"

// Japanese Yen
Util.formatCurrency(150000, 'JPY')              // => "¥150,000.00"

// Chinese Yuan
Util.formatCurrency(6800, 'CNY')                // => "¥6,800.00"

// Indian Rupee
Util.formatCurrency(50000, 'INR')               // => "₹50,000.00"

// Custom decimal places
Util.formatCurrency(1500000, 'VND', { decimals: 0 })  // => "1,500,000 ₫"
Util.formatCurrency(19.9, 'USD', { decimals: 3 })     // => "$19.900"

// Invalid input
Util.formatCurrency('invalid')                  // => "0"

| Param | Type | Default | Description | |-------|------|---------|-------------| | amount | number \| string | — | The value to format | | currency | 'VND' \| 'USD' \| 'JPY' \| 'CNY' \| 'INR' | 'VND' | Currency code | | options.decimals | number | 2 | Number of decimal places | | options.symbol | boolean | — | Reserved for future use | | Returns | string | | Formatted currency string with symbol |

Supported currencies:

| Currency | Symbol | Position | Example | |----------|--------|----------|---------| | VND | | After (with space) | 1,500,000.00 ₫ | | USD | $ | Before | $1,999.99 | | JPY | ¥ | Before | ¥150,000.00 | | CNY | ¥ | Before | ¥6,800.00 | | INR | | Before | ₹50,000.00 |


Util.convertCurrency(amount, fromCurrency, toCurrency, rates?): number

Converts an amount from one currency to another using exchange rates. Uses built-in default rates (base: USD) or accepts custom rates.

// USD to VND (default rates)
Util.convertCurrency(100, 'USD', 'VND')
// => 2450000

// VND to USD
Util.convertCurrency(2450000, 'VND', 'USD')
// => 100

// JPY to VND
Util.convertCurrency(10000, 'JPY', 'VND')
// => 2227272.73

// With custom exchange rates
Util.convertCurrency(100, 'USD', 'VND', { USD: 1, VND: 25000, JPY: 115, CNY: 7.2, INR: 83 })
// => 2500000

// Unknown currency returns the original amount
Util.convertCurrency(100, 'USD', 'EUR' as any)
// => 100

| Param | Type | Default | Description | |-------|------|---------|-------------| | amount | number | — | Amount to convert | | fromCurrency | 'VND' \| 'USD' \| 'JPY' \| 'CNY' \| 'INR' | — | Source currency | | toCurrency | 'VND' \| 'USD' \| 'JPY' \| 'CNY' \| 'INR' | — | Target currency | | rates | { [key: string]: number } | See below | Custom exchange rates (base: USD = 1) | | Returns | number | | Converted amount (2 decimal places) |

Default exchange rates (base: USD = 1):

| Currency | Rate | |----------|------| | USD | 1 | | VND | 24,500 | | JPY | 110 | | CNY | 6.5 | | INR | 74.5 |

💡 Pass your own rates object with live exchange rates for production use.


Util.to_vietnamese(number): string

Converts a number to its Vietnamese word representation (e.g. for invoices, contracts, checks).

Util.to_vietnamese(1500000)
// => "Một triệu năm trăm nghìn"

Util.to_vietnamese(42)
// => "Bốn mươi hai"

Util.to_vietnamese(1001)
// => "Một nghìn lẻ một"

| Param | Type | Description | |-------|------|-------------| | number | number \| string | Number to convert | | Returns | string | Vietnamese words (first letter capitalized), or '' if NaN |


Util.numberToAlphabet(n?): string

Converts a 1-based number to alphabetical column letters (like Excel columns: A, B, ... Z, AA, AB, ...).

Util.numberToAlphabet(1)    // => "A"
Util.numberToAlphabet(3)    // => "C"
Util.numberToAlphabet(26)   // => "Z"
Util.numberToAlphabet(27)   // => "AA"
Util.numberToAlphabet(703)  // => "AAA"
Util.numberToAlphabet(0)    // => ""

| Param | Type | Description | |-------|------|-------------| | n | number | 1-based index | | Returns | string | Alphabetical string, or '' if n ≤ 0 or undefined |


formatNumberConvert(num): string (standalone export)

Formats large numbers into compact notation (K, M, B).

formatNumberConvert(999)          // => "999"
formatNumberConvert(1500)         // => "1.5K"
formatNumberConvert(2500000)      // => "2.5M"
formatNumberConvert(3000000000)   // => "3B"
formatNumberConvert(10000)        // => "10K"

| Param | Type | Description | |-------|------|-------------| | num | number | Number to format | | Returns | string | Compact string with K/M/B suffix |


🎨 Color Utilities

Util.hexToRGB(hex): string

Converts a hex color string to rgb() or rgba() format. Supports 3-char, 6-char, and 8-char (with alpha) hex strings, with or without #.

Util.hexToRGB('#ff5733')     // => "rgb(255, 87, 51)"
Util.hexToRGB('#ff573380')   // => "rgba(255, 87, 51, 128)"
Util.hexToRGB('f00')         // => "rgb(255, 0, 0)"
Util.hexToRGB('00ff00')      // => "rgb(0, 255, 0)"

Util.rgbToHex(rgba): string

Converts an rgb() or rgba() string to hex format. Always returns 8-character hex (with alpha).

Util.rgbToHex('rgb(255, 87, 51)')        // => "#ff5733ff"
Util.rgbToHex('rgba(255, 87, 51, 0.5)')  // => "#ff573380"
Util.rgbToHex('rgba(0, 0, 0, 1)')        // => "#000000ff"

Throws Error if the input string is not a valid RGB/RGBA format.


Util.colorNameToHex(color): string

Converts a CSS named color to its hex value (without # prefix). Supports 140+ standard CSS color names.

Util.colorNameToHex('tomato')       // => "ff6347"
Util.colorNameToHex('dodgerblue')   // => "1e90ff"
Util.colorNameToHex('coral')        // => "ff7f50"
Util.colorNameToHex('black')        // => "000000"

Util.percentToHex(p): string

Converts a percentage (0–100) to a 2-digit hex string (00–FF). Useful for alpha channel values.

Util.percentToHex(100)  // => "FF"
Util.percentToHex(50)   // => "80"
Util.percentToHex(0)    // => "00"
Util.percentToHex(75)   // => "BF"

Util.hexToPercent(h): number

Converts a 2-digit hex string back to a percentage (0–100).

Util.hexToPercent('FF')  // => 100
Util.hexToPercent('80')  // => 50
Util.hexToPercent('00')  // => 0

Util.generateRandomColor(): string

Generates a random hex color string.

Util.generateRandomColor()  // => "#a3f29b" (random each call)

Util.generateLightColorRgb(): string

Generates a random light/pastel color in rgb() format (each channel between 230–255).

Util.generateLightColorRgb()  // => "rgb(240,235,245)" (random light color)

Util.generateDarkColorRgb(id?): string

Generates a dark color in HSL format. If an id is provided, the color is deterministic — the same id always produces the same color. Great for assigning consistent avatar/badge colors.

// Random dark color
Util.generateDarkColorRgb()
// => "hsl(142, 80%, 25%)"

// Deterministic from number
Util.generateDarkColorRgb(42)
// => "hsl(42, 80%, 25%)"

// Deterministic from string (hashed)
Util.generateDarkColorRgb('user-abc')
// => "hsl(237, 80%, 25%)"

// Same id = same color every time
Util.generateDarkColorRgb('user-abc') === Util.generateDarkColorRgb('user-abc')
// => true

| Param | Type | Description | |-------|------|-------------| | id | number \| string | Optional. Seed for deterministic color | | Returns | string | HSL color string with 80% saturation, 25% lightness |


Util.getRandomGradient(seed): string

Generates a deterministic CSS linear gradient from a seed string. Same seed = same gradient every time. Produces light/pastel colors suitable for backgrounds.

Util.getRandomGradient('project-123')
// => "linear-gradient(90deg, rgb(230, 210, 245), rgb(240, 235, 220))"

Util.getRandomGradient('user-abc')
// => "linear-gradient(90deg, rgb(245, 232, 228), rgb(230, 240, 235))"

// Use as inline style
<div style={{ background: Util.getRandomGradient(user.id) }} />

| Param | Type | Description | |-------|------|-------------| | seed | string | Seed value for deterministic output | | Returns | string | CSS linear-gradient(90deg, ...) string |


📝 String Utilities

Util.toSlug(input): string

Converts a string to a URL-friendly slug. Handles Vietnamese diacritics, accents, and special characters.

Util.toSlug('Xin Chào Thế Giới!')     // => "xin-chao-the-gioi"
Util.toSlug('Hello World 2024')         // => "hello-world-2024"
Util.toSlug('Đây là Tiếng Việt')       // => "day-la-tieng-viet"
Util.toSlug('  Multiple   Spaces  ')    // => "multiple-spaces"

Util.convertToKebabCase(str): string

Converts a camelCase, PascalCase, or space/underscore-separated string to kebab-case.

Util.convertToKebabCase('myComponentName')  // => "my-component-name"
Util.convertToKebabCase('Hello World')       // => "hello-world"
Util.convertToKebabCase('some_value')        // => "some-value"
Util.convertToKebabCase('backgroundColor')   // => "background-color"

Util.kebabToCamelCase(str): string

Converts a kebab-case string to camelCase.

Util.kebabToCamelCase('my-component-name')  // => "myComponentName"
Util.kebabToCamelCase('background-color')    // => "backgroundColor"
Util.kebabToCamelCase('font-size')           // => "fontSize"

Util.randomString(length): string

Generates a random alphanumeric string (a-z, A-Z, 0-9) of the given length.

Util.randomString(8)    // => "aB3kZ9mQ" (random)
Util.randomString(16)   // => "xR4pL2nW8vK1jM6q" (random)
Util.randomString(32)   // => 32-character random string

Util.extractHashtags(content): string[]

Extracts all hashtags from a string (also works with HTML content).

Util.extractHashtags('Hello #world and #react developers')
// => ["#world", "#react"]

Util.extractHashtags('<p>Check out #typescript and #vite</p>')
// => ["#typescript", "#vite"]

Util.extractHashtags('No hashtags here')
// => []

🗂 File Utilities

Util.stringToFile(content, fileName, type?): File

Creates a File object from a string. Useful for generating downloadable files or preparing upload payloads.

const jsonFile = Util.stringToFile('{"key":"value"}', 'data.json', 'application/json')
// => File { name: "data.json", type: "application/json", ... }

const csvFile = Util.stringToFile('name,age\nAlice,30', 'users.csv', 'text/csv')
// => File { name: "users.csv", type: "text/csv", ... }

const textFile = Util.stringToFile('Hello world', 'note.txt')
// => File { name: "note.txt", type: "text/plain", ... }

| Param | Type | Default | Description | |-------|------|---------|-------------| | content | string | — | File content as string | | fileName | string | — | Name of the file | | type | string | "text/plain" | MIME type | | Returns | File | | A File object |


💾 Storage Utilities

Convenience wrappers around the browser's localStorage, sessionStorage, and document.cookie APIs.

localStorage

| Method | Signature | Description | |--------|-----------|-------------| | Util.setStorage | (key: string, value: string) => void | Store a value | | Util.getStorage | (key: string) => string \| null | Retrieve a value | | Util.removeStorage | (key: string) => void | Remove a specific key | | Util.clearStorage | () => void | Clear all localStorage |

Util.setStorage('theme', 'dark')
Util.getStorage('theme')       // => "dark"
Util.removeStorage('theme')
Util.getStorage('theme')       // => null
Util.clearStorage()            // clears everything

sessionStorage

Data is automatically deleted when the browser tab is closed.

| Method | Signature | Description | |--------|-----------|-------------| | Util.setSession | (key: string, value: string) => void | Store a value | | Util.getSession | (key: string) => string \| null | Retrieve a value | | Util.removeSession | (key: string) => void | Remove a specific key | | Util.clearSession | () => void | Clear all sessionStorage |

Util.setSession('tempData', JSON.stringify({ step: 3 }))
Util.getSession('tempData')    // => '{"step":3}'
Util.removeSession('tempData')
Util.clearSession()

Cookies

| Method | Signature | Description | |--------|-----------|-------------| | Util.setCookie | (cname: string, cvalue: number \| string, exdays?: number) => void | Set a cookie (default 30 days expiry) | | Util.getCookie | (cname: string) => string | Get a cookie value (returns "" if not found) | | Util.deleteCookie | (cname: string) => void | Delete a specific cookie | | Util.clearCookie | (exceptCookie?: string[]) => void | Clear all cookies, optionally keeping specified ones |

Util.setCookie('token', 'abc123', 7)      // expires in 7 days
Util.setCookie('session', 'xyz', 1)       // expires in 1 day
Util.getCookie('token')                   // => "abc123"
Util.deleteCookie('token')
Util.getCookie('token')                   // => ""
Util.clearCookie(['session'])              // clears all cookies EXCEPT 'session'
Util.clearCookie()                         // clears ALL cookies

🔐 Auth & Encoding

Util.decodeJwtResponse(token): object

Decodes a JWT token and returns the parsed payload as a JavaScript object. Does not verify the signature — use for client-side display only.

const payload = Util.decodeJwtResponse('eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ...')
// => { sub: "1234567890", name: "John Doe", iat: 1516239022 }

| Param | Type | Description | |-------|------|-------------| | token | string | A JWT token string | | Returns | object | Parsed payload object |

⚠️ Security note: This only decodes, it does not validate. Always verify tokens server-side.


🖨 JSON Formatting

Util.prettyJsonToString(data): string

Converts a JSON object to a pretty-printed HTML string with <br> for newlines and &nbsp; for indentation. Useful for rendering formatted JSON inside HTML elements.

Util.prettyJsonToString({ name: "John", age: 30 })
// => '{<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"name": "John",<br>...'

Util.syntaxHighlight(json): string

Converts a JSON object to a syntax-highlighted HTML string with <span> wrappers for each value type. Useful for building JSON viewers/debuggers.

Util.syntaxHighlight({ active: true, count: 42, name: "test" })
// Returns HTML with:
//   <span className="boolean">true</span>
//   <span className="number">42</span>
//   <span className="string">"test"</span>

CSS class types: number, string, boolean, null, key


🆔 ID & Random Generation

randomGID(): string (standalone export)

Generates a random globally-unique ID using crypto.randomUUID() with dashes removed. Returns a 32-character hex string.

import { randomGID } from 'wini-web-components'

randomGID()  // => "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
randomGID()  // => "f7e8d9c0b1a2f3e4d5c6b7a8f9e0d1c2" (different each call)

Backend-Driven Modules

These components connect to the Wini backend and render entire UIs from server-side configuration (layouts, forms, cards, views). They require WiniProvider to be set up with a valid pid and url.

All five components share a common customization API: propsData, childrenData, and itemData — which let you override specific elements by their layer ID (the id field set in the Wini editor for that element).


PageById

Renders a full page (layout shell + body content) fetched from the backend by page ID. Typically used inside a <Route>.

import { PageById } from 'wini-web-components'

// Basic — renders the full page with its layout
<Route path="/dashboard" element={<PageById id="PAGE_ID" />} />

// Override props of a specific layer by its element ID
<PageById
  id="PAGE_ID"
  propsData={{
    "element-gid": { style: { backgroundColor: 'red' }, className: 'my-class' }
  }}
/>

// Replace children of a layer entirely
<PageById
  id="PAGE_ID"
  childrenData={{
    "element-gid": <MyCustomComponent />
  }}
/>

// Replace a layer entirely
<PageById
  id="PAGE_ID"
  itemData={{
    "element-gid": <MyCustomComponent />
  }}
/>

// Render only the layout shell (without body content)
<PageById id="PAGE_ID" onlyLayout />

// Render only the body content (without layout shell)
<PageById id="PAGE_ID" onlyBody />

PageByUrl

Same as PageById but looks up the page by its URL path configured in the Wini backend. Useful for dynamic routing where page IDs are not known at build time.

import { PageByUrl } from 'wini-web-components'

// Renders the page registered under "/dashboard" in the Wini backend
<PageByUrl url="/dashboard" />

// Inject custom children into the layout body slot
<PageByUrl url="/dashboard" onlyLayout>
  <MyBodyContent />
</PageByUrl>

// Same customization API as PageById
<PageByUrl
  url="/dashboard"
  propsData={{ "element-gid": { style: { color: 'blue' } } }}
  childrenData={{ "element-gid": <CustomWidget /> }}
/>

FormById

Renders a data-entry form from backend configuration by form ID. Manages its own react-hook-form state internally. Exposes an imperative ref for programmatic submit.

import { FormById } from 'wini-web-components'
import { useRef } from 'react'

const formRef = useRef(null)

// Basic — form submits and calls onSubmit with field values
<FormById
  id="FORM_ID"
  onSubmit={(data) => console.log('submitted:', data)}
  onError={(errors) => console.error('validation failed:', errors)}
/>

// Pre-populate with existing data (e.g. edit mode)
<FormById
  id="FORM_ID"
  data={{ Name: 'Alice', Age: 30 }}
  onSubmit={(data) => updateRecord(data)}
/>

// Load data from backend by query
<FormById
  id="FORM_ID"
  controller={{ searchRaw: '@Id:{some-id}' }}
  onSubmit={(data) => updateRecord(data)}
/>

// Load data from backend by IDs
<FormById
  id="FORM_ID"
  controller={{ ids: 'id1,id2', maxLength: 1 }}
  onSubmit={(data) => updateRecord(data)}
/>

// Trigger submit programmatically via ref
<FormById ref={formRef} id="FORM_ID" onSubmit={handleSubmit} />
<Button label="Save" onClick={() => formRef.current?.onSubmit()} />

// Supply custom dropdown options for a relation field
<FormById
  id="FORM_ID"
  customOptions={{ categoryId: [{ id: '1', name: 'Tech' }, { id: '2', name: 'Art' }] }}
  onSubmit={handleSubmit}
/>

// Customize a specific field's wrapper props or replace it entirely
<FormById
  id="FORM_ID"
  propsData={{ "field-element-gid": { style: { display: 'none' } } }}
  itemData={{ "field-element-gid": <MyCustomField /> }}
  onSubmit={handleSubmit}
/>

Ref handle:

formRef.current.onSubmit()           // trigger submit programmatically
formRef.current.methods              // UseFormReturn — full react-hook-form instance
formRef.current.cols                 // resolved column definitions from backend
formRef.current.rels                 // resolved relation definitions from backend

CardById

Renders a repeating list of items using a card template configured in the backend. Fetches its own data, supports pagination, filtering, sorting, and exposes a ref for imperative data refresh.

import { CardById } from 'wini-web-components'
import { useRef } from 'react'

const cardRef = useRef(null)

// Basic — fetches and renders all items
<CardById id="CARD_ID" />

// With query controller (search + pagination)
<CardById
  id="CARD_ID"
  controller={{ page: 1, size: 10, searchRaw: '@Status:[0 1]', sortby: [{ prop: 'Name', direction: 'ASC' }] }}
  onLoaded={({ data, totalCount }) => setTotal(totalCount)}
/>

// Provide data directly (skip backend fetch)
<CardById
  id="CARD_ID"
  cardData={[{ Id: '1', Name: 'Item A' }, { Id: '2', Name: 'Item B' }]}
/>

// Fetch all records
<CardById id="CARD_ID" controller="all" />

// Custom render per item — functions receive (itemData, index, methods)
<CardById
  id="CARD_ID"
  propsData={{
    "element-gid": (item, index) => ({ style: { color: item.IsActive ? 'green' : 'red' } })
  }}
  childrenData={{
    "element-gid": (item, index) => <span>{item.Name}</span>
  }}
  itemData={{
    "element-gid": (item, index) => <MyCustomRow data={item} />
  }}
/>

// Empty state
<CardById id="CARD_ID" emptyMessage="No items found" emptyLink="/create" />

// Imperative refresh
<CardById ref={cardRef} id="CARD_ID" controller={{ page: 1, size: 10, searchRaw: '*' }} />
<Button label="Refresh" onClick={() => cardRef.current?.getData(1)} />

Ref handle:

cardRef.current.getData(page?)       // re-fetch data (optionally set page)
cardRef.current.data                 // { data: Array<{...}>, totalCount?: number }
cardRef.current.setData(newData)     // manually update rendered data
cardRef.current.controller           // current active query controller

ViewById

Renders a single item detail view from backend configuration. Similar to CardById but for one record instead of a list.

import { ViewById } from 'wini-web-components'

// Pass data directly (e.g. from a parent list)
<ViewById
  id="VIEW_ID"
  data={{ Id: 'abc', Name: 'Alice', Email: '[email protected]' }}
/>

// Or let it fetch by query
<ViewById
  id="VIEW_ID"
  controller={{ searchRaw: `@Id:{${recordId}}` }}
  onLoaded={({ data }) => setRecord(data)}
/>

// Override specific elements — same API as PageById/CardById
<ViewById
  id="VIEW_ID"
  data={record}
  propsData={{ "label-gid": { style: { fontWeight: 'bold' } } }}
  childrenData={{ "value-gid": <strong>{record.Name}</strong> }}
/>

Responsive Grid Classes

The layout system uses a 24-column grid. Add these classes to children inside a row.

| Class | Columns | Notes | |---|---|---| | col1col24 | 1/24 – 24/24 | Always applied | | remain | fills rest | flex: 1 | | col1-mincol24-min | at < 576px | phones | | col1-smcol24-sm | at ≥ 576px | small devices | | col1-mdcol24-md | at ≥ 768px | tablets | | col1-lgcol24-lg | at ≥ 992px | laptops | | col1-xlcol24-xl | at ≥ 1200px | desktops | | col1-xxlcol24-xxl | at > 1200px | wide screens |

// 2-column on desktop, stacks on mobile
<div className="row" style={{ gap: 16 }}>
  <div className="col24 col12-lg" style={{ "--gutter": "16px" }}>Left panel</div>
  <div className="col24 col12-lg" style={{ "--gutter": "16px" }}>Right panel</div>
</div>

Design Tokens & Theming

Colors are defined as CSS variables in root.css and can be overridden by WiniProvider at runtime.

/* Default token examples */
--primary-main-color: #287CF0;
--primary-bolder-color: #0F62D7;
--neutral-bolder-border: 1px solid #D7D7DB;
/* semantic */
--success-main-color: ...;
--error-main-color: ...;
--warning-main-color: ...;

Dark mode is toggled by adding the .dark class to <html>. Use setTheme from useWiniContext:

const { setTheme } = useWiniContext()
setTheme('dark')   // adds .dark to <html>
setTheme('light')  // removes .dark from <html>