0.0.1-3 • Published 7 months ago

osyka v0.0.1-3

Weekly downloads
-
License
MIT
Repository
-
Last release
7 months ago

Osyka

Simple and opinionated SolidJS UI library based on Material Design 3 and a set of enhancements for Solid itself, built for Osyah.

Install

pnpm add osyka

Osyka is published as a set of .jsx and .styl files, so you will need a bundler configured to compile SolidJS JSX and Stylus. You should also ensure that your minifier doesn't mess the names of classes for Osyka to produce correct DOM.

Reactivity

Channels

SolidJS loves read/write segregation, but we don't. Adopting the shared read/write function approach reduces the verbosity of all code, makes it possible to use reactive primitives as object properties and improves code composability. We call a function that can be used for both reading and writing a reactive state a channel. Osyka provides a few channel wrappers around common SolidJS reactive primitives:

  • Signal, SignalPersist: signal value is read by calling the returned function without arguments, written by calling with one argument (setting the value through an update function is unsupported)
  • Resource: resource actions (refetch/mutate) are simply made resource's properties
  • Store, StorePersist: state is accessed by calling the store with zero arguments, updated by calling with one or more arguments (forwarded to store's SetStoreFunction)

Contexts

Like read/write segregation, Context API in SolidJS was inherited from React. Though the idea of contexts is practical, provider-based API is not, so in Osyka we suggest a channel-styled API that uses the same context system under the hood:

const MyContext = Context(0)

MyContext() // 0
MyContext(1) // 1
MyContext() // 1

function App() {
	MyContext(2) // 2

	return <a-few children={
		<wrappers children={
			<to-imitate children={
				<depth children={
					<Consumer />
				} />
			} />
		} />
	} />
}

function Consumer() {
	const myContextValue = MyContext() // 2

	return <>{/* ... */}</>
}

View

Osyka View is a micro-framework for building customizable UI components as well as application pages. It allows describing components such that their consumers can flexibly override all aspects of behavior and appearance with no effort needed from component's author. Since this model powers Osyka's Material components, you should grasp it to efficiently utilize them.

Internally, every View consists of three parts. The first one is called the model - it is a class whose properties are the data that the View displays, all of its appearance options, internal states, behaviour-related functions like event listeners and mixin implementations, which we'll cover later.

The second part is elements description. It specifies semantic names and types of all DOM elements and embedded components created by a View. We'll describe the purpose and benefits of such named elements in the next paragraphs.

The third section where you put everything above together is called compose. It is like a component's render function where you instantiate and configure elements you defined in the elements section using the data and display options you declared in the model section.

With this knowledge, we are ready to implement a simple application as a View:

const App = OsykaView(
	class App {
		value = Signal(0.5)
		hot() {
			return this.value() > 0.5
		}
	},
	{
		root: 'div',
		range: 'input',
		caption: 'span',
	},
	x => <x.root>
		<x.range
			type='range'
			value={ x.value() }
			onChange={ e => x.value(e.target.valueAsNumber) }
		/>
		<x.caption class={x.hot() ? 'hot' : 'cold'}>
			{ x.hot() ? 'Hot!' : 'Cold!' }
		</x.caption>
	</x.root>
)

All elements created by x.<e> components automatically have readable classes for easy styling. Let's see the DOM it produced:

<div class='OsykaView App'>
	<input class="OsykaView App_range" type="range" value="0.5" />
	<span class="OsykaView App_caption cold">
		Cold!
	</span>
</div>

From model and element descriptions, Osyka View also generates lots of props for effortless customization of components:

  • {modelMember}: overrides any member of model
  • {element}_{elementProp}: overrides any prop of an element
  • {element}_replace: replaces element completely
const value = Signal(0)

<App
	value={value} // can override any model field
	root_style_background='var(--color)'//  can override any prop of any element
	root_style_--color='red' // can set styles and CSS variables easily
	range_replace={ Original => <Original /> } // can wrap or exclude any elements from rendering
/>

View: Using mixins

Mixins are functions for reusing element-related functionality. In Osyka, such functionality includes attaching shadows, interaction effects and disabled state overlays to components. Mixins can work with elements they are applied to imperatively as well as add props to them declaratively.

To use a mixin, you should firstly register it by simply adding a property to the view's model and initializing it with mixin's implementation. To attach a mixin to an element, add mix_{localMixinName} prop to it. Most mixins accept configuration objects. To set properties of these object's, use mix_{mixin}_{configField} props.

Example of usage of Osyka's built-in OsykaShadow mixin:

import {OsykaView, OsykaShadow} from 'osyka'

const App = OsykaView(
	class App {
		shadow = OsykaShadow
	},
	{
		root: 'div',
		box: 'div',
	},
	x => <x.root>
		<x.box
			mix_shadow
			mix_shadow_resting={2}
		/>
	</x.root>
)

This will add to box a shadow corresponding to Material Design's elevation level 2.

View: Creating mixins

A mixin is a function that follows the (config: Record<string, any>) => OsykaViewMix. OsykaViewMix is a dictionary of all props a mixin can set, which includes ref, style_ props and DOM attributes. But there are two important things to remember:

  • for adding event handlers, instead of onEvent or onevent props you should use on:event
  • for adding children from mixins, you should use special childrenPrepend and childrenAppend props that won't replace the children set by element author

Components

OsykaButton

Button types

Represents Material Design Common buttons.

By default, this component renders a button element, but it is also possible to render anchor (a) buttons by setting href prop. Depending on whether a button is a link button, different props are available.

Elements:

  • root: either a or button depending on whether href is set

Props:

  • type: 'elevated' | 'filled' | 'tonal' | 'outlined' | 'text', defaults to filled
  • label: a string describing the action that will occur if a user taps the button
  • icon: an icon to visually communicate the button's action and help draw attention
  • onClick: an alias to root_onClick
  • href: makes a button an anchor and sets root_href
  • target: available only in link buttons, an alias to root_target
  • disabled: not available in link buttons, sets mix_disable_disabled

CSS variables:

  • --OsykaButton_shape: roundness of buttons, defaults to completely round

Mixins:

  • disable: OsykaDisable, applied if rendering a button element
  • interactive: OsykaInteractive
  • shadow: OsykaShadow, applied if type prop is elevated

Mixins

OsykaInteractive

Adds hover and click effects to an element.

CSS variables:

  • --OsykaInteractive_color: state layer color, defaults to onSurface
  • --OsykaInteractive_opacityHover: opacity of state layer when hovered, defaults to 0.08
  • --OsykaInteractive_opacityActive: opacity of ripple, defaults to 0.12

OsykaShadow

Applies shadows to an element.

Config:

  • resting: default elevation level of the element, valid values are integers in range 0..3
  • hover: elevation level of the element in hover state, valid values are integers in range 0..5
  • active: elevation level of the element in active state, valid values are integers in range 0..5

OsykaDisable

Represents the disabled state of Material Design components.

This mixin does not provide any styling, it just adds OsykaDisable class to an element it is applied to and renders an overlay that blocks interactions.

Web API utilities

OsykaLight

OsykaLight is a reactive accessor for user's light/dark mode preference. To allow selecting the color scheme from UI, use OsykaLightOverride signal that is synchronized with the eponymous local storage entry. If OsykaLightOverride is null, the value is derived from browser's preference.

Tip: there is a built-in component for selecting the color scheme — OsykaLightSelect.

OsykaContrast

OsykaContrast is a reactive accessor for user's contrast preference. Possible returned values are:

  • -1: user prefers less contrast
  • 0: no explicit contrast preference
  • 1: user prefers more contrast

Material Design

Customization

The look of your UI can be tuned through --Osyka* CSS variables that correspond to a subset of Material Design tokens.

  • Font: - --OsykaFontPlain - --OsykaFontBrand
  • Shape: - --OsykaShape_xs - --OsykaShape_sm - --OsykaShape_md - --OsykaShape_lg - --OsykaShape_xl
  • Scheme

All variables are initialized except for Scheme ones to avoid bloating the bundle with unused default colors.

Scheme

A scheme is a set of semantic color variables derived from:

  • selected scheme variant
  • selected source color
  • user's light/dark theme preference
  • user's contrast preference

Scheme generation in Osyka is handled by osyka_scheme module which uses @material/material-color-utilities under the hood. There are several ways to setup a scheme:

Scheme: Baseline

Baseline scheme is Osyka's sensible default scheme that adjusts to user's contrast and dark mode preferences. To use it, just import the stylesheet:

import 'osyka/dist/osyka_scheme_baseline.css'

Technically speaking, baseline scheme is an adaptive scheme with blue (#45b1e8) source color generated with Tonal Spot variant.

Scheme: Adaptive

Adaptive scheme stylesheets adjust to user's dark mode and contrast preferences.

You can generate an adaptive scheme from your brand color and a scheme variant through the CLI:

pnpm osyka_scheme_gen <variant> <hex> <path_prefix>

This will write to the following files: {path_prefix}.css, {path_prefix}_contrast_more.css, {path_prefix}_contrast_less.css.

To start using the generated scheme, import it from your application's entry point.

By default, adaptive scheme appears dark or light based on the browser preference. If your application has an in-UI control for toggling the dark mode (see OsykaLight), set osyka_scheme_adaptive attribute of application's root element to dark/light:

function App() {
	return <div class='App'
		attr:osyka_scheme_adaptive={OsykaLight() ? 'light' : 'dark'}
	>
		<OsykaLightSelect />
	</div.App>
}

Scheme: Manual

A "smart" adaptive scheme is composed from a set of "dumb" color sets. To generate one, use OsykaSchemeColors API, which returns a CSS string with list of color variables and accepts the same config object as OsykaSchemeAdaptive but also two extra parameters for specifying light/dark appearance and contrast level:

const css = OsykaSchemeColors(
	{variant, sourceColor},
	light, // boolean
	contrast, // -1 | 0 | 1
)

Second and third parameters are optional and default to OsykaLight and OsykaContrast calls correspondingly, which are reactive accessors for browser preferences.

A great use case for OsykaSchemeColors is dynamic color schemes based on app content or user-generated colors.

import {OsykaSchemeColors} from 'osyka'
import {SchemeContent} from '@material/material-color-utilities'

function AlbumPage( props: {
	title: string
	coverColor: string
} ) {
	const css = () => OsykaSchemeColors( {
		variant: SchemeContent,
		sourceColor: props.coverColor,
	} )

	return <div class='AlbumPage' style={ css() } >
		<h1 class='AlbumPage_title'>{props.title}</h1>
	</div>
}

Scheme: Variant

A variant is responsible for turning a source color to a set of tonal palettes with its color magic. To use a variant when generating a scheme programmatically, import one of the classes listed below from @material/material-color-utilities.

Tip: the variant most developers will want is SchemeTonalSpot.

  • SchemeContent: uses the source color as background; has similiar appearance in light and dark mode; tertiary palette is analogous to the primary one
  • SchemeExpressive: a scheme that is intentionally detached from the source color
  • SchemeFidelity: uses the source color as background; similiar appearance in light and dark mode; tertiary palette is complementary to the primary one
  • SchemeFruitSalad: a playful theme — the source color's hue does not appear in the theme
  • SchemeMonochrome: a grayscale scheme
  • SchemeNeutral: a nearly grayscale scheme
  • SchemeRainbow: a playful theme — the source color's hue does not appear in the theme
  • SchemeTonalSpot: has low to medium colorfullness; the tertiary palette is related to the source color
  • SchemeVibrant: maxes out colorfullness of the primary tonal palette

OsykaElevation

Represents Material Design's elevation level.

A type that matches integers from 0 to 5. Similiarly, resting elevation levels (0 to 3) are represented by OsykaElevationResting.