@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
137 lines (103 loc) • 5.02 kB
Markdown
Shades is a UI library for FuryStack that uses type-safe JSX components, unidirectional data binding, and the same DI/IoC, logging, and utility libraries as FuryStack backend services.
```bash
npm install @furystack/shades
yarn add @furystack/shades
```
You can check the [@furystack/boilerplate](https://github.com/furystack/boilerplate) repository for a working example.
A shade (component) can be constructed from the following properties:
- `render:(options: RenderOptions)=>JSX.Element` – A required method that will be executed on each render. Use `useDisposable` within render for one-time setup that needs cleanup.
- `customElementName` – The custom element tag name. Must follow Custom Elements naming convention (lowercase, must contain a hyphen).
- `style` – Optional inline styles applied to each component instance. Use for per-instance overrides.
- `css` – Optional CSS styles injected as a stylesheet during component registration. Supports pseudo-selectors and nested selectors.
### Styling
Shades provides two complementary approaches to styling components:
#### `style` Property (Inline Styles)
The `style` property applies inline styles to each component instance. Use this for:
- Per-instance style overrides
- Dynamic styles that change based on props/state
- Quick prototyping
```typescript
const MyComponent = Shade({
customElementName: 'my-component',
style: {
display: 'flex',
padding: '16px',
},
render: () => <div>Content</div>,
})
// Override styles on specific instances
<MyComponent style={{ marginTop: '20px' }} />
```
The `css` property injects CSS rules into a stylesheet once per component type. Use this for:
- Component-level default styles
- Pseudo-selectors (`:hover`, `:active`, `:focus`, `:disabled`, etc.)
- Nested selectors (child elements, class names)
- Better performance (styles injected once, not per-instance)
```typescript
const Button = Shade({
customElementName: 'my-button',
css: {
padding: '12px 24px',
backgroundColor: 'blue',
color: 'white',
border: 'none',
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: 'darkblue',
},
'&:active': {
transform: 'scale(0.98)',
},
'&:disabled': {
opacity: '0.5',
cursor: 'not-allowed',
},
'& .icon': {
marginRight: '8px',
},
},
render: ({ props }) => (
<button disabled={props.disabled}>
{props.icon && <span className="icon">{props.icon}</span>}
{props.children}
</button>
),
})
```
| Use Case | `style` | `css` |
| ------------------------- | ------- | ----- |
| Hover/focus/active states | ❌ | ✅ |
| Per-instance overrides | ✅ | ❌ |
| Nested element styling | ❌ | ✅ |
| Dynamic values from props | ✅ | ❌ |
| Component defaults | ⚠️ | ✅ |
Both properties can be used together. Inline `style` will override `css` due to CSS specificity rules.
The `render` function receives a `RenderOptions` object with these hooks:
- `props` – The current readonly props object. Passed from the parent, treat as immutable.
- `injector` – The injector instance, inherited from the closest parent or set explicitly.
- `children` – The children element(s) of the component.
- `renderCount` – How many times this component has rendered.
- `useState(key, initialValue)` – Local state that triggers re-renders on change.
- `useObservable(key, observable, options?)` – Subscribes to an `ObservableValue`; re-renders on change by default. Provide a custom `onChange` to skip re-renders.
- `useSearchState(key, initialValue)` – State synced with URL search parameters.
- `useStoredState(key, initialValue, storageArea?)` – State persisted to `localStorage` or `sessionStorage`.
- `useDisposable(key, factory)` – Creates a resource that is automatically disposed when the component unmounts.
- `useHostProps(hostProps)` – Declaratively sets attributes, styles, and CSS variables on the host element. Prefer this over direct DOM manipulation.
- `useRef(key)` – Creates a ref object for imperative access to child DOM elements.
The **@furystack/shades** package contains a router component, a router-link component, a location service, and a lazy-load component.
- Shade is close to the DOM and the natives. You are encouraged to use native browser methods when possible.
- You can use small independent services for state tracking with the injector.
- You can use resources (value observers) that will be disposed automatically when your component is removed from the DOM.
- Re-rendering can be skipped on state update. For example, why re-render a whole form if only one field has changed?
- Nothing is true, everything is permitted. 🗡️