├── .gitignore
├── LICENSE
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── src
├── Box
│ ├── Box.css.ts
│ └── Box.tsx
├── Breakout
│ ├── Breakout.css.ts
│ └── Breakout.tsx
├── Clamp
│ ├── Clamp.css.ts
│ └── Clamp.tsx
├── Columns
│ ├── Columns.css.ts
│ └── Columns.tsx
├── DebugProvider
│ ├── DebugProvider.css.ts
│ └── DebugProvider.tsx
├── Grid
│ ├── Grid.css.ts
│ └── Grid.tsx
├── Row
│ ├── Row.css.ts
│ └── Row.tsx
├── Split
│ ├── Split.css.ts
│ └── Split.tsx
├── Stack
│ ├── Stack.css.ts
│ └── Stack.tsx
├── component-icons.png
├── favicon.svg
├── hooks
│ └── useAlign.ts
├── ideas.md
├── index.ts
├── logo.svg
├── main.tsx
├── useRatio.ts
├── utils
│ └── toSelectorString.ts
└── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Christian Kaindl
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
LYTS
3 |
4 |
Layout primitives for React.
5 |
6 |
7 | # LYTS
8 |
9 | Layout primitives to build any kind of layout with useful props like `bleed`, `asChild` and `xAlign`/`yAlign`.
10 |
11 | - **Like Lego** – Compose primitives to create complex layouts
12 | - **Tiny bundle** – Only 445 Bytes total ([source](https://bundlephobia.com/package/@christiankaindl/lyts@2.0.0-beta.3))
13 | - **Unstyled** – Bring your own styling solution—Tailwind, CSS Modules, you name it
14 | - **Layout props** – Simple & productive API
15 |
16 | ⚛️ [Components API](https://lyts.christiankaindl.com/components) · 📚 [Guides](https://lyts.christiankaindl.com/guides) · 📖 [Examples](https://lyts.christiankaindl.com/examples)
17 |
18 | To get started, import a base component and compose them together—Stack, Row, Clamp, Columns, Grid—happy layout building!
19 |
20 | > [!NOTE]
21 | > Version 2.0 introduced support for React 19, and migrated away from using `forwardRef()`. This means v2.0 may not work as intended when using with React 18 or earlier. If you want to pass `ref`s to LYTS components and use React 18 or lower, consider using v1.2.0 instead, which has full support.
22 |
23 | ## Usage
24 |
25 | Layout components can be composed until you achieve your desired layout. For example, The following \ component renders a card with a max-width of 400px, centers it and uses a Stack to get consistent spacing:
26 |
27 | ![image]()
28 |
29 | ```jsx
30 | const CenterCard: FunctionComponent = function () {
31 | return (
32 | // A card with clamped 400px and centered
33 |
34 |
35 |
Card title
36 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
37 |
38 |
39 | )
40 | }
41 | ```
42 |
43 | Check out the Examples page for a comprehensive collection of layouts and how to build them with LYTS.
44 |
45 | A real-world example using LYTS is this documentation site, which makes extensive use of all components. [Check out the code here](https://github.com/christiankaindl/LYTS-website/)!
46 |
47 | ## Support & help
48 |
49 | If you get stuck, [reach out to @christiankaindl](https://twitter.com/christiankaindl) on Twitter. In case of bugs, [open an issue on GitHub](https://github.com/christiankaindl/LYTS/issues).
50 |
51 | ## Local Development
52 |
53 | ```sh
54 | npm install
55 | npm run dev
56 | ```
57 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@christiankaindl/lyts",
3 | "description": "Layout primitives for React",
4 | "version": "2.0.1",
5 | "author": "Christian Kaindl",
6 | "license": "MIT",
7 | "homepage": "https://lyts.christiankaindl.com",
8 | "bugs": "https://github.com/christiankaindl/LYTS/issues",
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/christiankaindl/LYTS.git"
12 | },
13 | "scripts": {
14 | "dev": "vite",
15 | "build": "tsc && vite build",
16 | "preview": "vite preview"
17 | },
18 | "prepublishOnly": "npm run build",
19 | "files": [
20 | "dist"
21 | ],
22 | "type": "module",
23 | "main": "./dist/lyts.js",
24 | "types": "./dist/index.d.ts",
25 | "exports": {
26 | "./style.css": "./dist/style.css",
27 | ".": {
28 | "types": "./dist/index.d.ts",
29 | "default": "./dist/lyts.js"
30 | }
31 | },
32 | "sideEffects": [
33 | "**/*.css"
34 | ],
35 | "dependencies": {
36 | "@radix-ui/react-slot": "^1.1.1",
37 | "@vanilla-extract/dynamic": "^2.1.2"
38 | },
39 | "peerDependencies": {
40 | "react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
41 | "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
42 | },
43 | "devDependencies": {
44 | "@radix-ui/colors": "^0.1.8",
45 | "@types/react": "^19.0.1",
46 | "@types/react-dom": "^19.0.2",
47 | "@vanilla-extract/css": "^1.16.1",
48 | "@vanilla-extract/css-utils": "^0.1.4",
49 | "@vanilla-extract/sprinkles": "^1.6.3",
50 | "@vanilla-extract/vite-plugin": "^4.0.18",
51 | "@vitejs/plugin-react": "^4.3.4",
52 | "react": "^19.0.0",
53 | "react-dom": "^19.0.0",
54 | "typescript": "^5.7.2",
55 | "vite": "5.4.11",
56 | "vite-plugin-dts": "^4.3.0",
57 | "vite-plugin-lib-inject-css": "^2.1.1",
58 | "vite-tsconfig-paths": "^5.1.4"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Box/Box.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, style } from "@vanilla-extract/css";
2 | import { calc } from '@vanilla-extract/css-utils'
3 |
4 | export const vars = {
5 | gap: createVar('gap'),
6 | alignItems: createVar('align-items'),
7 | justifyContent: createVar('justify-content'),
8 | flexDirection: createVar('flex-direction'),
9 | bleedTop: createVar('bleed-top'),
10 | bleedRight: createVar('bleed-right'),
11 | bleedBottom: createVar('bleed-bottom'),
12 | bleedLeft: createVar('bleed-left'),
13 | }
14 |
15 | export const box = style({
16 | display: 'flex',
17 | // Set default values so that values don't propagate beyond the element where props are applied, making it possible to reduce the amount of inline CSS variables
18 | vars: {
19 | [vars.gap]: '1rem',
20 | [vars.alignItems]: 'initial',
21 | [vars.justifyContent]: 'initial',
22 | [vars.flexDirection]: 'column',
23 | [vars.bleedTop]: '0px',
24 | [vars.bleedRight]: '0px',
25 | [vars.bleedBottom]: '0px',
26 | [vars.bleedLeft]: '0px'
27 | },
28 | gap: vars.gap,
29 | alignItems: vars.alignItems,
30 | justifyContent: vars.justifyContent,
31 | flexDirection: vars.flexDirection,
32 | // Make sure horizontal alignment is applied to Grid elements (like Clamp and Grid)
33 | // `justifyItems` does nothing in flexbox contexts
34 | justifyItems: vars.justifyContent,
35 | marginTop: calc(vars.bleedTop).negate().toString(),
36 | marginRight: calc(vars.bleedRight).negate().toString(),
37 | marginBottom: calc(vars.bleedBottom).negate().toString(),
38 | marginLeft: calc(vars.bleedLeft).negate().toString(),
39 | })
40 |
--------------------------------------------------------------------------------
/src/Box/Box.tsx:
--------------------------------------------------------------------------------
1 | import { useAlign } from "@lib/hooks/useAlign";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { assignInlineVars } from "@vanilla-extract/dynamic";
4 | import { CSSProperties, type Ref } from "react"
5 | import * as styles from './Box.css'
6 |
7 | // TODO: Add `inline` prop?
8 | export interface BoxProps extends React.HTMLAttributes {
9 | ref?: Ref
10 | /**
11 | * Amount of space between elements. Same as CSS' `gap`.
12 | * @docs https://developer.mozilla.org/en-US/docs/Web/CSS/gap
13 | */
14 | gap?: CSSProperties['gap']
15 | /**
16 | * Forward props onto its immediate child (must be the only child)
17 | *
18 | * When `true`, the first child is used as the container. Useful for writing semantic HTML with correct element behavior (keyboard interactions, screen-readers).
19 | * Inspired by Radix UI's API.
20 | *
21 | * @docs https://www.radix-ui.com/docs/primitives/utilities/slot
22 | */
23 | asChild?: boolean
24 | /** Alias for the CSS `flex-direction` property */
25 | orientation?: 'row' | 'column'
26 | /** Visually break out of the parent's box. Useful for visually aligning e.g. transparent buttons */
27 | bleed?: string | number
28 | /** Top `bleed` value @see bleed */
29 | bleedTop?: CSSProperties['padding']
30 | /** Right `bleed` value @see bleed */
31 | bleedRight?: CSSProperties['padding']
32 | /** Bottom `bleed` value @see bleed */
33 | bleedBottom?: CSSProperties['padding']
34 | /** Left `bleed` value @see bleed */
35 | bleedLeft?: CSSProperties['padding']
36 | /**
37 | * Horizontal alignment.
38 | *
39 | * Same as CSS' `align-items` property when `orientation='row'`.
40 | * Same as CSS' `justify-content` property when `orientation='column'`.
41 | */
42 | xAlign?: Direction extends 'row'
43 | ? CSSProperties['justifyContent']
44 | : CSSProperties['alignItems']
45 | /**
46 | * Vertical alignment.
47 | *
48 | * Same as CSS' `justify-content` property when `orientation='row'`.
49 | * Same as CSS' `align-items` property when `orientation='column'`.
50 | */
51 | yAlign?: Direction extends 'row'
52 | ? CSSProperties['alignItems']
53 | : CSSProperties['justifyContent']
54 | }
55 |
56 | export const boxStyles = styles ?? {}
57 |
58 | /**
59 | * Generic flexbox context with convenience props such as `xAlign`/`yAlign`for general alignment, `bleed` for visual alignment and `asChild` to customize the rendered element.
60 | */
61 | export function Box ({
62 | children,
63 | gap,
64 | asChild = false,
65 | bleed,
66 | bleedTop,
67 | bleedRight,
68 | bleedBottom,
69 | bleedLeft,
70 | style = {},
71 | orientation = 'column',
72 | xAlign = 'initial',
73 | yAlign = 'initial',
74 | ...props
75 | }: BoxProps) {
76 | const Comp = asChild ? Slot : 'div';
77 | const align = useAlign(orientation, xAlign, yAlign)
78 |
79 | if (typeof bleed === 'number') {
80 | bleed = `${bleed}px`
81 | }
82 | const _bleed = bleed ? bleed.split(' ') : []
83 | bleedTop = bleedTop ?? _bleed?.[0]
84 | bleedRight = bleedRight ?? _bleed?.[1] ?? _bleed?.[0]
85 | bleedBottom = bleedBottom ?? _bleed?.[2] ?? _bleed?.[0]
86 | bleedLeft = bleedLeft ?? _bleed?.[3] ?? _bleed?.[1] ?? _bleed?.[0]
87 |
88 | // Only set Create an object that only contains properties which are set
89 | const inlineVars = Object.assign({},
90 | gap !== undefined && { [styles.vars.gap]: typeof gap === 'number' ? `${gap}rem` : gap },
91 | bleedTop && { [styles.vars.bleedTop]: toCssValue(bleedTop) },
92 | bleedRight && { [styles.vars.bleedRight]: toCssValue(bleedRight) },
93 | bleedBottom && { [styles.vars.bleedBottom]: toCssValue(bleedBottom) },
94 | bleedLeft && { [styles.vars.bleedLeft]: toCssValue(bleedLeft) },
95 | align.justifyContent !== 'initial' && { [styles.vars.justifyContent]: align.justifyContent },
96 | align.alignItems !== 'initial' && { [styles.vars.alignItems]: align.alignItems },
97 | align.flexDirection !== 'column' && { [styles.vars.flexDirection]: align.flexDirection }
98 | )
99 |
100 | return (
101 |
109 | {children}
110 |
111 | )
112 | }
113 |
114 | export default Box
115 |
116 | function toCssValue (value: string | number) {
117 | return (typeof value === 'number' || value === '0') ? `${value}px` : value
118 | }
119 |
--------------------------------------------------------------------------------
/src/Breakout/Breakout.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from "@vanilla-extract/css";
2 |
3 | export const breakout = style({
4 | gridColumn: '1 / -1'
5 | })
6 |
--------------------------------------------------------------------------------
/src/Breakout/Breakout.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps } from "@lib/Box/Box";
2 | import * as styles from './Breakout.css'
3 |
4 | export const breakoutStyles = styles ?? {}
5 |
6 | /**
7 | * Spans the full row in a CSS Grid context. Use within a `` or `` to make elements full-width.
8 | *
9 | * @see Clamp
10 | * @see Grid
11 | */
12 | export function Breakout ({ children, ...props }: BoxProps) {
13 | return (
14 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/src/Clamp/Clamp.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2 |
3 | export const maxWidth = createVar('max-width')
4 | export const maxHeight = createVar('max-height')
5 |
6 | // TODO: Make more generic, by mirroring the technique for horizontal clamping
7 | // So, set gridTemplateRows when vertical clamping is used. This would unify how alignment works across horizontal/vertical clamping
8 | // and also fix yAlign, which currenlty does nothing on Clamp.
9 |
10 | // But that would (I think) also mean that Clamp can't support the `gap` prop anymore, as it has to be 0 (or otherwise it grows too large)
11 | // A workaround for this could be to dynamically replace "100%" with a calc: "1fr min(${fallbackVar(maxWidth, '100%')}, calc(100% - ${gap}*2)) 1fr"
12 | export const clamp = style({
13 | display: 'grid',
14 | gridTemplateColumns: `1fr min(${fallbackVar(maxWidth, '100%')}, 100%) 1fr`,
15 | columnGap: 0, // Update when uni-directional clamping is implemented
16 | // Only applies when the container has an explicit height, or vertical clamping is used
17 | alignItems: 'center'
18 | })
19 |
20 | globalStyle(`${clamp} > *`, {
21 | gridColumn: 2,
22 | maxHeight,
23 | boxSizing: 'border-box'
24 | })
25 |
--------------------------------------------------------------------------------
/src/Clamp/Clamp.tsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react'
2 | import * as styles from './Clamp.css'
3 | import { assignInlineVars } from '@vanilla-extract/dynamic'
4 | import Box, { BoxProps } from '@lib/Box/Box'
5 |
6 | export interface ClampProps extends BoxProps {
7 | /**
8 | * The maximum width (or height) of its children.
9 | *
10 | * If an array with two values are passed, the second value is used as the maximum height. In that case, you can also set the first value to `null` to get only vertical centering.
11 | *
12 | * Note: When vertical clamping is used, only a single direct child is supported.
13 | */
14 | clamp: string | [maxWidth: string | null, maxHeight: string]
15 | }
16 |
17 | export const clampStyles = styles ?? {}
18 |
19 | /**
20 | * Center-constrained component, supporting both horizontal and vertical clamping. Individual children can "opt out" of the clamping with the `` component.
21 | *
22 | * @see https://www.joshwcomeau.com/css/full-bleed/ for a detailed explanation of how Clamp and Breakout work internally.
23 | */
24 | export function Clamp ({
25 | children,
26 | clamp,
27 | style = {},
28 | ...props
29 | }: ClampProps) {
30 | const isArray = Array.isArray(clamp)
31 | const clampWidth = isArray ? clamp[0] : clamp
32 | const clampHeight = isArray ? clamp[1] : undefined
33 |
34 | if (clampHeight !== undefined && Children.count(children) > 1) {
35 | throw Error(` with a vertical clamp set must be used with a single child only. Current children: ${Children.count(children)}`)
36 | }
37 |
38 | return (
39 |
51 | {children}
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/src/Columns/Columns.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2 |
3 | export const flexGrow = createVar('flex-grow')
4 | export const collapseAt = createVar('collapse-at')
5 |
6 | export const columns = style({
7 | flexWrap: 'wrap'
8 | })
9 |
10 | globalStyle(`${columns} > *`, {
11 | flexGrow: fallbackVar(flexGrow, '1'),
12 | // Holy Albatross technique: https://heydonworks.com/article/the-flexbox-holy-albatross-reincarnated/
13 | flexBasis: `calc((${collapseAt} - 100%) * 999)`,
14 | width: 0,
15 | boxSizing: 'border-box'
16 | })
17 |
--------------------------------------------------------------------------------
/src/Columns/Columns.tsx:
--------------------------------------------------------------------------------
1 | import { Children, cloneElement, isValidElement, type CSSProperties, type ReactElement } from 'react'
2 | import * as styles from './Columns.css'
3 | import Box, { BoxProps } from '@lib/Box/Box'
4 | import { useRatio } from '@lib/useRatio'
5 | import { assignInlineVars } from '@vanilla-extract/dynamic'
6 |
7 | export interface ColumnsProps extends BoxProps {
8 | /**
9 | * Control the proportional distribution of the columns. By default, all columns are distributed equally.
10 | *
11 | * Needs to be set to a string in the format `${number}[.../${number}]` each value separated with a slash "/" is used as a proportion for the `flex-grow` value.
12 | * For example, a `ratio` value of "1/1.5/1" will grow the middle child 1.5 times more than the other two elements (assuming there are 3 children).
13 | *
14 | * Note: When using the `ratio` prop, make sure that all direct children forward the `style` prop to the underlying DOM element.
15 | */
16 | ratio?: string
17 | /**
18 | * Collapse children into a vertical layout at this width.
19 | *
20 | * Note that `collapseAt` is based on the total element width, and **not** the window width.
21 | */
22 | collapseAt?: string
23 | }
24 |
25 | export const columnsStyles = styles ?? {}
26 |
27 | /**
28 | * Extrinsicly sized columns, filling the whole available space and wrapping all-at-once when the `collapseAt` value is reached. Space distribution can be customized with the `ratio` prop.
29 | */
30 | export function Columns ({
31 | children,
32 | ratio,
33 | collapseAt = '0',
34 | ...props
35 | }: ColumnsProps) {
36 | const ratios = useRatio(ratio)
37 | return (
38 |
49 | {ratios === undefined
50 | ? children
51 | : Children.map(children, (child, index) => {
52 | if (!child) return null
53 | if (!isValidElement(child)) return child
54 | return (
55 | cloneElement(child as ReactElement<{ style: CSSProperties }>, {
56 | style: {
57 | ...(child as ReactElement<{ style: CSSProperties }>).props.style,
58 | ...assignInlineVars({
59 | [styles.flexGrow]: typeof ratios === 'string'
60 | ? ratios
61 | : (ratios?.[index] || '1')
62 | })
63 | }
64 | })
65 | )
66 | })}
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/src/DebugProvider/DebugProvider.css.ts:
--------------------------------------------------------------------------------
1 | // import { boxStyles, stackStyles, rowStyles, clampStyles, columnsStyles, gridStyles } from "@christiankaindl/lyts"
2 |
3 | import { blueA, crimsonA, mauveA, plumA, redA, tealA, tomatoA, violetA } from "@radix-ui/colors";
4 | import { globalStyle, style } from "@vanilla-extract/css";
5 | import { boxStyles } from "../Box/Box";
6 | import { stackStyles } from "../Stack/Stack";
7 | import { rowStyles } from "../Row/Row";
8 | import { clampStyles } from "../Clamp/Clamp";
9 | import { columnsStyles } from "../Columns/Columns";
10 | import { gridStyles } from "../Grid/Grid";
11 |
12 | globalStyle('html, body', {
13 | margin: 0,
14 | fontFamily: 'sans-serif'
15 | })
16 |
17 | export const debug = style({
18 | display: 'contents'
19 | })
20 | globalStyle(`${debug} ${toClassName(boxStyles.box)}`, {
21 | outline: `2px solid ${mauveA.mauveA6}`,
22 | padding: 18,
23 | borderRadius: 15,
24 | backgroundColor: mauveA.mauveA2
25 | })
26 | globalStyle(`${debug} ${toClassName(boxStyles.box)} > ${toClassName(boxStyles.box)}`, {
27 | borderRadius: 9
28 | })
29 |
30 | globalStyle(`${debug} ${toClassName(stackStyles.stack)}`, {
31 | // outline: '2px solid #f9c6c6', // Red 6
32 | outline: `2px solid ${redA.redA6}`,
33 | backgroundImage: `linear-gradient(160deg, ${tomatoA.tomatoA1}, ${crimsonA.crimsonA1})`
34 | })
35 |
36 | globalStyle(`${debug} ${toClassName(rowStyles.row)}`, {
37 | outline: '2px solid #f3c6e2',
38 | backgroundImage: `linear-gradient(160deg, ${crimsonA.crimsonA1}, ${plumA.plumA1})`
39 | })
40 |
41 | globalStyle(`${debug} ${toClassName(clampStyles.clamp)}`, {
42 | outline: '2px solid #e3ccf4',
43 | backgroundImage: `linear-gradient(160deg, ${plumA.plumA1}, ${violetA.violetA1})`
44 | })
45 |
46 | globalStyle(`${debug} ${toClassName(columnsStyles.columns)}`, {
47 | outline: '2px solid #c6d4f9',
48 | backgroundImage: `linear-gradient(160deg, ${violetA.violetA1}, ${blueA.blueA1})`
49 | })
50 |
51 | globalStyle(`${debug} ${toClassName(gridStyles.grid)}`, {
52 | outline: '2px solid #aadee6',
53 | backgroundImage: `linear-gradient(160deg, ${blueA.blueA1}, ${tealA.tealA1})`
54 | })
55 |
56 | export const nav = style([debug, {
57 | borderRadius: 24,
58 | justifyContent: 'center',
59 | backgroundColor: 'rgb(255, 255, 255, 0.25)',
60 | // backdropFilter: 'blur(10px)',
61 | boxShadow: '0 5px 24px -7px rgb(0 0 0 / 0.1)',
62 | padding: 18
63 | }])
64 |
65 | // Convert class name string as used in HTML's `class` attribute (separated by a space " ") to the CSS dot-notation so that they can be composed with vanilla extract more easily
66 | function toClassName (htmlClassNames: string) {
67 | return `.${htmlClassNames.split(' ').join('.')}`
68 | }
69 |
--------------------------------------------------------------------------------
/src/DebugProvider/DebugProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent, PropsWithChildren } from "react";
2 | import { debug } from './DebugProvider.css'
3 |
4 | /**
5 | * Render all children LYTS components with extra visual cues
6 | * @private
7 | */
8 | const DebugProvider: FunctionComponent = function DebugProvider ({ children }) {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
16 | export default DebugProvider
17 |
--------------------------------------------------------------------------------
/src/Grid/Grid.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, style } from '@vanilla-extract/css'
2 |
3 | export const gridItemMinWidth = createVar('gridItemMinWidth')
4 | export const gridMaxRowItems = createVar('gridMaxRowItems')
5 |
6 | export const grid = style({
7 | display: 'grid',
8 | gridTemplateColumns: `repeat(auto-fit, minmax(min(${gridItemMinWidth}, 100%), 1fr))`
9 | })
10 |
--------------------------------------------------------------------------------
/src/Grid/Grid.tsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react'
2 | import * as styles from './Grid.css'
3 | import { assignInlineVars } from '@vanilla-extract/dynamic'
4 | import Box, { BoxProps } from '@lib/Box/Box'
5 |
6 | export interface GridProps extends BoxProps {
7 | gridItemMinWidth?: string
8 | /** TODO: Different columns API? See https://css-tricks.com/responsive-layouts-fewer-media-queries/ */
9 | gridMaxRowItems?: number
10 | }
11 |
12 | export const gridStyles = styles ?? {}
13 |
14 | /**
15 | * Grid layout with with responsive defaults, but also fully customizable with standard CSS grid properties.
16 | */
17 | export function Grid ({
18 | children,
19 | gridItemMinWidth = '300px',
20 | gridMaxRowItems,
21 | style = {},
22 | ...props
23 | }: GridProps) {
24 | return (
25 |
37 | {children}
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/Row/Row.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2 |
3 | export const flexGrow = createVar('flex-grow')
4 | export const wrap = createVar('wrap')
5 |
6 | export const row = style({
7 | flexWrap: wrap,
8 | })
9 |
10 | globalStyle(`${row} > *`, {
11 | flexGrow: fallbackVar(flexGrow, '0')
12 | })
13 |
--------------------------------------------------------------------------------
/src/Row/Row.tsx:
--------------------------------------------------------------------------------
1 | import { Children, cloneElement, CSSProperties, isValidElement, type ReactElement } from 'react'
2 | import * as styles from './Row.css'
3 | import { assignInlineVars } from '@vanilla-extract/dynamic'
4 | import Box, { BoxProps } from '@lib/Box/Box'
5 | import { useRatio } from '@lib/useRatio'
6 |
7 | export interface RowProps extends BoxProps {
8 | /**
9 | * Same as CSS' `flex-wrap` property.
10 | * @docs https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap
11 | */
12 | wrap?: boolean | CSSProperties['flexWrap']
13 | /**
14 | * Grow the Row's children to fill the container's width.
15 | *
16 | * If a string is supplied in the format `${number}[.../${number}]` each value separated with a slash "/" is used as a proportion for the `flex-grow` value.
17 | * For example, an `expandChildren` value of "1/1.5/1" will grow the middle child 1.5 times more than the other two elements (assuming there are 3 children).
18 | *
19 | * @see `ratio` in Columns props
20 | */
21 | expandChildren?: boolean | string
22 | }
23 |
24 | export const rowStyles = styles ?? {}
25 |
26 | /**
27 | * Horizontally stacked components, with convenience `wrap` and `expand` props. By default, all children are vertically centered and horizontally start-aligned.
28 | */
29 | export function Row ({
30 | children,
31 | wrap = 'nowrap',
32 | expandChildren = false,
33 | yAlign = 'center',
34 | style = {},
35 | ...props
36 | }: RowProps) {
37 | if (wrap === true) {
38 | wrap = 'wrap'
39 | } else if (wrap === false) {
40 | wrap = 'nowrap'
41 | }
42 | const ratios = useRatio(expandChildren)
43 | return (
44 |
56 | {ratios === undefined
57 | ? children
58 | // TODO: Only need to map the children like this if expandChildren is a string.
59 | : Children.map(children, (child, index) => {
60 | if (!child) return null
61 | if (!isValidElement(child)) return child
62 | return (
63 | cloneElement(child as ReactElement<{ style: CSSProperties }>, {
64 | style: {
65 | ...(child as ReactElement<{ style: CSSProperties }>).props.style,
66 | ...assignInlineVars({
67 | [styles.flexGrow]: typeof ratios === 'string'
68 | ? ratios
69 | : (ratios?.[index] || '0')
70 | })
71 | }
72 | })
73 | )
74 | })
75 | }
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/Split/Split.css.ts:
--------------------------------------------------------------------------------
1 | import { style } from '@vanilla-extract/css'
2 |
3 | export const split = style({
4 | flexGrow: 1
5 | })
6 |
--------------------------------------------------------------------------------
/src/Split/Split.tsx:
--------------------------------------------------------------------------------
1 | import * as styles from './Split.css'
2 | import { BoxProps } from '@lib/Box/Box'
3 |
4 | export interface SplitProps extends BoxProps {
5 | children?: undefined
6 | }
7 |
8 | export const splitStyles = styles ?? {}
9 |
10 | /**
11 | * Helper to separate items along a single axis. Split takes no children and composes with ``, `` or any CSS flexbox context.
12 | */
13 | export function Split ({
14 | style = {},
15 | ...props
16 | }) {
17 | return (
18 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/Stack/Stack.css.ts:
--------------------------------------------------------------------------------
1 | import { createVar, fallbackVar, globalStyle, style } from '@vanilla-extract/css'
2 |
3 | export const flexGrow = createVar('flex-grow')
4 |
5 | export const stack = style({})
6 |
7 | globalStyle(`${stack} > *`, {
8 | flexGrow: fallbackVar(flexGrow, '0')
9 | })
10 |
--------------------------------------------------------------------------------
/src/Stack/Stack.tsx:
--------------------------------------------------------------------------------
1 | import { Children, cloneElement, isValidElement, type CSSProperties, type ReactElement } from 'react'
2 | import * as styles from './Stack.css'
3 | import Box, { BoxProps } from '@lib/Box/Box'
4 | import { useRatio } from '@lib/useRatio'
5 | import { assignInlineVars } from '@vanilla-extract/dynamic'
6 |
7 | export interface StackProps extends BoxProps<'column'> {
8 | /**
9 | * Grow the Stack's children to fill the container's height.
10 | *
11 | * If a string is supplied in the format `${number}[.../${number}]` each value separated with a slash "/" is used as a proportion for the `flex-grow` value.
12 | * For example, an `expandChildren` value of "1/1.5/1" will grow the middle child 1.5 times more than the other two elements (assuming there are 3 children).
13 | *
14 | * @see `ratio` Columns
15 | */
16 | expandChildren?: boolean | string
17 | }
18 |
19 | export const stackStyles = styles ?? {}
20 |
21 | /**
22 | * Vertically stacked elements, taking up the full width by default. Best nested within other Stacks.
23 | */
24 | export function Stack ({
25 | children,
26 | expandChildren = false,
27 | ...props
28 | }: StackProps) {
29 | const ratios = useRatio(expandChildren)
30 | return (
31 |
36 | {ratios === undefined
37 | ? children
38 | // TODO: Only need to map the children like this is expandChildren is a string.
39 | : Children.map(children, (child, index) => {
40 | if (!child) return null
41 | if (!isValidElement(child)) return child
42 | return (
43 | cloneElement(child as ReactElement<{ style: CSSProperties }>, {
44 | style: {
45 | ...(child as ReactElement<{ style: CSSProperties }>).props.style,
46 | ...assignInlineVars({
47 | [styles.flexGrow]: typeof ratios === 'string'
48 | ? ratios
49 | : (ratios?.[index] || '0')
50 | })
51 | }
52 | })
53 | )
54 | })}
55 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/src/component-icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/christiankaindl/LYTS/270d8e2c4df5776e9eb68b7d87826c20f542f067/src/component-icons.png
--------------------------------------------------------------------------------
/src/favicon.svg:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/hooks/useAlign.ts:
--------------------------------------------------------------------------------
1 | export function useAlign (orientation: 'row' | 'column', xAlign: string, yAlign: string) {
2 | return {
3 | flexDirection: orientation,
4 | alignItems: orientation === 'row' ? yAlign : xAlign,
5 | justifyContent: orientation === 'row' ? xAlign : yAlign
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/ideas.md:
--------------------------------------------------------------------------------
1 | - Add new `clamp` prop to Stack, Row and Columns out-of-the-box. If yes, then that would automatically wrap them in Clamp
2 | - Columns "auto" keyword?
3 | - Columns "/"-syntax confusing?
4 | - Columns "style"-forwarding example would be good
5 | - Columns xAlign doesn't do anything? (only has effect on mobile)
6 | - Merge Columns and Row into one?
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export const lytsId = Symbol('Identifier for LYTS React components')
2 |
3 | export * from './Box/Box'
4 | export * from './Stack/Stack'
5 | export * from './Row/Row'
6 | export * from './Clamp/Clamp'
7 | export * from './Columns/Columns'
8 | export * from './Grid/Grid'
9 | export * from './Split/Split'
10 | export * from './Breakout/Breakout'
11 |
12 | export { toSelectorString } from './utils/toSelectorString'
13 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import React, { FunctionComponent, useLayoutEffect, useRef, useState, type RefObject } from 'react'
3 | import { Stack, Row, Clamp, Columns, Grid, Box, Breakout, Split } from './'
4 | import DebugProvider from './DebugProvider/DebugProvider'
5 |
6 | function ForwardRefTest () {
7 | const ref = useRef(null)
8 | const [height, setHeight] = useState()
9 |
10 | useLayoutEffect(() => {
11 | setHeight(ref.current?.clientHeight)
12 | })
13 |
14 | return (
15 |