osyka v0.0.1-3
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 propertiesStore
,StorePersist
: state is accessed by calling the store with zero arguments, updated by calling with one or more arguments (forwarded to store'sSetStoreFunction
)
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
oronevent
props you should useon:event
- for adding
children
from mixins, you should use specialchildrenPrepend
andchildrenAppend
props that won't replace the children set by element author
Components
OsykaButton
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
: eithera
orbutton
depending on whetherhref
is set
Props:
type
:'elevated' | 'filled' | 'tonal' | 'outlined' | 'text'
, defaults tofilled
label
: a string describing the action that will occur if a user taps the buttonicon
: an icon to visually communicate the button's action and help draw attentiononClick
: an alias toroot_onClick
href
: makes a button an anchor and setsroot_href
target
: available only in link buttons, an alias toroot_target
disabled
: not available in link buttons, setsmix_disable_disabled
CSS variables:
--OsykaButton_shape
: roundness of buttons, defaults to completely round
Mixins:
disable
:OsykaDisable
, applied if rendering abutton
elementinteractive
:OsykaInteractive
shadow
:OsykaShadow
, applied iftype
prop iselevated
Mixins
OsykaInteractive
Adds hover and click effects to an element.
CSS variables:
--OsykaInteractive_color
: state layer color, defaults toonSurface
--OsykaInteractive_opacityHover
: opacity of state layer when hovered, defaults to0.08
--OsykaInteractive_opacityActive
: opacity of ripple, defaults to0.12
OsykaShadow
Applies shadows to an element.
Config:
resting
: default elevation level of the element, valid values are integers in range0..3
hover
: elevation level of the element in hover state, valid values are integers in range0..5
active
: elevation level of the element in active state, valid values are integers in range0..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 contrast0
: no explicit contrast preference1
: 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 oneSchemeExpressive
: a scheme that is intentionally detached from the source colorSchemeFidelity
: uses the source color as background; similiar appearance in light and dark mode; tertiary palette is complementary to the primary oneSchemeFruitSalad
: a playful theme — the source color's hue does not appear in the themeSchemeMonochrome
: a grayscale schemeSchemeNeutral
: a nearly grayscale schemeSchemeRainbow
: a playful theme — the source color's hue does not appear in the themeSchemeTonalSpot
: has low to medium colorfullness; the tertiary palette is related to the source colorSchemeVibrant
: 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
.