@maxenve/rui-core
v0.1.14
Published
rui's components: a complete set of layout, inputs, combobox, buttons, navigation, feedback, overlays, data display and typography, with hooks, forms and extensions (dates, charts, notifications, launcher, carousel, dropzone, modals, snippet, load bar, ed
Readme
rui-core
rui's components: a complete set of layout, inputs, combobox, buttons, navigation, feedback, overlays, data display and typography, with hooks, forms and extensions, built on ren-core's portable primitives. The same component renders the same on ren-dom (the browser lays it out), ren-skia and ren-three (ren-canv lays it out with Taffy and paints it), in a worker too, since it is only application code.
import { $signal, $active, $read, $write } from 'ren-core';
import { Rui, Surface, Stack, Heading, Text, Row, Button } from 'rui-core';
const Counter = () => {
const count = $signal($active(), 0);
return (
<Rui scheme="dark">
<Surface p="md" bordered>
<Stack gap="sm">
<Heading level={3}>Counter</Heading>
<Text fg="quiet">{() => `pressed ${$read(count)} times`}</Text>
<Row>
<Button onPress={() => $write(count, $read(count) + 1)}>Add</Button>
<Button variant="soft" color="red" onPress={() => $write(count, 0)}>Reset</Button>
</Row>
</Stack>
</Surface>
</Rui>
);
};The package is published on npm as @maxenve/rui-core under the MIT license;
install it under its import name: npm install rui-core@npm:@maxenve/rui-core.
Its components ship as .jsx source, which the application's JSX build
compiles (ren-core's README, "Components and JSX"): a bundle built with
--jsx=automatic --jsx-import-source=ren-core or with the ren-core/esbuild
plugin, or node --import ren-core/loader under Node.
Theme
BASE_THEME is the theme every component starts from:
colors: 13 hues in color-wheel order (red, orange, amber, yellow, lime, green, teal, cyan, blue, indigo, violet, purple, magenta), then gray and night, each 10 shades (0 lightest) drawn in OKLCH on one lightness curve and one chroma curve, so a shade index means the same weight in every hue. White text on shade 6 passes WCAG AA in every hue.accentindigo ataccentShade6 in both schemes;cornermd (the radius components take by default);contrastoff (solid text is white;contrast: truepicks black on fills pastcontrastThreshold.179, where black and white text have equal contrast; checks and dots always follow the threshold);gradientteal to indigo at 120 degrees.spacing(8, 12, 16, 24, 32 on a 4 px grid),radius(3, 6, 10, 14, 24),fontSizes(12, 13, 15, 17, 19),lineHeights,breakpoints(xs 32em, sm 44em, md 60em, lg 72em, xl 90em),shadows,extra(free values for the application).headings:{ weight: 700, levels: [{ size, leading }, …] }, level 1 first: 36, 29, 23, 19, 17 and 15 px, a 1.25 ratio up from the body.
themeOf(overrides, base) merges deeply over BASE_THEME (a color's shades
replace); Rui provides a theme and a scheme ('light' or 'dark'),
each a value or a signal. A new theme or scheme restyles every component in
place: a component's style is one binding (styled(props, own)).
The theme's own tables (colors, spacing, sizes and the rest) have no
prototype, so a name such as toString is no entry: read them with
Object.hasOwn or in, not .hasOwnProperty. extra is the
application's own data and keeps its prototype.
The words components show of their own (a close button's name, 'Clear',
'Show password', 'Loading', a chart's summary, …) are BASE_LABELS,
English, keyed by what each names; the ones that take a value are
functions of it (remove(value), step(i, n), chartSummary(kind,
names, points)). Rui's labels puts other words over them for
everything inside (a map, or a signal or accessor of one; a nested Rui's
over the outer's), and a change of the words reaches what is shown. A
component's own prop for a text (label, placeholder, moreLabel, …)
still wins. $ui().labels() reads them. The dates extension's words are
DateSettings' labels, the editor's its labels, and Dropzone's
rejections REJECT_LABELS.
Colors: a theme color name (its accent shade),
'name.shade'('blue.3','night.7'), or CSS hex.accentShade()andpageColors()read the theme for a scheme.Color roles (
pageColors(theme, scheme)): components never name a gray or night shade. Every surface, edge, track and hover reads a role, andsrc/theme/resolve.jsis the one place that maps roles to shades (a test fails on a raw shade anywhere else insrc/). Tinted grounds of a theme color useTINTS.| Role | For | Light | Dark | | --- | --- | --- | --- | |
body| the page | white | night.8 | |text| body text | gray.9 | gray.1 | |quiet| secondary text, inactive labels (7 : 1 or more) | gray.7 | night.1 | |placeholder| hints, outside days | gray.6 | night.2 | |border| field and control edges (3 : 1) | gray.5 | night.3 | |rule| dividers, card edges, soft outlines | gray.2 | night.5 | |edge| thick lips and grips (Key's lip, Panes' grips) | gray.4 | night.4 | |neutral| theneutralvariant's ground | white | night.6 | |hover| hover on page-colored surfaces and rows | gray.0 | night.5 | |field| input ground | white | night.7 | |sunken| wells below the page: code, Snippet, Tag, Key, soft inputs | gray.1 | night.9 | |raised| surfaces above the page: Card, Notification, panels | white | night.7 | |track| the unfilled part of sliders, switches, progress, rings; Skeleton | gray.2 | night.5 | |stripe| alternate table rows | gray.2 atopacity.soft| night.7 | |disabled,disabledText| disabled ground and text | gray.1, gray.4 | night.7, night.4 |In the light scheme a raised panel is white on white, set off by its shadow and
ruleedge; in the dark scheme it is lighter than the body.sunkengoes below the page in both. Tooltips and the Slider's value label weartooltipColors, the other scheme's roles.Variants (
variantColors):solid,soft,outline,ghost,neutral,plain,inverse. Solid text is white; withcontrast: truethe theme picks black on fills pastcontrastThreshold. Tinted grounds use one scale (TINTS: rest .14, hover .22, wash .08 in light; .22, .30, .12 in dark), with text at the deep shade (8 in light, 3 in dark). Button, IconButton, Badge and IconBox takevariant="gradient"withgradient={{ colors, angle }}(two or more colors spread evenly along the angle; the theme'sgradient, teal to indigo at 120, by default).Layers (
LAYERS):raised10 (Pin, Busy, sticky heads),sheet20 (Modal, Drawer, Dialog, ActionBar, Window, Floater, Launcher),float30 (Popover, HoverCard, Menu, the combobox and picker panels),tip35 (tooltips),toast40 (Notifications),top50 (LoadBar). A sheet sits under the panels it opens. On a viewport narrower than the theme'ssmbreakpoint a sheet fills it, so notices in the portal layer claim their edge's room (spacing.lgin, their height and aspacing.smstep) and the sheets placed against the viewport stand clear of it: a Modal's scroller ends there (its content scrolling up with the bottom room, so what stood above it stays in view), a Drawer's edge box and a Dialog's top or bottom inset count from it. Wider, nothing moves.Motion (
MOTION):quick110 ms,base190,calm300, andhold1500 (how long a confirmation stays), easing ease-out; amotionprop takes a preset's name or{ effect, duration, exit, easing }. Popovers, menus and tooltips move quick, Modal and Dialog base, Drawer calm.Responsive values, in object form (
columns={{ base: 1, sm: 2, lg: 4 }}): the value of the widest breakpoint the application root's viewport reaches (min-width, mobile first; ren-core's$viewport, so on every renderer and in a worker),basebelow them. Grid (gap, Grid.Colspan,offset,order), Tiles (columns,gap,rowGap), Shell (part sizes, andfold: below it a nav or side lies over Main, hidden withhide: { nav: 'narrow' }) and Carousel (span,gap) take them.Style props on every component:
m,mx,my,mt,mr,mb,ml,p, … (scale steps or pixels),w,h,minWidth,maxWidth,minHeight,maxHeight,bg,fg('quiet'too),fontSize,fontWeight,textAlign,textTransform,lineHeight,opacity,flex, thenstyle(the portable style) over them. A margin or padding prop for some sides (py,mt) sets those; the others keep the component's own.
Conventions
One word for one concept, the same in every component, and no DOM or CSS vocabulary, so a name means the same on every renderer.
- Presses are
onPress; a drag's end isonCommit; a combobox option chosen isonPick. - The main value is
value,defaultValueandonChange(checkedanddefaultCheckedfor choices). Any other statexisx,defaultXandonXChangetogether:open,defaultOpen,onOpenChange(open)for everything that opens or discloses (Modal, Drawer, Popover, Menu, Tooltip, Excerpt, NavItem, the combobox fields, Launcher, Window; Collapse readsopenalone);search,sort,expanded(JsonTree too, as node ids: the path's keys joined by dots, a key's\and.escaped with a backslash),shown(PasswordInput; Busy, Transition, Dot and Combobox.Panel readshownalone),period(Calendar and the date panels: the month, year or decade on view),flipped(Flip),started(Onboarding) likewise. Handles take options last: CarouselgoTo(index, { instant }). - Content before and after the main content is
startandend(withstartWidth,endWidth); a side isstartorend, never left or right (labelAt,checkAt,chevronAt,listAt). Placement against something isat('top','bottom-start', …). Paired content isbeforeandafter(Compare), paired navigationpreviousandnext(previousLabel,nextIcon). - A boolean that shows a part is the part's noun (
arrow,legend,tooltip,controls,dots,check,backdrop,portal); a quality is an adjective (bordered,closable,copyable,removable,sticky,persistent). A boolean names what is on; its default is true where the part shows by default (controls={false},outside={false}). - A part prop takes four forms:
false(no part),true(the built-in part), a node (your own) or an object of options for the built-in one:closable={{ label: 'Dismiss' }},arrow={{ size: 8 }},backdrop={{ opacity: 0.4 }},motion={{ effect: 'fade', duration: 150 }},xAxis={{ title: 'Month' }},chevron={{ size: 12, turn: false }}. A styleable part has its own*Styleprop (fieldStyle,outerStyle); a per-item function ends inOf(dayOf,tipOf,iconOf); render callbacks arerender*. Commands come throughgetHandle(handle). - Overlays:
dismiss({ escape, outside }, orfalse),trap,refocus,portal(true),layer(stacking order),persistent(built while closed),stack,full,offset.EFFECTSnames the motions:fade,riseandsink(in from below or above, by spacing xs),from-startandfrom-end,grow(about the center), andslide-top,slide-bottom,slide-start,slide-end(in from that edge). - Parts are reached as
Parent.Partonly. Shared roles use these names:Trigger(what opens),Panel(what opens),List(the options),Close,Frame(the composition base),Backdrop,TrackandSegment(progress), and Table'sHead,Body,Foot,Row,Heading,Cell,Scroll; other parts are named by what they are (Grid.Col,Card.Media,Modal.Body,Carousel.Slide). - Variants:
solid,soft,outline,ghost,neutral,plain,inverse,gradient,dot; fieldsoutline,soft,bare; Accordionplain,boxed,soft,split; Tabsline,outline,pill. A shape iskind(Loaderring,bars,dots), a line styleline, a date picker's selectionmode(single,multiple,range). - A provider is named by what it provides (
Rui,DateSettings,Modals,ModalHost); a context is a noun (Theme,Scheme,Clock); a store is made by<noun>Store()(notificationsStore,loadBarStore,launcherStore, asmanualClock()), reached by$name()and its*Context, read through accessors (isOpen(),active()) and driven by verbs (open(),next()). - Hooks are
$and a plain word or two:$open,$list,$patch,$bound,$model,$history,$pages,$debounced,$debounce,$throttled,$outside,$shortcuts,$trap,$spy,$scheme,$fits,$online,$title, … - What a panel opens from is its
trigger(width: 'trigger',Flip.Trigger, the combobox'ssetTrigger); Onboarding's highlighted element is itsSpot. - A choice of form is a
kind(chart kindsgroup,overlap,stack,share,diverge,cascade;MixedChartseries; TimePicker; Loader). Line and grid choices share'rows' | 'columns' | 'both' | false(Tablelines, chartgrid); signs and trends areupanddown. - An option is
{ value, label, disabled }, or{ …, action: true, icon }for one that does something rather than stand for a value (a list's "Manage…"): drawn apart, under a rule, as a soft button with its icon. - Grouped data is
{ label, options }(Launcher:{ label, actions }); a change reports the value only, andonPick(value, option)gives the chosen item. Chart items are{ label, value, color }. A search matches one item at a time,match(item, search) → boolean, andlimitcaps the shown count (Select, Autocomplete, Picker, Cascader, Launcher; Cascader's item is a path,match(nodes, search)with its nodes from the root);maxcaps the picks (MultiSelect, TagInput, TreeSelect). - DataTable reads a
fieldsmap keyed by the row path ('address.city'): its key order is the column order, and a field left out is not shown;order { by, desc },pages,pickinganddensityare one prop each. - One part takes a value:
Editor.Heading level,Editor.Align value,Dropzone.Show when,Menu.Branch label. Label maps are keyed by the part's own name (LABELS.bold,LABELS.heading(2)). - Results use short verbs and plain returns:
$form'svalidate() → { ok, errors };$pages→{ items, page, go, step };$modalStack(ids?)→{ value (the open ids, bottom to top), top(), isOpen(id), open, close, toggle, clear, bind(id) }(an id outside a given list throws); LoadBar'srun,hold,to,add,finish,clear;$modals'show,ask(a Promise of true or false),showNamed,hide,clear,update;$notifications'show,hide,update,clear;$treeState'sexpand,collapse,select,deselect,expandAll,collapseAll. - A hook returns one handle: its state as rrx-core nodes
named by nouns, the main one always
value(read with$read, or given to JSX as is), and its commands as verbs.const menu = $open(false); menu.value; menu.toggle(). Options are one object after the hook's own inputs; collections use JavaScript's verbs (push,unshift,pop,shift,filter,map; a Set'sadd,delete,has,clear). Each handle has a type named…Handle(OpenHandle,ListHandle<T>). - Numbers take their marks from the locale (
Intl.NumberFormat): alocaleprop, the platform's by default (Pie and Donut: 'en-US', see Defaults); an explicitgroupMarkordecimalMarkstring wins. - Date formats take the tokens
YYYY MM DD HH mm,EEEE,a, …; dates are ISO 8601;weekStartis 1 andweekend[0, 6]; time is 24-hour, or 12-hour with AM and PM. - Modifiers: Button
dense, IconButton and CloseButtonfield(as tall as a field of that size). - Text is selectable only where a component means it: an Edit,
Code,Snippet, or anything styledselect: 'text'; the rest is an app's, not a page's (no I-beam, no page-wide Ctrl+A, no browser menu outside an Edit). The renderer'sbehavioroption ('page', or one switch such as{ like: 'app', select: true }) brings a page's back. - An application's own menus, off by default, the same on the DOM, Skia
and three.js (each acts through ren-core's text actions):
- The field menu: with
control.editMenu'app' in the theme (or a field's owneditMenuprop, which wins: every text field, the combobox fields' search, Combobox.Filter, TagInput's entry, Launcher's field, the Editor's link field), a right press, a finger's long press, Shift+F10 or the ContextMenu key opens rui's menu in place of the platform's: Undo, Redo, Cut, Copy, Paste and Select all, each with its keys as the platform writes them ('Ctrl+X', '⌘X'). It keeps the field's selection and focus (opened by the keys, the focus goes to its first item); each item puts them back before it acts; Cut and Copy leave a secret field alone, and Undo, Redo, Cut and Paste a read-only one; a disabled field keeps the platform's. Paste reads the clipboard ($clipboard'spaste); where the platform refuses, the item turns into 'Paste with Ctrl+V' and the menu stays open. A right press on a combobox's field opens this menu, not its list. In worker mode the field's actions stay on the main thread, so the platform's menu stays. The Editor's blocks keep their own behavior (the platform's menu). - The text menu: with
control.textMenu'text', a right press on static text (a Label's, not a press target's or a field's), with 'all' on any text but a field's (a button's, a pressable row's, a bar's too; while one is selected its presses select, never press it), opens Copy (that text) and Select text (that text alone becomes selectable, all of it selected, until a press elsewhere or Escape; a drag in it keeps a part, which Ctrl+C or the menu copies); over a selection it opens Copy of the selection. The outermostRuiholds it (no box: its menu is a portal). A ContextMenu's region or a field's menu that took the press keeps it. A finger's long press does not open it.
- The field menu: with
- The pointer over what presses is the theme's
cursor:press(a hand, 'pointer') over every press target (a button, an option, a pressed field all over and a field's chevron, a Box or Card withonPress, a bar…) anddisabled('not-allowed') over one that takes no press now. An app sets other CSS cursor names, ornullfor none (the renderer's own pointer):themeOf({ cursor: { press: null } }). It shows the same on the DOM, Skia and three.js. - Every component style number comes from a theme token, never an inline
literal:
opacity(faint .16, soft .3, disabled .45, muted .7, dense .88),weights(regular 400, strong 600 for labels, bold 700),ratios(glyph .42, mark .3, inset .3, icon .62: a share of a box's height; pad .75 of a font size),control(labelGap 6, panelPad 6, optionPad [6, 12], iconGap 8, textStep 1,padanddensePadby size, tabledensity),chart(pointArea 48, axisWidth 56, axisShare 1/3, band .72, bandGap 6, labelOffset 6, outer .84, inner .16, dim .16),sizes(every component's sizes by step: control heights, icons, avatars, checks, sliders, loaders, modals, …;glyphfor inline icons andboxfor components' default boxes),radius.roundfor pills and circles,lineHeights.code1.65, MOTION for durations, LAYERS for stacking, and the spacing, type and leading scales; a control's notes sitfontBelow(theme, size)(steps down the type scale), never a pixel offset. A theme overrides any of them withthemeOf.test/tokens.test.jsrefuses literal opacities (in styles, alpha() and '#rrggbbaa' colors), weights, leadings, font sizes, stacking orders, size divisions and decimal ratios, any literal in a padding, margin or gap, pill radii,durationdefaults and tween durations, px boxes, glyphs and offsets (props, defaults, widths and heights, UPPER_CASE px constants, default positions), hidden tables of them and size tables keyed by step, insrc/core,src/ext,src/hooksandsrc/form; drawing math may mark a line// geometry: <reason>and a loop period// cycle: <reason>, and those stay few.
Defaults
The defaults come from rui's scales: spacing, the palette, the motion presets and the layers.
| Component | Defaults |
| --- | --- |
| Modal | size 560, at 'top' at offset 64, padding lg, shadow lg, motion base grow; taller than the screen, it scrolls in its layer (a Drawer's body in its panel) |
| Drawer | at 'end', size 360, offset 0, motion calm |
| Dialog | at { bottom: lg, right: lg }, padding lg, shadow lg, motion base rise; its close in flow at the first row's end |
| Popover, Menu, HoverCard | padding 8 × 12, spacing xs × sm (Menu: panel 6, items 6 × 12, labels 600), offset 6, shadow md, motion quick; HoverCard delays 80 / 240 |
| Tooltip | the other scheme's roles (tooltipColors), padding 3 × 9 (half the label gap; ratios.pad of its font), font xs, offset 6, delays 500 / 100, at once on keyboard focus (ren-core's focusVisible: not a press's focus, nor one code gives after a press); Tooltip.Follow at 'bottom-end', offset 12 |
| Combobox panels | offset 4, motion quick fade |
| Backdrop, Busy | night.9 at opacity.disabled .45 (opacity.muted .7 dark), opacity fading the color or the gradient's stops (never the children); Busy while shown; Busy the body at .7; no blur |
| Buttons, Chip | side padding control.pad (10, 12, 16, 20, 24 from xs), dense and a checked Chip control.densePad (5, 6, 8, 10, 12) |
| Button.Group, IconButton.Group | a frame: its edge and a line between each pair of children as rendered (a For's items and an accessor's parts one by one, a Show showing nothing none) divider xs (1 px) in the page's border, the theme's corner, clipping its children; the children draw no border and no radius |
| Switch, SegmentedControl | duration quick and base (a MOTION name or ms) |
| Checkbox, Radio | variant 'solid' or 'outline'; the check ratios.icon of the box, Radio's dot ratios.mark |
| Card | a frame: Card.Media sits across the card's top (all media, in order; the first with the card's corner), the body padded below; Card.Rule a hairline across |
| Skeleton, Loader | Skeleton covers while loading and breathes between opacity.disabled and opacity.dense while pulse; the ring's track opacity.faint, resting dots opacity.soft |
| Onboarding | value 0 (Book too), layer float, backdrop at opacity.disabled; Flip, TextAnimate duration calm; Flip and Compare orientation 'horizontal'; Book two calm steps |
| QrCode, encodeQr | level 'M', versions [1, 40] (the smallest that fits), mask the lowest penalty, raise false (raises the level while the version still fits) |
| $modals | a record's kind 'ask', 'plain' or 'named' |
| Dropzone | accept one list (extensions, MIME types, image/* families; none: everything); past max the first files stay and the rest are rejected 'too-many'; maxSize Infinity, minSize 0; mimeTypes holds 35 extensions in eight kinds (images, audio, video, documents, sheets, slides, archives, text), alphabetical within each, their values IANA media types (tar, which has none registered, its common x- type), and each kind has its list (imageTypes, audioTypes, documentTypes, …) |
| Pin, Dot | Pin at { bottom: lg, right: lg }; Dot size 8, offset 0, anchor { x: 1, y: 0 } (fractions of the box), shown true; bordered adds a 1 px ring in raised |
| Notifications | at 'top-end', limit 4, timeout 5000 ms, width 360 |
| LoadBar | size 2, interval 400, the accent, layer top |
| Launcher | size 640 (sizes.box.launcherWidth) at offset 96, list height 360, no limit (as Select, Autocomplete, Picker, Cascader), loop true (the arrow keys wrap), shortcut 'primary+K', placeholder 'Find an action', empty text 'No matches' |
| Ring, Arc | Ring 112 × 10 (sizes.progress.lg); Arc 180 × 14, sweep 180 from start -sweep/2 (standing over the top, open at the bottom), label={{ at }} 'center' (or 'bottom', 'end'), a negative sweep fills the other way; motion calm |
| Notification | a tone dot (ratios.mark of the first line) or the icon in its color, title strong, message in the text color one step below, dimmed to opacity.muted under a title, loading a rule along the bottom edge, edge border, shadow md, closable at xs |
| Progress | sizes 3, 4, 6, 10, 14 |
| Badge | heights 18, 20, 22, 26, 30, radius sm |
| Timeline | value none (nothing reached), marker 16, line 2 |
| Excerpt | height 120, 'More' / 'Less' |
| Carousel | align 'start', span 100 %, ends 'open' (or 'fill': the frame stays filled), dots named n / total, duration calm, buttonSize 32, buttonInset xs, autoplay 5000 (never starting itself while the user asks for reduced motion; its play still starts it); a percent span holds the gap; its region and slide groups carry aria-roledescription |
| CopyButton, $clipboard | hold MOTION.hold (1500 ms); CopyButton 'Copy' / 'Done' (as Snippet) |
| Field menu, text menu | control.editMenu 'browser' (the platform's; 'app': rui's own), control.textMenu 'off' ('text', 'all'); the menu at the press's point ('bottom-start'), a long press held 600 ms (twice MOTION.calm) by a finger or a pen, canceled past 10 px; 'Undo', 'Redo', 'Cut', 'Copy', 'Paste', 'Select all', 'Select text', pasteWithKeys(keys) ('Paste with Ctrl+V') |
| TagInput | separators [',', ';'] |
| FileInput | one file shows its name ('file' when it has none), several countText(n) ('N files'); empty is null in every mode, and a clear sets null |
| Chip.Group | single: null, toggle true (a press on the chosen chip clears it); multiple: [] in press order, max none |
| Rating | amber, size md, count 5, divisions 1 (whole symbols) |
| PinInput | length 6, accept 'digits', placeholder '·', gap xs |
| NumberInput | clamp 'commit' (on blur and Enter), marks from the locale |
| CurrencyInput | display 'symbol' ('narrowSymbol', 'code', 'name', 'none'), placed and spaced as the locale writes money (R$ 12,50; 12,50 €), a name following the amount; the currency's decimals (currencyDecimals) unless decimals is given, written out at rest and as typed until blur or Enter (fixedDecimals true; false: as the number is); NumberInput's other defaults |
| Combobox panels (height) | panelHeight 280 |
| Select, MultiSelect lightbox | the theme's control.lightbox (null: never; a breakpoint key below which the options open in a centered lightbox titled by the field's label), a filter there past control.typeahead (10) options or for a searchable Select, a MultiSelect's Done; the options' height the viewport's less sizes.box.boxed (240), at least panel |
| Combobox lists | as high as their options up to panelHeight (a ScrollBox.Fit), scrolling down only, without bars; an option's label too wide for the panel ends with an ellipsis (its full text still its name) |
| ColorPicker, ColorInput | format 'hex' (or 'rgb', 'hsl'), alpha false, no value until picked (opens on the accent), swatchColumns 8; picker true: the spectrum, then the swatches ('collapsed': the swatches, then 'More colors', which shows the spectrum under them) |
| ColorInput | value '' (no color); the panel at 'bottom-start', opened by a press or ArrowDown (not by focus); revert, sticky, typing, preview and eyedropper true; the empty preview a Swatch of 'transparent'; the eye dropper named 'Pick a color from the screen', disabled while the field is disabled or read-only |
| JsonArea, PasswordInput | JsonArea rows 4, indent 2, parseError 'Not valid JSON', pretty false; PasswordInput's toggle 'Show password' / 'Hide password' |
| Checkbox, Radio | 14, 18, 22, 26, 30 (xs to xl); Radio's dot ratios.mark (30 %); checkColor and dotColor black or white by the theme's contrast rule |
| Chip, Slider | radius the theme's corner |
| Tag | size xs, radius sm |
| Rings | 112 × 10, trackOpacity opacity.faint |
| Stepper | value 0; numbered chips (sizes.step at size, or chipSize) without a border, rounded by the corner up to a circle; a track (sizes.progress) between them that fills in the stage's tone once the stage is done; reach 'back' (a press moves back to a done stage only); gap lg between the stages and the content |
| Breadcrumbs, Pagination | '›' with gap sm between the crumbs as rendered (as Button.Group's lines); Pagination around 1 and ends 1, pages gap 4, ghost controls, the current page a soft wash with a tone underline |
| EmptyState | parts .Mark, .Heading, .Note, .Buttons (props mark, heading, note), aligned to the start |
| Progress | moving runs a sheen along the bar (apart from striped); Progress.Text inside it |
| Tree | store (from $treeState); renderItem({ open, chosen, handle }); the open nodes kept as a Set |
| DataList | rows apart by a gap and a hairline in the edge role between them |
| Menu.Branch, ActionBar | a branch opens at offset control.panelPad / 2 after MOTION.quick; ActionBar closes on Escape, its parts apart by a gap |
| Modal | Modal.Body padded spacing.xs at the top under a header |
| DigitRoll | easing 'settle' |
| Tree, NavItem, Outline | indent md (Outline from level 2, its value 0); NavItem ghost, padding control.optionPad, label md, an edge at its start while active; chevrons turn quick |
| Accordion | trigger padding control.optionPad, gap control.iconGap; panel [0, optionPad x, control.panelPad], indented under the label when the chevron is at the start; chevron sizes.glyph.md pointing to the end (or start) and turning 90° down as it opens, motion base |
| Tabs | tab padding control.optionPad (control.pad.sm across), label md strong, a hairline under the list, a round mark under the active tab; the outline variant raises the active tab with a top tone edge |
| Row, Flex | wrap a boolean: Row wraps by default, Flex does not; alignment takes these words only (start, end, center, between, around, stretch) |
| NavToggle | label 'Menu' |
| Link, NavItem | Link follows and shows only safe addresses; NavItem shows only safe addresses (it never navigates: its press is its onPress). Safe, as the editor keeps them: relative, #…, http:, https:, mailto:, tel:; any other scheme (javascript:, data:, read without case, control characters or whitespace) is not followed and shows no href |
| DataTable, Table | density 'even' from control.density (tight 4 × 8, even 8 × 12, airy 12 × 16); DataTable's empty text 'Nothing here yet'; a Table cell's number w (pixels) sizes its column (at least w, the rest shared; minmax(w, 1fr) with fixed), the cell stretching across it, a spanning cell's w its least width; a string w is the cell's own width |
| Shell | header 56, nav 280 |
| Panes | a number is pixels, a string '30%' a percent of the room (the container less its handles); a pixel list sizes every pane but the last (which takes the rest), a missing size sizes.box.pane (200); a percent list sizes every pane as a true percent of the room (under 100% in all leaves the rest empty, over 100% shares the room in proportion), the missing ones sharing what is left, each at least its min, a number in it pixels of the room; a given size outside its pane's bounds laid out and told within them; min 0 and max none (100% in a percent list), each in either unit; orientation 'horizontal'; handleSize spacing.xs (the handle's thickness), step its own prop, spacing.xs (the keys' step; Shift five), handle true: a sizes.divider.xs line in rule and a sizes.glyph.xl × sizes.track.sm grip in edge |
| Grid, Avatar.Group | Grid 12 columns, gap lg, Grid.Col span a number or 'auto'; Avatar.Group overlap ratios.mark of the side (or overlap), set apart by shadow sm |
| ScrollBox | bars true: the renderer's bars, as wide as its theme's bar, the thumb the page's border on its track (thin half as wide; false none, no room kept, still scrolling); onEnd (an endless list's next page) called within endGap (spacing lg) of the end down, after a scroll and as the box or its content changes size, once a frame at most, again once a promise it returns settles; ScrollBox.Fit as high as its content up to maxHeight (measured on every renderer: a Scroll without a height is the renderer's region high) |
| Marquee | repeat 2, gap lg, fade false (fadeSize xl when on, three stops to the page ground); the copies after the first are inert decoration (no Tab stop, no pointer, out of the accessibility tree), the first the live content; it holds still while the user asks for reduced motion |
| Dropzone | outline, teal valid, orange invalid, interactive |
| $form, $idle | touchOn 'blur', afterError 'check', onChange(values, previous); $idle starts active |
| Editor | links keep only safe addresses: relative, #…, http:, https:, mailto:, tel:; any other scheme (javascript:, data:, read without case, control characters or whitespace) makes no link, in HTML, JSON or link(href): the text stays, the mark is dropped; a document's text or highlight color is a CSS color: hex, a name, or rgb/hsl/hwb/lab/lch/oklab/oklch/color() of plain values (not var() or color-mix()), else the mark is dropped (a theme name such as teal.3 is resolved by Editor.Swatch and Editor.Colors before it reaches the document, never by the model); hex, rgb and hsl draw on every renderer, a CSS name and the other functions on the DOM only; highlights write background-color; in its HTML a list item's text is a paragraph (<li><p>…</p></li>) |
| Snippet | leading lineHeights.code 1.65 (the editor's code blocks too), collapsed height 240, 'Show all lines' / 'Fold', 'Copy' / 'Done' |
| Charts | signColors { up: 'teal', down: 'orange' }, a share axis 0 to 100 % (each value's share of its row's absolute total), reaching down in quarters to a negative value's share, grid 'rows' at opacity.muted, area fills opacity.faint, bars opacity.dense (fillOpacity), gridColor the page's border (muted), textColor quiet, dash '4 2', bridge false, curve 'monotone' (Sparkline's too; the curves are linear, monotone, natural, step, bump), an axis domain with the keywords 'dataMin' and 'dataMax'; Pie and Donut 180, drawn clockwise from three o'clock, gap 1° (pie) and 2° (donut), PieChart labelsAt 'inside' or 'outside', labels as percent (Intl, one decimal under 10 %) in locale 'en-US' by default (the exception to Conventions' platform locale); Funnel 260, labels inside, rows 2 px apart with a square last row; Radar grid, axes, points, tooltip, legend with two or more series; RadialBar labels at each bar's end and tooltip, thickness sizes.progress.xl (14); Bubble 300 tall on a value axis (as wide as its tick labels, chart.axisWidth at least, a third of its width at most), areas 24 to 400 px²; Heatmap cell { size: 12, gap: 2, radius: 'xs' } (px or a radius step), today's calendar year, months and weekdays on, legend and tooltip off; Bars a label line over a thin track (barSize sizes.progress.md), a long label wrapping beside its whole value, rows spacing sm apart, each pressed with onPress(item, index) (named by its label and value, the page's hover tint); a cartesian chart's categories chosen with onSelect(index, row) (a press, or ← → Home End then Enter or Space on the focused chart, the tooltip following the keys). Every legend, a Legend of its own too, sits under the chart at its start (bottom-start) and follows a changing at; points are circles of chart.pointArea; the tooltip is a table of tone dots and names over their values on the sunken ground. The rest reads theme.chart: point area 48 px², a value axis as wide as its tick labels (charWidth a character, beside its tick), 56 at least and axisShare (a third) of the chart's width at most (a longer label ends with an ellipsis), a band's bars .72 of it with gap 6, labels offset 6, RadialBar radii .84/.16; Donut a 24 px ring (sizes.box.donut), others dimmed to .16 as a legend item is active (Legend onActiveChange(key | null)), point labels xs, reference lines orange (their label at at) |
| Dates | Calendar, the panels and DayStrip period from the value or today; DateInput 'D MMM YYYY' and revert true, DateTimeSelect 'YYYY-MM-DD HH:mm', DateSelect ranges ' → ', Calendar and DayStrip md; Calendar views month, year, decade, monthFormat 'MMM YYYY', yearFormat 'YYYY', decadeFormat 'YYYY' (each end of 'YYYY…YYYY'), monthCellFormat 'MMM' and yearCellFormat 'YYYY'; DayStrip 7 days, monthFormat 'MMM' (on a month's 1st) and weekdayFormat 'EEEEE'; week numbers by each row's Thursday (ISO), weekends in quiet, TimeSlots 4 columns; TimePicker presets in groups { label, times } |
| Divider, Container | Divider size xs (1 px); Container side padding md |
| MaskInput | '_' for an empty slot |
| RangeSlider, AngleSlider | RangeSlider push true; AngleSlider Home at 0 and End at the maximum |
Components
What exists is listed in test/exports.lock.json; open work is in
TODO.md.
- Text is inline: children that hold Text, Link, Code or
Mark elements are one Label of styled runs (ren-core's
runs), which wraps across them; a Link's run presses as the Link does. Highlight, Mark, each code line of Snippet, InlineSnippet and the Editor's drawn blocks are Labels of runs too; the block being edited shows its marks as its Edit's runs (code a tint, sub and superscript plain there), in a multi-line Edit that wraps and grows as the drawn block does (one row at the least), so a block keeps its height and rows as it is edited; Enter splits a block, Up and Down leave a block from its first or last drawn row (its Edit'scaretRow(); where a worker's Handle cannot tell, a one-row block at once and a longer one from its first or last place). - Focus: FocusTrap,
$trap, Modal, Drawer and a trapped Popover move the focus to their first focusable (ren-core's$focus) unless given one; Launcher's shortcut is heard anywhere in the root ($keys).
Meaning
Components say what they mean with ren-core's Meaning props (WAI-ARIA
roles and states, following the Authoring Practices patterns), so
ren-json's $json gives a view's data by meaning, not by its visual tree;
the DOM gains the same accessibility, and the canvases paint nothing of
it. Decoration (icons, chevrons, markers, separators, backdrops, drawn
axis text) is aria-hidden; layout (Stack, Row, Grid, Surface, Card, …)
has no role and dissolves. A caller's role, aria-* and microdata
(itemscope, itemtype, itemprop, itemref, id, content,
datetime, href) pass on to the
layout components' View, and so do data-* props (the DOM
writes them, ren-json drops them) and tabIndex (ren-core's place in the
focus: a ScrollBox at -1 is a skip link's target, focused by its ref's
focus()).
- Typography: Heading
heading(aria-levelits level), Textparagraph(not withinlineorinherit), Linklinkwithhref, Code and Snippetcode, Quoteblockquote, Listlistoflistitems, Tabletableof boxlessrows (display: 'contents') ofcolumnheader,rowheaderandcell(ren-json: a record per row keyed by the column headers), its captioncaption; Mark, Highlight, Key, Badge stay text (Badge's as written: it shows upper case bytextTransform). - Data display: Avatar
imgnamed bylabelorname, Image the portable Image'simg, Accordion triggersaria-expandedoverregionpanels, DataList alistoftermanddefinition, Timeline alist, DigitRoll named by its value, Excerpt's controlaria-expanded. - Inputs: a field is named by its label (
aria-label; the label shown is then decoration), witharia-required,aria-invalidand its value (a password's never); NumberInputspinbutton; Checkbox and Chipcheckbox, Switchswitch, Radio and SegmentedControlradioin aradiogroup, eacharia-checked(SegmentedControl's segments Actions, one Tab stop: the checked one attabIndex0, the others -1, the arrow keys, Home and End move it); Slider, RangeSlider, AngleSlider, Rating and the color sliderssliderwith value,minandmax; the groups and FieldGroupgroupnamed by their label or legend; Select, Autocomplete, MultiSelect, TagInput and Cascadercombobox(aria-expanded) over alistboxofoptions (aria-selected); TreeSelect acomboboxover atreeoftreeitems (aria-level,aria-selected, or withchecksaria-checkedtrue, false or mixed by the leaves under a row; parentsaria-expanded; the chevrons that open them are decoration, the arrow keys open and close). - Buttons: the Action's
button,aria-disabled,aria-busywhile loading; CloseButton named 'Close'. - Navigation: Tabs
tablist,tab(aria-selected),tabpanel, the list one Tab stop (every tab an Action, the active one attabIndex0, the others -1; the arrow keys, Home and End move it); NavItemlink(aria-current'page'; a parentaria-expanded); Breadcrumbs anavigationof alistoflistitems, the lastaria-current'page'; Pagination and Outlinenavigation(aria-current); Stepper alistoflistitems (the current onearia-current'step'), abuttonin each stage a press may move to (named by its label, or 'Step i of n'); NavTogglearia-expanded; Treetreeoftreeitems; Menumenuofmenuitem,menuitemcheckbox,menuitemradio; Menubarmenubar. - Feedback: Alert
alert, Notificationstatus(alertin red), the progress bars and ringsprogressbarwith their value (a ring's text label itsaria-valuetextonly, not its name too), Loader and Busy astatusnamed 'Loading' (said once over the backdrop), Skeletonaria-hiddenwhile it shows. - Overlays: Modal and Drawer a modal
dialognamed by the title (aheading), Dialog, Popover, HoverCard and Floaterdialog, Tooltiptooltip, ActionBartoolbar; backdropsaria-hidden. - Layout: Shell's Header
banner, Navnavigation, Mainmain, Sidecomplementary, Footercontentinfo; a bare Divideraria-hidden; Panes' handlesseparators named 'Resize pane N' with the pane's size and bounds (in the list's unit) andaria-orientation; Marquee's first copy is its content and the repeated copies areinertdecoration (no Tab stop, out of the accessibility tree and of ren-json's data). - Extensions: charts
imgnamed by a summary (their kind, series and points;aria-labelreplaces it) holding their data as microdata, a schema.orgDatasetitem:rows as given (their plain fields, colors left out) with theseries(name,label), a Sparkline'svalues, a Heatmap'srows ofdateandvalue, a ScatterChart'sserieswith theirpoints; the values sit in empty boxes out of sight (UNSEEN: nothing drawn on any renderer). Bars alistof items (microdatanameand rawvalue); Calendar agridofrows andgridcells (aria-selected, todayaria-current; each day's ISO date itsdatetime, a month'sYYYY-MM, a year'sYYYY, as DayStrip's days and TimeSlots' times; ren-json: a record per week keyed by the weekdays, each day{ datetime, label, selected, text }), the picker inputs buttons named by their label (aria-expanded), TimePickerspinbuttons; Carousel aregionof slidegroups; Dropzonebutton; Launcher acomboboxover alistbox; LoadBar aprogressbar; the Editor's content a multi-linetextboxof its blocks' roles; DataTable atableofrows (aria-sorton sortable headers). - Goodies: JsonTree a
treeoftreeitems named by their paths (aria-level,aria-expanded, open children agroup), Onboarding's card adialognamed by the step's title (aheading), AnalogClock animgnamed by its time ('10:09:30'), QrCode animgnamed 'QR code: ' and its value, Gantt agrid(arowper task: its name therowheader, its bar agridcellnamed by its days, progress and dependencies), Led animg'On' or 'Off', Spinner astatus, Rings agroupofprogressbars, Compare's handle aslider, TextAnimate named by its text (its segments decoration), Window adialog, Flip.Trigger abutton(named by its content), pressed by pointer or keyboard, so its content holds no controls; given a function child(props) => controlit puts no button of its own and hands the caller's controlonPress,aria-pressed('true' while the back shows) and itslabel(oraria-label) aslabel; Book's pages turn by pointer only (no buttons, no keys), so the keyboard turns them throughvalueandonChange, as Buttons beside the book that set the page.
Hooks and form
rui-core/hooks holds the hooks as rrx-core operations ($open,
$counter, $list, $debounced, $shortcuts, $localStorage,
…); rui-core/form holds $form and its validators (required,
email, pattern, range, length, same).
- Each takes an optional root first (rrx-core's two spellings); the short spelling uses the root being built, so call it while a view is built, not in an event handler.
- Each returns one handle (Conventions):
$open→{ value, open, close, toggle },$toggle→{ value, next, set },$counter→{ value, add, set, reset },$list→{ value, push, unshift, insert, pop, shift, removeAt, move, swap, replace, setField, filter, map, mapWhere, set },$history({ limit })→{ value, entries, index, set, back, forward, reset }(100 kept by default),$selection→{ value, has, add, delete, toggle, all, some, set, clear },$validated(initial, { check, valid })→{ value, valid, good, reason, set }(reason: the textcheckgave for a failure),$patch→{ value, set }(a patch: its own keys over the current ones, a key given as undefined removed, no write when nothing changes),$model→{ value, set, controlled },$interval→{ value (running), start, stop, toggle },$timeout→{ value (pending), start, stop },$clipboard→{ value (copied), error, copy, paste, reset }(copythrough the Clipboard API, else the browser's copy command, so it copies outside a secure context too;paste()the clipboard's text, null where the platform refuses or has none),$fullscreen→{ value (shown), ref, toggle },$localStorage({ key, fallback, print, parse })→{ value, set, remove }(JSON by default), and$fits,$scheme,$online,$os→{ value }.$debouncereturns the debounced function withcancel,flushandpending. - Values are rrx-core nodes; they clean up with their owner (timers, listeners).
- An input hook hands out props for a primitive
(
$hover→{ value, props },$move(onChange, { onMoveStart, onMoveEnd, dir })→{ value (held), props },$idle→{ value, reset, props },$spy→{ value (the active heading's index, 0 first), measured, headings, scroll, heading, scrollTo, refresh },$eyedropper→{ value (the last color picked), available, pick },$outside→{ inside, outside },$shortcuts→ anonKeyhandler, or with{ global: true }the root's every key (ren-core's$keys; a key typed in a field, the event'seditable, is left alone unlessfields: true); shortcuts are an object map,{ 'primary+S': save }: one chord each, parts joined by+(case and spaces ignored), modifiersctrl(control),alt(option,opt),shift,meta(cmd,command,win,super) andprimary(Cmd on macOS and iOS, Ctrl elsewhere), and one key as the browser names it orspace,plus,esc,del(the key typed first; its position,code, only when no chord for the key is bound, as on a layout without that letter); a malformed chord throws; a match prevents the default unlessallowDefault;$trap→{ props: { trap, ref }, start, active }), so they work on every renderer.$outsidesees presses inside the element holdingoutsideonly. $fits,$scheme,$reducedMotionand$viewportSize, called in a component, read the application root's viewport (ren-core's$mediaand$viewport: the window on the DOM, the canvas on Skia and three.js, the main thread's in a worker); elsewhere the platform'smatchMediaand window.- Platform hooks (network, title, fullscreen, clipboard, media queries outside a view) keep inert defaults where the platform lacks them (node, a worker).
$localStorageuses the application root'sStoragewhere there is one.$form(R, { values, rules, validateOn, transform, keyed }): values in a$store, paths'a.b.0.c',rulesby field or a schema function, validation on change or blur (validateOn: { change, blur }),validate() → { ok, errors }andcheck(path) → { ok, error },valid(),touched()anddirty(),get,setandmerge, one verb each for errors and marks (setError,clear,touch,untouch,markDirty,settle),watch(path, (value, previous)), list helpers (insert,remove,move(path, from, to),replace) that carry errors along,bind(path, { as })for every input, andsubmit(handler, onInvalid)withtransform(busywhile it runs). Keyed (keyed: true), the inputs keep their own state:bindgivesdefaultValue, andform.keyOf(path), read in a region around the input, re-mounts it when the form sets the field.validateOnlists paths and path patterns, segment arrays whereEACH(a Symbol) stands for any list index (['items', EACH, 'name']). The validators return a short message when no error is named ('Fill this in', 'Not an email address', 'Wrong format', 'Does not match', and the bounds ofrangeandlength);requiredletsfalsethrough and stopsNaN;lengthcounts characters as seen, untrimmed unless{ trim: true }.
Portability
- Components are portable primitives, styled by the portable
styleand the style props. - Font weights reach the DOM as given (500 and 600 too); the canvases draw the nearest face they have (100 to 500 regular, 600 to 900 bold).
- A press handler is
onPress, on every renderer. - Hooks are rrx-core operations (
$open, …).
Tests
npm test: the declarations, tsc, the TypeScript consumer of every
entry (test/types/), then the suites under the runtime JSX and the
compiled templates. test/harness.jsx renders a view on ren-dom
(happy-dom) and on ren-skia (headless CanvasKit) for parity checks; its
DOM window's ResizeObserver reports the boxes a test gives (report()), as
happy-dom lays nothing out. test/meaning.test.jsx reads
representative components through ren-json's $json (a devDependency)
and checks their roles, names, states and values.
