= {
54 | (args: A): C
55 | $: Observable
56 | }
57 |
58 | type TDummyAction = {
59 | (...args: any[]): void
60 | $: Observable
61 | }
62 |
63 | export interface IActionDust {
64 | action: TDummyAction
65 | key: string
66 | name: string
67 | byHuman: boolean
68 | namespace: string
69 | payload: any
70 | }
71 |
72 | /* const d = ca()
73 | d()
74 | d.$
75 |
76 | const dd = ca()
77 | dd('h')
78 | dd.$
79 |
80 | const dd1 = ca(null)
81 | dd1.$
82 | const h1 = dd1('dd')
83 | h1()
84 |
85 | const dd2 = ca void>((R, smth) => (x) => R(x + Number(smth)))
86 | dd2.$
87 | const h2 = dd2('df')
88 | h2(3)
89 |
90 | const tt = ga('tool', {
91 | d,
92 | dd,
93 | dd2,
94 | }) */
95 |
--------------------------------------------------------------------------------
/src/_generic/supply/get-color.ts:
--------------------------------------------------------------------------------
1 | export const COLORS = [
2 | '#f44336',
3 | '#e91e63',
4 | '#9c27b0',
5 | '#673ab7',
6 | '#3f51b5',
7 | '#436eb1',
8 | '#03a9f4',
9 | '#00d4cc',
10 | '#009688',
11 | '#4caf50',
12 | '#8bc34a',
13 | '#cddc39',
14 | '#ffeb3b',
15 | '#ffc107',
16 | '#ff9800',
17 | '#ff5722',
18 | '#795548',
19 | '#607d8b',
20 | ]
21 |
22 | let lastIndex = 0
23 |
24 | export const getColor = () => COLORS[lastIndex++ % COLORS.length]
25 |
--------------------------------------------------------------------------------
/src/_generic/supply/utils.ts:
--------------------------------------------------------------------------------
1 | import { Observable } from 'rxjs'
2 | import { tap } from 'rxjs/operators'
3 |
4 | export const NTA = (v: string | null) => v || 'auto'
5 |
6 | export const debug = (...args: any[]) => (stream: Observable) => {
7 | if (process.env.NODE_ENV === 'production') {
8 | return stream
9 | } else {
10 | return stream.pipe(
11 | // tslint:disable-next-line
12 | tap(console.log.bind(console, ...args))
13 | )
14 | }
15 | }
16 |
17 | export const isShallowEqual = (a: T1, b: T2): boolean => {
18 | if (!a || !b) {
19 | return !a && !b
20 | }
21 |
22 | const aKeys = Object.keys(a)
23 | const bKeys = Object.keys(b)
24 | const aLen = aKeys.length
25 | const bLen = bKeys.length
26 |
27 | if (aLen === bLen) {
28 | for (let i = 0; i < aLen; ++i) {
29 | const key = aKeys[i]
30 | if (a[key] !== b[key]) {
31 | return false
32 | }
33 | }
34 | return true
35 | }
36 |
37 | return false
38 | }
39 |
--------------------------------------------------------------------------------
/src/_generic/types/common.ts:
--------------------------------------------------------------------------------
1 | import { Atom, ReadOnlyAtom } from '@grammarly/focal'
2 |
3 | export interface IGridSettings {
4 | isInline: boolean
5 | width: string | null
6 | height: string | null
7 | colGap: string | null
8 | rowGap: string | null
9 | justifyItems: string | null
10 | alignItems: string | null
11 | justifyContent: string | null
12 | alignContent: string | null
13 | autoFlow: string | null
14 | isGrow: boolean
15 | }
16 |
17 | export const defaultGridSettings: IGridSettings = {
18 | isInline: false,
19 | width: '10em',
20 | height: '10em',
21 | colGap: '1em',
22 | rowGap: '1em',
23 | justifyItems: null,
24 | alignItems: null,
25 | justifyContent: null,
26 | alignContent: null,
27 | autoFlow: null,
28 | isGrow: true,
29 | }
30 |
31 | export interface IGrid {
32 | cols: ITrack[]
33 | rows: ITrack[]
34 | }
35 |
36 | export interface ITrack {
37 | id: string
38 | value: string | null
39 | min: string | null
40 | max: string | null
41 | minmax: boolean
42 | fitContent: boolean
43 | repeat: string | number
44 | }
45 |
46 | export const defaultTrack: ITrack = {
47 | id: 'def-track',
48 | value: null,
49 | min: '1px',
50 | max: '1fr',
51 | minmax: false,
52 | fitContent: false,
53 | repeat: 0,
54 | }
55 |
56 | export interface IItem {
57 | id: string
58 | name: string
59 | characters: string
60 | color: string
61 | width: string | null
62 | height: string | null
63 | colStart: string | number
64 | rowStart: string | number
65 | colEnd: string | number
66 | rowEnd: string | number
67 | justifySelf: string | null
68 | alignSelf: string | null
69 | isHidden: boolean
70 | }
71 |
72 | export const defaultItem: IItem = {
73 | id: 'def-item',
74 | name: 'def-item',
75 | characters: '',
76 | color: 'red',
77 | width: null,
78 | height: null,
79 | colStart: 1,
80 | rowStart: 1,
81 | colEnd: 2,
82 | rowEnd: 2,
83 | justifySelf: null,
84 | alignSelf: null,
85 | isHidden: false,
86 | }
87 |
88 | export interface ITrackOverlay {
89 | track$: Atom
90 | position$: ReadOnlyAtom<[number, number]> // Start, End
91 | row: boolean
92 | repeat: boolean
93 | rect: ClientRect | DOMRect
94 | }
95 |
--------------------------------------------------------------------------------
/src/_generic/ui/Btn/index.tsx:
--------------------------------------------------------------------------------
1 | import { lift } from '@grammarly/focal'
2 | import cc from 'classcat'
3 | import * as React from 'react'
4 | import { Ico } from '../Ico'
5 | import { icons } from '../Ico/icons'
6 | import $ from './style.scss'
7 |
8 | type TProps = {
9 | label?: string
10 | ico?: keyof typeof icons
11 | icoFill?: string
12 | icoAfterLabel?: true
13 | transparent?: true
14 | narrow?: true
15 | special?: true
16 | disabled?: boolean
17 | }
18 |
19 | export const Btn = ({
20 | label,
21 | ico,
22 | icoFill,
23 | icoAfterLabel,
24 | transparent,
25 | narrow,
26 | special,
27 | ...props
28 | }: TProps & React.HTMLProps) => {
29 | const className = cc([
30 | $.btn,
31 | !narrow && $.wide,
32 | special && $.special,
33 | !transparent && $.normal,
34 | icoAfterLabel && $.icoAfterLabel,
35 | ])
36 | return (
37 |
45 | )
46 | }
47 |
48 | export const BtnLifted = lift(Btn)
49 |
--------------------------------------------------------------------------------
/src/_generic/ui/Btn/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .btn {
4 | @extend %tappable;
5 | display: flex;
6 | align-items: center;
7 | padding: 0.4em 0.6em;
8 | font-size: 1em;
9 | line-height: 1.1em;
10 | background: transparent;
11 | color: inherit;
12 | border: 0;
13 | box-sizing: border-box;
14 | min-height: 3em;
15 | transition-property: background, color;
16 | transition-duration: 120ms;
17 |
18 | .ico {
19 | flex-shrink: 0;
20 | display: flex;
21 | justify-content: center;
22 | &:last-child {
23 | flex-grow: 1;
24 | }
25 | }
26 |
27 | &.icoAfterLabel {
28 | flex-direction: row-reverse;
29 | .ico {
30 | font-size: 0.7em;
31 | margin-left: 1em;
32 | }
33 | }
34 |
35 | &:disabled {
36 | opacity: 0.3;
37 | > * {
38 | pointer-events: none;
39 | }
40 | }
41 |
42 | &:active {
43 | padding: 0.5em 0.6em 0.3em;
44 | }
45 | }
46 |
47 | .label {
48 | flex-grow: 1;
49 | font-weight: 700;
50 | }
51 |
52 | .normal {
53 | color: #fff;
54 | background: #4555d0;
55 | border: solid 1px #2b3bb6;
56 | border-radius: 0.3em;
57 | box-shadow: inset 0px 1px 0px 0px #90ace0;
58 | &:hover {
59 | background: #5c6ad6;
60 | }
61 | &:active {
62 | box-shadow: inset 0px 2px 6px 0px #33456d;
63 | }
64 | &.special {
65 | color: #7a7192;
66 | background: #413952;
67 | border-color: #141027;
68 | box-shadow: inset 0px 1px 0px 0px #76678a;
69 | &:hover {
70 | background: #524867;
71 | color: #9c8dc7;
72 | }
73 | }
74 | }
75 |
76 | .wide {
77 | flex-grow: 1;
78 | width: 100%;
79 | }
80 |
--------------------------------------------------------------------------------
/src/_generic/ui/Ico/icons.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:max-line-length
2 | export const icons = {
3 | visible:
4 | 'm20 15c2.7 0 5 2.3 5 5s-2.3 5-5 5-5-2.3-5-5 2.3-5 5-5z m0 13.4c4.6 0 8.4-3.8 8.4-8.4s-3.8-8.4-8.4-8.4-8.4 3.8-8.4 8.4 3.8 8.4 8.4 8.4z m0-20.9c8.4 0 15.5 5.2 18.4 12.5-2.9 7.3-10 12.5-18.4 12.5s-15.5-5.2-18.4-12.5c2.9-7.3 10-12.5 18.4-12.5z',
5 | invisible:
6 | 'm19.8 15h0.2c2.7 0 5 2.3 5 5v0.3z m-7.2 1.3c-0.6 1.1-1 2.4-1 3.7 0 4.6 3.8 8.4 8.4 8.4 1.3 0 2.6-0.4 3.7-1l-2.6-2.6c-0.3 0.1-0.7 0.2-1.1 0.2-2.7 0-5-2.3-5-5 0-0.4 0.1-0.8 0.2-1.1z m-9.2-9.2l2.1-2.1 29.5 29.5-2.1 2.1c-1.9-1.8-3.8-3.6-5.6-5.5-2.3 0.9-4.7 1.4-7.3 1.4-8.4 0-15.5-5.2-18.4-12.5 1.4-3.3 3.6-6.1 6.3-8.3-1.5-1.5-3-3.1-4.5-4.6z m16.6 4.5c-1.1 0-2.1 0.3-3 0.7l-3.6-3.6c2-0.8 4.3-1.2 6.6-1.2 8.4 0 15.4 5.2 18.3 12.5-1.3 3.1-3.2 5.8-5.7 7.9l-4.9-4.9c0.4-0.9 0.7-1.9 0.7-3 0-4.6-3.8-8.4-8.4-8.4z',
7 | close:
8 | 'm31.8 10.7l-9.3 9.3 9.3 9.3-2.4 2.3-9.3-9.3-9.3 9.3-2.3-2.3 9.3-9.3-9.3-9.3 2.3-2.3 9.3 9.3 9.3-9.3z',
9 | addAfterRow: 'M40 0H0V40H40V0ZM1 1H39V32H1V1ZM18 18V24H21V18H27V15H21V9H18V15H12V18H18Z',
10 | addAfterCol: 'M0 40V0H40V40H0ZM1 1V39H32V1H1ZM15 21V27H18V21H24V18H18V12H15V18H9V21H15Z',
11 | addBeforeRow: 'M40 40H0V0H40V40ZM1 39H39V8H1V39ZM18 22V16H21V22H27V25H21V31H18V25H12V22H18Z',
12 | addBeforeCol: 'M40 40V0H0V40H40ZM39 1V39H8V1H39ZM22 21V27H25V21H31V18H25V12H22V18H16V21H22Z',
13 | removeRow:
14 | 'M0 0V40H40V0H0ZM39 39H1V24H11L15 20L11 16H1V1H39V16H29L25 20L29 24H39V39ZM20 18L11 9L9 11L18 20L9 29L11 31L20 22L29 31L31 29L22 20L31 11L29 9L20 18Z',
15 | removeCol:
16 | 'M40 0L0 -1.74846e-06L-1.74846e-06 40L40 40L40 0ZM0.999998 39L1 0.999998L16 0.999999L16 11L20 15L24 11L24 0.999999L39 1L39 39L24 39L24 29L20 25L16 29L16 39L0.999998 39ZM22 20L31 11L29 9L20 18L11 9L9 11L18 20L9 29L11 31L20 22L29 31L31 29L22 20Z',
17 | copy:
18 | 'm31.6 35v-23.4h-18.2v23.4h18.2z m0-26.6c1.8 0 3.4 1.4 3.4 3.2v23.4c0 1.8-1.6 3.4-3.4 3.4h-18.2c-1.8 0-3.4-1.6-3.4-3.4v-23.4c0-1.8 1.6-3.2 3.4-3.2h18.2z m-5-6.8v3.4h-20v23.4h-3.2v-23.4c0-1.8 1.4-3.4 3.2-3.4h20z',
19 | remove:
20 | 'm15.9 30.7v-15.7q0-0.3-0.2-0.5t-0.5-0.2h-1.4q-0.3 0-0.5 0.2t-0.2 0.5v15.7q0 0.3 0.2 0.5t0.5 0.2h1.4q0.3 0 0.5-0.2t0.2-0.5z m5.7 0v-15.7q0-0.3-0.2-0.5t-0.5-0.2h-1.4q-0.3 0-0.5 0.2t-0.2 0.5v15.7q0 0.3 0.2 0.5t0.5 0.2h1.4q0.3 0 0.5-0.2t0.2-0.5z m5.8 0v-15.7q0-0.3-0.2-0.5t-0.6-0.2h-1.4q-0.3 0-0.5 0.2t-0.2 0.5v15.7q0 0.3 0.2 0.5t0.5 0.2h1.4q0.4 0 0.6-0.2t0.2-0.5z m-12.2-22.1h10l-1.1-2.6q-0.1-0.2-0.3-0.3h-7.1q-0.2 0.1-0.4 0.3z m20.7 0.7v1.4q0 0.3-0.2 0.5t-0.5 0.2h-2.1v21.2q0 1.8-1.1 3.2t-2.5 1.3h-18.6q-1.4 0-2.5-1.3t-1-3.1v-21.3h-2.2q-0.3 0-0.5-0.2t-0.2-0.5v-1.4q0-0.3 0.2-0.5t0.5-0.2h6.9l1.6-3.8q0.3-0.8 1.2-1.4t1.7-0.5h7.2q0.9 0 1.8 0.5t1.2 1.4l1.5 3.8h6.9q0.3 0 0.5 0.2t0.2 0.5z',
21 | question:
22 | 'm23 30.7v-4.3q0-0.3-0.2-0.5t-0.5-0.2h-4.3q-0.3 0-0.5 0.2t-0.2 0.5v4.3q0 0.3 0.2 0.5t0.5 0.2h4.3q0.3 0 0.5-0.2t0.2-0.5z m5.7-15q0-2-1.2-3.6t-3.1-2.6-3.8-0.9q-5.4 0-8.3 4.7-0.3 0.6 0.2 1l2.9 2.2q0.2 0.1 0.5 0.1 0.3 0 0.5-0.2 1.2-1.6 1.9-2.1 0.8-0.5 2-0.5 1 0 1.9 0.6t0.8 1.3q0 0.8-0.4 1.3t-1.6 1q-1.4 0.7-2.5 2t-1.2 2.8v0.8q0 0.3 0.2 0.5t0.5 0.2h4.3q0.3 0 0.5-0.2t0.2-0.5q0-0.5 0.5-1.1t1.2-1.1q0.7-0.4 1.1-0.7t1-0.8 1-1 0.6-1.4 0.3-1.8z m8.6 4.3q0 4.7-2.3 8.6t-6.3 6.2-8.6 2.3-8.6-2.3-6.2-6.2-2.3-8.6 2.3-8.6 6.2-6.2 8.6-2.3 8.6 2.3 6.3 6.2 2.3 8.6z',
23 | }
24 |
--------------------------------------------------------------------------------
/src/_generic/ui/Ico/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { icons } from './icons'
3 |
4 | type TProps = {
5 | ico: keyof typeof icons
6 | fill?: string
7 | }
8 |
9 | export class Ico extends React.PureComponent {
10 | render() {
11 | return (
12 |
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/_generic/ui/Logo.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | // tslint:disable:max-line-length
4 | export const Logo = ({ size = '1em' }: { size?: string }) => {
5 | return (
6 |
7 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/_generic/ui/MapElement.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Observable } from 'rxjs'
3 | import { LiftWrapper } from '@grammarly/focal/dist/src/react'
4 |
5 | export class MapElement extends React.PureComponent<{
6 | stream: Observable
7 | children?: (stream: T) => React.ReactNode
8 | }> {
9 | render() {
10 | return React.createElement(LiftWrapper, {
11 | component: ({ stream }: any) =>
12 | this.props.children ? (this.props.children as any)(stream) : stream,
13 | props: { stream: this.props.stream },
14 | })
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/_generic/ui/Overlay/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Popover, { Align, ArrowContainer, Position } from 'react-tiny-popover'
3 | import { Observable } from 'rxjs'
4 | import { Btn } from '../Btn'
5 | import { MapElement } from '../MapElement'
6 | import $ from './style.scss'
7 |
8 | type TProps = {
9 | isOpen$: Observable
10 | children: JSX.Element
11 | content: () => React.ReactNode
12 | align?: Align
13 | withArrow?: boolean
14 | position?: Position | Position[]
15 | stopPropagation?: boolean
16 | close?: () => void
17 | }
18 |
19 | export const Overlay = ({
20 | isOpen$,
21 | children,
22 | content,
23 | align = 'center',
24 | withArrow = true,
25 | position,
26 | stopPropagation = false,
27 | close,
28 | }: TProps) => {
29 | let onContainerClick: ((e: React.MouseEvent) => void) | undefined
30 | if (stopPropagation) {
31 | onContainerClick = (e) => {
32 | e.stopPropagation()
33 | }
34 | }
35 | return (
36 |
37 | {(isOpen) => (
38 | {
47 | const overlay = (
48 |
49 | {Boolean(close) && (
50 |
51 |
52 |
53 | )}
54 | {content()}
55 |
56 | )
57 | return withArrow ? (
58 |
66 | {overlay}
67 |
68 | ) : (
69 | overlay
70 | )
71 | }}
72 | >
73 | {children}
74 |
75 | )}
76 |
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/_generic/ui/Overlay/style.scss:
--------------------------------------------------------------------------------
1 | .overlay {
2 | position: relative;
3 | display: flex;
4 | flex-direction: column;
5 | padding: 0.1em 0.3em 0.3em;
6 | background: #4c4a56;
7 | border-radius: 0.2em;
8 | box-shadow: 0 0 0 1px #3c444a, 0 4px 8px rgba(16, 22, 26, 0.5),
9 | 0 18px 46px 6px rgba(16, 22, 26, 0.5);
10 | }
11 |
12 | .close {
13 | position: absolute;
14 | display: flex;
15 | right: -8px;
16 | top: -8px;
17 | font-size: 0.6em;
18 | color: #8ebec7;
19 | background: #4c4a56;
20 | border-radius: 50%;
21 | transition-duration: 120ms;
22 | transition-property: background;
23 | box-shadow: 0px 0.5em 1.6em 0 #3b3c4c;
24 | &:hover {
25 | background: #5e587b;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/_generic/ui/ReactiveList.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, Lens, ReadOnlyAtom } from '@grammarly/focal'
2 | import { LiftWrapper, reactiveList } from '@grammarly/focal/dist/src/react'
3 | import * as React from 'react'
4 |
5 | export class ReactiveList<
6 | T1,
7 | T2 extends { [key: string]: T1 } | T1[],
8 | T3 extends Atom | ReadOnlyAtom
9 | > extends React.PureComponent<{
10 | items: T3
11 | defaultItem: T1
12 | children: (
13 | item$: T3 extends Atom ? Atom : ReadOnlyAtom,
14 | key: T2 extends T1[] ? number : string
15 | ) => React.ReactNode
16 | }> {
17 | render() {
18 | const { items, defaultItem, children } = this.props
19 | return React.createElement(LiftWrapper, {
20 | component: ({ result }: any) => result,
21 | props: {
22 | result: reactiveList(
23 | items.map((x) =>
24 | Array.isArray(x) ? x.map((_, index) => index) : (Object.keys(x) as any)
25 | ),
26 | (key) => {
27 | let item$: any
28 | const lensDefault = Lens.withDefault(defaultItem)
29 | const lensItem =
30 | typeof key === 'string' ? Lens.key(key) : (Lens.index(key) as any)
31 | if ((items as any).set) {
32 | item$ = (items as Atom).lens(lensItem).lens(lensDefault)
33 | } else {
34 | item$ = items.view(lensItem).view(lensDefault)
35 | }
36 | return children(item$, key as any)
37 | }
38 | ),
39 | },
40 | })
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/_generic/ui/ShowIf.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { lift } from '@grammarly/focal'
3 |
4 | type TProps = {
5 | value: any
6 | eq: any
7 | children: (instShow: boolean) => React.ReactElement
8 | }
9 |
10 | export const ShowIf = lift(
11 | class extends React.Component {
12 | private didRendered = false
13 | render() {
14 | const { value, eq, children } = this.props
15 | const instShow = !this.didRendered
16 | if (instShow) {
17 | this.didRendered = true
18 | }
19 | return value === eq && children(instShow)
20 | }
21 | }
22 | )
23 |
--------------------------------------------------------------------------------
/src/_generic/ui/Video/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import $ from './style.scss'
3 |
4 | export const Video = ({ autoplay = false }: { autoplay?: boolean }) => {
5 | const auto = (autoplay ? 1 : 0).toString()
6 | return (
7 |
8 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/_generic/ui/Video/style.scss:
--------------------------------------------------------------------------------
1 | .video {
2 | position: relative;
3 | padding-bottom: 56.25%; /* 16:9 */
4 | height: 0;
5 | iframe {
6 | position: absolute;
7 | top: 0;
8 | left: 0;
9 | width: 100%;
10 | height: 100%;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/_shell/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { setConfig, hot } from 'react-hot-loader'
3 | import { actionsShell } from '../_generic/actions'
4 | import { Shell } from './Shell'
5 |
6 | setConfig({
7 | logLevel: 'debug',
8 | ignoreSFC: true, // RHL will be __completely__ disabled for SFC
9 | pureRender: true, // RHL will not change render method
10 | })
11 |
12 | export const App = hot(module)(
13 | class extends React.Component {
14 | componentDidMount() {
15 | actionsShell._mounted()
16 | }
17 | render() {
18 | return
19 | }
20 | }
21 | )
22 |
--------------------------------------------------------------------------------
/src/_shell/ButtonDego/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import $ from './style.scss'
4 |
5 | // tslint:disable:max-line-length
6 | export const ButtonDego: React.FC = () => {
7 | return (
8 |
13 |
14 | Flexbox
15 |
16 | Generator
17 |
18 | with UX like
19 |
20 | Figma/Sketch
21 |
22 |
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/_shell/ButtonDego/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .btn {
4 | @extend %tappable;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | width: 65px;
9 | margin-top: 2em;
10 | padding: 0.6em 0.4em;
11 | font-size: 8px;
12 | background: #fff;
13 | color: #b8233d !important;
14 | text-decoration: none;
15 | border: 1px solid;
16 | border-radius: 0.6em;
17 | transition-duration: 120ms;
18 | transition-property: transform;
19 | span {
20 | display: block;
21 | margin-bottom: 0.2em;
22 | font-size: 1.25em;
23 | text-align: center;
24 | }
25 | &:hover {
26 | text-decoration: none;
27 | transform: translateY(20px) translateX(20px) scale(1.5);
28 | }
29 | & b {
30 | font-weight: inherit;
31 | font-size: 1.5em;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/_shell/HowToUse/index.tsx:
--------------------------------------------------------------------------------
1 | import { Dialog } from '@blueprintjs/core'
2 | import * as React from 'react'
3 | import { actionsShell } from '../../_generic/actions'
4 | import { Ico } from '../../_generic/ui/Ico'
5 | import { Video } from '../../_generic/ui/Video'
6 | import $ from './style.scss'
7 |
8 | const modalProps = {
9 | autoFocus: true,
10 | canEscapeKeyClose: true,
11 | canOutsideClickClose: true,
12 | enforceFocus: true,
13 | usePortal: true,
14 | style: {
15 | padding: 0,
16 | width: '90%',
17 | minWidth: '600px',
18 | maxWidth: '1100px',
19 | },
20 | }
21 |
22 | export class HowToUse extends React.PureComponent<{}, { isOpen: boolean }> {
23 | state = {
24 | isOpen: false,
25 | }
26 |
27 | private handleOpen = () => {
28 | actionsShell.howToUse()
29 | this.setState({ isOpen: true })
30 | }
31 | private handleClose = () => this.setState({ isOpen: false })
32 |
33 | render() {
34 | return (
35 | <>
36 |
37 |
38 | How
39 |
40 | To Use
41 |
42 |
43 |
44 |
47 | >
48 | )
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/_shell/HowToUse/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .btn {
4 | @extend %tappable;
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | width: 5.2em;
9 | margin-top: 2em;
10 | padding: 0.6em 0.4em;
11 | font-size: 0.7em;
12 | background: linear-gradient(to bottom, #38322c 0%, #2d251d 100%);
13 | box-shadow: inset 0 0 0 1px rgba(179, 137, 70, 0.56);
14 | color: #b58a46;
15 | border-radius: 0.6em;
16 | transition-duration: 120ms;
17 | transition-property: box-shadow, letter-spacing, transform;
18 | span {
19 | display: block;
20 | margin-bottom: 0.2em;
21 | font-size: 1.25em;
22 | text-align: center;
23 | }
24 | &:hover {
25 | letter-spacing: 0.1em;
26 | transform: translateY(-0.2em);
27 | box-shadow: inset 0 0 0 1px rgb(179, 137, 70), 0 0 6em 0px rgba(179, 137, 70, 0.4);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/_shell/Shell/index.tsx:
--------------------------------------------------------------------------------
1 | import cc from 'classcat'
2 | import * as React from 'react'
3 | import { merge } from 'rxjs'
4 |
5 | import { Atom, classes, F } from '@grammarly/focal'
6 |
7 | import { actionsItems } from '../../_generic/actions'
8 | import { Logo } from '../../_generic/ui/Logo'
9 | import { ShowIf } from '../../_generic/ui/ShowIf'
10 | import { GetTheCode } from '../../get-the-code'
11 | import { Grid } from '../../grid'
12 | import { explicitGrid$, implicitGrid$ } from '../../grid/state'
13 | import { Items } from '../../items'
14 | import { GridSettings } from '../../kit-sections/GridSettings'
15 | import { ItemSettings } from '../../kit-sections/ItemSettings'
16 | import { Preview } from '../../preview'
17 | import { ButtonDego } from '../ButtonDego'
18 | import { HowToUse } from '../HowToUse'
19 | import $ from './style.scss'
20 |
21 | enum EGridPanel {
22 | Explicit,
23 | Implicit,
24 | }
25 |
26 | enum ESettingPanel {
27 | Item,
28 | Grid,
29 | }
30 |
31 | const state$ = Atom.create<{
32 | grid: EGridPanel
33 | settings: ESettingPanel
34 | }>({
35 | grid: EGridPanel.Explicit,
36 | settings: ESettingPanel.Grid,
37 | })
38 |
39 | const gridPanel$ = state$.lens('grid')
40 | const settingsPanel$ = state$.lens('settings')
41 |
42 | merge(actionsItems.add.$, actionsItems.select.$).subscribe(() => {
43 | settingsPanel$.set(ESettingPanel.Item)
44 | })
45 |
46 | // tslint:disable:max-line-length
47 | export const Shell: React.FC = () => {
48 | return (
49 |
50 |
74 |
75 |
Items
76 |
77 |
78 |
79 |
80 |
81 |
82 | settingsPanel$.set(ESettingPanel.Item)}
84 | {...classes(
85 | $.title,
86 | settingsPanel$.view((v) => v !== ESettingPanel.Item && $.tabBtn)
87 | )}
88 | >
89 | Item Settings
90 |
91 | settingsPanel$.set(ESettingPanel.Grid)}
93 | {...classes(
94 | $.title,
95 | settingsPanel$.view((v) => v !== ESettingPanel.Grid && $.tabBtn)
96 | )}
97 | >
98 | Container Settings
99 |
100 |
101 |
102 |
103 | {() => }
104 |
105 |
106 | {() => }
107 |
108 |
109 |
110 |
111 |
112 | gridPanel$.set(EGridPanel.Explicit)}
114 | {...classes(
115 | $.title,
116 | gridPanel$.view((v) => v !== EGridPanel.Explicit && $.tabBtn)
117 | )}
118 | >
119 | Grid Container
120 |
121 | gridPanel$.set(EGridPanel.Implicit)}
123 | {...classes(
124 | $.title,
125 | gridPanel$.view((v) => v !== EGridPanel.Implicit && $.tabBtn)
126 | )}
127 | >
128 | Auto Grid
129 |
130 |
131 |
132 |
133 | {() => }
134 |
135 |
136 | {() => }
137 |
138 |
139 |
140 |
141 |
Layout Preview
142 |
145 |
146 |
147 | )
148 | }
149 |
--------------------------------------------------------------------------------
/src/_shell/Shell/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .container {
4 | flex-grow: 1;
5 | display: grid;
6 | grid-template-columns: 4em 12em 20em minmax(0px, 1fr) minmax(0px, 1fr);
7 | grid-template-rows: minmax(0px, 1fr);
8 | grid-gap: 1em 1em;
9 | grid-column-gap: 1em;
10 | }
11 |
12 | .sideBar,
13 | .items,
14 | .settings,
15 | .grid,
16 | .layout {
17 | display: flex;
18 | flex-direction: column;
19 | }
20 |
21 | .sideBar {
22 | align-items: center;
23 | }
24 |
25 | .ver {
26 | font-size: 0.6em;
27 | margin-top: -0.5em;
28 | }
29 |
30 | .code {
31 | margin-top: 0.8em;
32 | flex-shrink: 0;
33 | font-size: 1.2em;
34 | }
35 |
36 | .ghLink {
37 | line-height: 0;
38 | margin-top: auto;
39 | }
40 |
41 | .gh {
42 | @extend %tappable;
43 | width: 2em;
44 | height: 2em;
45 | padding: 0.8em;
46 | fill: #4c4a56;
47 | transition-duration: 120ms;
48 | transition-property: fill;
49 | &:hover {
50 | fill: #ff9b70;
51 | }
52 | }
53 |
54 | .tabs {
55 | flex-shrink: 0;
56 | display: flex;
57 | }
58 |
59 | .title {
60 | align-self: flex-start;
61 | margin-bottom: -1px;
62 | padding: 0.4em 0.6em;
63 | font-size: 0.9em;
64 | font-weight: 700;
65 | background: #4c4a56;
66 | border: 1px solid #6a6586;
67 | border-bottom: 0;
68 | z-index: 1;
69 | & + & {
70 | margin-left: 0.6em;
71 | }
72 | &.tabBtn {
73 | @extend %tappable;
74 | color: #9299a7;
75 | background: #252a3c;
76 | box-shadow: inset 0 -1px 0 0 #6a6586;
77 | transition-duration: 120ms;
78 | transition-property: background, color;
79 | &:hover {
80 | background: #524a3f;
81 | color: #ff9b70;
82 | }
83 | }
84 | }
85 |
86 | .content {
87 | flex-grow: 1;
88 | display: flex;
89 | flex-direction: column;
90 | border: 1px solid #6a6586;
91 | background: #4c4a56;
92 | overflow: auto;
93 | }
94 |
95 | .contentGrid {
96 | flex-direction: row;
97 | background-color: #a8a5b7;
98 | background-image: url('data:image/svg+xml;charset=utf-8,');
99 | background-size: 20px 20px;
100 | }
101 |
102 | .doneBtn {
103 | @extend %tappable;
104 | background: #009a00;
105 | padding: 0.4em 0.6em;
106 | font-size: 0.9em;
107 | font-weight: 700;
108 | box-shadow: inset 0 0 0 1px #002f0a, inset 0px 1px 0 1px #89f39f;
109 | }
110 |
--------------------------------------------------------------------------------
/src/_shell/analytics.ts:
--------------------------------------------------------------------------------
1 | import { merge } from 'rxjs'
2 | import { actionsGrid, actionsItems, actionsShell } from '../_generic/actions'
3 |
4 | type TAnalyticsEvent = {
5 | action: string
6 | }
7 |
8 | merge(
9 | actionsShell.howToUse.$.map(() => ({ action: 'how_to_use' })),
10 | actionsShell.getCode.$.map(() => ({ action: 'get_the_code' })),
11 | actionsItems.add.$.map(() => ({ action: 'add_item' })),
12 | merge(actionsGrid.addCol.$, actionsGrid.addAfterCol.$, actionsGrid.addBeforeCol.$).map(() => ({
13 | action: 'add_column',
14 | })),
15 | merge(actionsGrid.addRow.$, actionsGrid.addAfterRow.$, actionsGrid.addBeforeRow.$).map(() => ({
16 | action: 'add_row',
17 | }))
18 | ).subscribe((e: TAnalyticsEvent) => {
19 | if (process.env.NODE_ENV === 'production') {
20 | window.gtag('event', e.action)
21 | } else {
22 | // tslint:disable:no-console
23 | console.info('AnalyticsEvent:', e.action)
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/src/_shell/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import { App } from './App'
4 | import './analytics'
5 | import './blueprint.css'
6 | import './overrides.css'
7 |
8 | document.body.classList.add('bp3-dark')
9 |
10 | export const mount = (el: HTMLElement) => {
11 | ReactDOM.render(React.createElement(App), el)
12 | }
13 |
--------------------------------------------------------------------------------
/src/_shell/overrides.css:
--------------------------------------------------------------------------------
1 | input,
2 | textarea,
3 | select,
4 | button {
5 | font-family: inherit;
6 | font-weight: inherit;
7 | }
8 |
9 | .react-tiny-popover-container {
10 | z-index: 100;
11 | overflow: visible !important;
12 | transition: none !important;
13 | }
14 |
--------------------------------------------------------------------------------
/src/area-selector/index.tsx:
--------------------------------------------------------------------------------
1 | import { F, reactiveList } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { combineLatest, Observable, Subscription } from 'rxjs'
4 | import {
5 | distinctUntilChanged,
6 | endWith,
7 | filter,
8 | map,
9 | mergeMap,
10 | shareReplay,
11 | startWith,
12 | takeUntil,
13 | withLatestFrom,
14 | } from 'rxjs/operators'
15 | import { selectedItem$ } from '../items/state'
16 | import { ca } from '../_generic/supply/action-helpers'
17 | import { isShallowEqual } from '../_generic/supply/utils'
18 | import { MapElement } from '../_generic/ui/MapElement'
19 | import { ShowIf } from '../_generic/ui/ShowIf'
20 | import $ from './style.scss'
21 |
22 | type TProps = {
23 | colsLength$: Observable
24 | rowsLength$: Observable
25 | }
26 |
27 | export class AreaSelector extends React.PureComponent {
28 | private areaRef = React.createRef()
29 | private sub: Subscription
30 |
31 | private units$ = combineLatest(this.props.colsLength$, this.props.rowsLength$, (cols, rows) => {
32 | const res: string[] = []
33 | for (let colIndex = 0; colIndex < cols; colIndex++) {
34 | for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
35 | res.push(colIndex + DIV + rowIndex)
36 | }
37 | }
38 | return res
39 | }).pipe(shareReplay(1))
40 | private isSelected$ = this.units$.map((units) => Boolean(units.length))
41 | private list$ = reactiveList(this.units$, (st) => {
42 | const [px, py] = st.split(DIV)
43 | return (
44 |
49 | )
50 | })
51 | private gridColumnEnd$ = this.props.colsLength$.map((v) => v + 2)
52 | private gridRowEnd$ = this.props.rowsLength$.map((v) => v + 2)
53 |
54 | componentDidMount() {
55 | const el = this.areaRef.current as HTMLDivElement
56 | this.sub = selection$
57 | .pipe(
58 | filter(Boolean),
59 | withLatestFrom(this.props.colsLength$, this.props.rowsLength$),
60 | map(([selection, colsLength, rowsLength]) => {
61 | if (colsLength && rowsLength) {
62 | const { x1, y1, x2, y2 } = selection
63 | const { left, top, width, height } = el.getBoundingClientRect()
64 | const hts = width / colsLength
65 | const vts = height / rowsLength
66 | const colStart = Math.ceil((x1 - left) / hts)
67 | const rowStart = Math.ceil((y1 - top) / vts)
68 | const colEnd = Math.ceil((x2 - left) / hts) + 1
69 | const rowEnd = Math.ceil((y2 - top) / vts) + 1
70 | return { colStart, rowStart, colEnd, rowEnd }
71 | } else {
72 | return { colStart: 1, rowStart: 1, colEnd: 2, rowEnd: 2 }
73 | }
74 | }),
75 | distinctUntilChanged(isShallowEqual)
76 | )
77 | .subscribe((location) => {
78 | selectedItem$.modify((item) => ({ ...item, ...location }))
79 | })
80 | }
81 |
82 | componentWillUnmount() {
83 | this.sub.unsubscribe()
84 | }
85 |
86 | render() {
87 | return (
88 |
101 | {this.list$}
102 |
103 | {() => (
104 |
110 | )}
111 |
112 |
113 |
114 | )
115 | }
116 | }
117 |
118 | const Selections = () => (
119 |
120 | {(style) => style && }
121 |
122 | )
123 |
124 | const down = ca>()
125 | const move = ca>()
126 | const up = ca>()
127 | const down$ = down.$
128 | const move$ = move.$
129 | const up$ = up.$
130 |
131 | const selection$ = down$.pipe(
132 | mergeMap((e) => {
133 | const x1 = e.clientX
134 | const y1 = e.clientY
135 | return move$.pipe(
136 | map(({ clientX, clientY }) => {
137 | return {
138 | x1: Math.min(x1, clientX),
139 | y1: Math.min(y1, clientY),
140 | x2: Math.max(x1, clientX),
141 | y2: Math.max(y1, clientY),
142 | }
143 | }),
144 | takeUntil(up$),
145 | endWith(null)
146 | )
147 | }),
148 | startWith(null),
149 | shareReplay(1)
150 | )
151 |
152 | const selectionStyle$ = selection$
153 | .pipe(withLatestFrom(selectedItem$.view('color')))
154 | .map(([selection, backgroundColor]) => {
155 | if (selection) {
156 | const { x1, y1, x2, y2 } = selection
157 | return { left: x1, top: y1, width: x2 - x1, height: y2 - y1, backgroundColor }
158 | } else {
159 | return null
160 | }
161 | })
162 |
163 | const gridArea$ = selectedItem$.view(
164 | ({ colStart, rowStart, colEnd, rowEnd }) => `${rowStart} / ${colStart} / ${rowEnd} / ${colEnd}`
165 | )
166 |
167 | const DIV = ':'
168 |
--------------------------------------------------------------------------------
/src/area-selector/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .area {
4 | @extend %tappable;
5 | cursor: crosshair;
6 | display: grid;
7 | grid-auto-rows: minmax(3em, 1fr);
8 | grid-auto-columns: minmax(3em, 1fr);
9 | grid-gap: 2px;
10 | }
11 |
12 | .unit {
13 | background: #ebebec;
14 | }
15 |
16 | .selectedArea {
17 | background: rgba(0, 147, 255, 0.34);
18 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.75), inset 0 0 0 3px rgba(79, 140, 204, 0.75);
19 | }
20 |
21 | .selection {
22 | position: absolute;
23 | background-image: linear-gradient(
24 | 45deg,
25 | rgba(255, 255, 255, 0.15) 25%,
26 | transparent 25%,
27 | transparent 50%,
28 | rgba(255, 255, 255, 0.15) 50%,
29 | rgba(255, 255, 255, 0.15) 75%,
30 | transparent 75%,
31 | transparent
32 | );
33 | background-size: 1.2em 1.2em;
34 | box-shadow: inset 0 0 0px 1px rgba(255, 255, 255, 0.8);
35 | opacity: 0.85;
36 | animation: stripes 2s linear infinite;
37 | }
38 |
39 | @keyframes stripes {
40 | 0% {
41 | background-position: 8em 0;
42 | }
43 | 100% {
44 | background-position: 0 0;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare const PRERENDER: boolean
2 |
3 | declare module 'from-html' {
4 | const fromHTML: (template: string) => { [elName: string]: HTMLElement }
5 | export default fromHTML
6 | }
7 |
8 | declare module 'clipboard-copy' {
9 | const clipboardCopy: (toClipboard: any) => void
10 | export default clipboardCopy
11 | }
12 |
13 | declare module '*.css' {
14 | const content: string
15 | export default content
16 | }
17 |
18 | declare module '*.scss' {
19 | const classes: { [key: string]: string }
20 | export default classes
21 | }
22 |
23 | // tslint:disable-next-line:interface-name
24 | declare interface NodeModule {
25 | hot: any
26 | }
27 |
28 | // tslint:disable-next-line:interface-name
29 | declare interface Window {
30 | dataLayer: any[]
31 | gtag: (...args: any[]) => void
32 | }
33 |
--------------------------------------------------------------------------------
/src/first-interaction/Landing.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { hot, setConfig } from 'react-hot-loader'
3 | import { Logo } from '../_generic/ui/Logo'
4 | import { Video } from '../_generic/ui/Video'
5 | import $ from './style.scss'
6 |
7 | setConfig({
8 | logLevel: 'debug',
9 | ignoreSFC: true, // RHL will be __completely__ disabled for SFC
10 | pureRender: true, // RHL will not change render method
11 | })
12 |
13 | const textJSDisabled = [
14 | 'JavaScript is disabled.',
15 | 'The app requires JS.',
16 | 'Please, enable JavaScript and refresh the page.',
17 | ].join(' ')
18 |
19 | const textNoSupport = [
20 | "Unfortunately, your browser doesn't support CSS Grid Layout.",
21 | 'Please, upgrade your browser.',
22 | 'Google Chrome is a good option.',
23 | 'Hope to see you again :-)',
24 | ].join(' ')
25 |
26 | export const Landing = hot(module)(() => {
27 | return (
28 | <>
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | >
47 | )
48 | })
49 |
50 | const Title = () => (
51 |
52 |
53 |
54 | CSS
55 |
56 | Grid
57 |
58 | Layout
59 |
60 | Generator
61 |
62 |
63 | )
64 |
65 | const Subtitle = () => (
66 |
67 |
Build complex CSS grid layouts — visually
68 |
69 | It's a professional tool: implicit grid tracks (auto-generated grid), minmax(),
70 | fit-content(), export in JSX and Styled Components.
71 |
72 |
73 | )
74 |
75 | const AppLoading = () => (
76 |
77 |
App is loading...
78 |
79 | )
80 |
81 | const AppButton = () => (
82 |
83 |
84 |
85 | )
86 |
87 | const Message = ({ text, id, hidden }: { text?: string; id?: string; hidden?: boolean }) => (
88 |
91 | )
92 |
93 | const hide = (className: string) => className + ' hidden'
94 |
--------------------------------------------------------------------------------
/src/first-interaction/logic.ts:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | window.dataLayer = window.dataLayer || []
3 | // tslint:disable-next-line:only-arrow-functions
4 | window.gtag = function() {
5 | window.dataLayer.push(arguments)
6 | }
7 | window.gtag('js', new Date())
8 | window.gtag('config', 'UA-129342832-1')
9 | }
10 |
11 | let isAppMounted = false
12 | const MIN_WIDTH = 1099
13 | const HIDDEN = 'hidden'
14 | const SKIP_INTRO = '__SKIP_INTRO__'
15 | const hide = (el: HTMLElement) => {
16 | if (!isHidden(el)) {
17 | el.classList.add(HIDDEN)
18 | }
19 | }
20 | const show = (el: HTMLElement) => {
21 | if (isHidden(el)) {
22 | el.classList.remove(HIDDEN)
23 | }
24 | }
25 | const isHidden = (el: HTMLElement) => el.classList.contains(HIDDEN)
26 | const getElement = (id: string) => document.getElementById(id) as HTMLElement
27 |
28 | const App = getElement('App')
29 | const Video = getElement('Video')
30 | const NoSupport = getElement('NoSupport')
31 | const AppButton = getElement('AppButton')
32 | const JSDisabled = getElement('JSDisabled')
33 | const AppLoading = getElement('AppLoading')
34 | const LittleSpace = getElement('LittleSpace')
35 |
36 | const init = () => {
37 | hide(JSDisabled)
38 | show(Video)
39 | if (CSS && CSS.supports && CSS.supports('display: grid')) {
40 | screenSize()
41 | window.addEventListener('resize-end', screenSize)
42 | } else {
43 | show(NoSupport)
44 | }
45 | }
46 |
47 | const screenSize = () => {
48 | if (window.innerWidth > MIN_WIDTH) {
49 | hide(LittleSpace)
50 | if (isAppMounted) {
51 | canRunTheApp()
52 | } else {
53 | show(AppLoading)
54 | import(/* webpackChunkName: "app" */
55 | '../_shell').then(({ mount }) => {
56 | mount(App)
57 | hide(AppLoading)
58 | canRunTheApp()
59 | isAppMounted = true
60 | })
61 | }
62 | } else {
63 | hide(App)
64 | hide(AppButton)
65 | showLittleSpace(window.innerHeight)
66 | }
67 | }
68 |
69 | const canRunTheApp = () => {
70 | if (localStorage.getItem(SKIP_INTRO)) {
71 | show(App)
72 | } else {
73 | showAppButton()
74 | }
75 | }
76 |
77 | const showLittleSpace = (innerHeight: number) => {
78 | const p = LittleSpace.children.item(0) as HTMLParagraphElement
79 | const text = [
80 | 'The app requires at least 1100px of screen width.',
81 | innerHeight > MIN_WIDTH
82 | ? 'Please, try to switch your device to landscape mode.'
83 | : 'Please, open this page on your laptop or desktop with big screen.',
84 | ]
85 | p.innerHTML = text.join(' ')
86 | show(LittleSpace)
87 | }
88 |
89 | const showAppButton = () => {
90 | const button = AppButton.children.item(0) as HTMLButtonElement
91 | button.onclick = () => {
92 | localStorage.setItem(SKIP_INTRO, 'y')
93 | show(App)
94 | }
95 | show(AppButton)
96 | }
97 |
98 | init()
99 |
100 | document.addEventListener('DOMContentLoaded', () => {
101 | let resizeEnd: NodeJS.Timeout
102 | window.addEventListener('resize', () => {
103 | clearTimeout(resizeEnd)
104 | resizeEnd = setTimeout(() => {
105 | window.dispatchEvent(new Event('resize-end'))
106 | }, 100)
107 | })
108 | })
109 |
--------------------------------------------------------------------------------
/src/first-interaction/prerender.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom'
3 | import { Landing } from './Landing'
4 |
5 | if (process.env.NODE_ENV === 'production') {
6 | document.head.insertAdjacentHTML(
7 | 'afterbegin',
8 | ''
9 | )
10 | }
11 |
12 | document.head.insertAdjacentHTML(
13 | 'beforeend',
14 | ''
15 | )
16 |
17 | ReactDOM.render(React.createElement(Landing), document.body)
18 |
--------------------------------------------------------------------------------
/src/first-interaction/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | body {
4 | height: 100vh;
5 | margin: 0;
6 | padding: 0;
7 | overscroll-behavior: none;
8 | font-family: $font-stack;
9 | font-weight: 400;
10 | font-size: 14px;
11 | contain: strict;
12 | overflow: hidden;
13 | }
14 |
15 | .guard {
16 | height: 100vh;
17 | overflow-y: auto;
18 | color: #3f3f3f;
19 | background: #f7eef2;
20 | }
21 |
22 | .app {
23 | display: flex;
24 | position: absolute;
25 | top: 0;
26 | left: 0;
27 | width: 100%;
28 | height: 100vh;
29 | padding: 10px;
30 | box-sizing: border-box;
31 | contain: strict;
32 | overflow: hidden;
33 | color: #f4f4f4;
34 | background: #242424;
35 | animation: open-app 0.4s cubic-bezier(0.4, 0, 0.2, 1);
36 | }
37 |
38 | @keyframes open-app {
39 | 0% {
40 | opacity: 0;
41 | transform: scale(0.4);
42 | }
43 | 60% {
44 | opacity: 1;
45 | }
46 | 100% {
47 | transform: scale(1);
48 | }
49 | }
50 |
51 | .container {
52 | margin: 0 auto;
53 | padding: 3em 6em;
54 | min-width: 320px;
55 | max-width: 1200px;
56 | box-sizing: border-box;
57 | }
58 |
59 | .title {
60 | display: flex;
61 | h1 {
62 | margin: 0 0 0 0.2em;
63 | font-size: 2.3em;
64 | }
65 | }
66 |
67 | .subtitle {
68 | h2 {
69 | margin: 0;
70 | font-size: 1.7em;
71 | font-weight: 400;
72 | line-height: 1.5em;
73 | }
74 | p {
75 | margin: 1.4em 0 0;
76 | font-size: 1.6em;
77 | line-height: 1.5em;
78 | }
79 | }
80 |
81 | .wrapper {
82 | display: flex;
83 | margin-top: 2em;
84 | }
85 |
86 | %app-common {
87 | flex-grow: 1;
88 | max-width: 20em;
89 | margin: 0 auto;
90 | padding: 1em 1.5em;
91 | color: #fff;
92 | font-size: 2em;
93 | font-weight: 700;
94 | letter-spacing: 1px;
95 | line-height: 1;
96 | border-radius: 0.4em;
97 | text-align: center;
98 | }
99 |
100 | .appButton {
101 | @extend %app-common;
102 | @extend %tappable;
103 | -webkit-appearance: none;
104 | border: 0 none;
105 | background-color: #6d3392;
106 | outline: 0 none;
107 | letter-spacing: 0.2em;
108 | transition-duration: 120ms;
109 | transition-property: background, letter-spacing;
110 | animation: wiggle 6s cubic-bezier(0.65, 0.05, 0.36, 1) infinite;
111 | &:hover {
112 | letter-spacing: 0.4em;
113 | background-color: #994eca;
114 | }
115 | }
116 |
117 | .appLoading {
118 | @extend %app-common;
119 | position: relative;
120 | overflow: hidden;
121 | background-color: #351919;
122 | &:before {
123 | position: absolute;
124 | display: block;
125 | content: '';
126 | bottom: 0;
127 | left: 0;
128 | width: 98%;
129 | height: 100%;
130 | background-color: rgba(255, 220, 196, 0.5);
131 | pointer-events: none;
132 | animation: loading 30s;
133 | }
134 | }
135 |
136 | .video {
137 | margin-top: 3em;
138 | }
139 |
140 | .message {
141 | display: flex;
142 | margin-top: 2em;
143 | p {
144 | flex-grow: 1;
145 | max-width: 17em;
146 | margin: 0 auto;
147 | padding: 1em 1.2em;
148 | background: #fff;
149 | font-size: 2.2em;
150 | box-shadow: inset 0 0 0 0.1em #ff6a00, inset 0 0 0 0.2em #ffda00;
151 | border-radius: 0.4em;
152 | color: #a92d10;
153 | }
154 | }
155 |
156 | @media screen and (min-width: 820px) {
157 | .header {
158 | display: flex;
159 | .subtitle {
160 | margin-left: 7em;
161 | }
162 | }
163 |
164 | .wrapper {
165 | margin-top: 4em;
166 | }
167 |
168 | .video {
169 | margin-top: 5em;
170 | }
171 | }
172 |
173 | @media screen and (max-width: 819px) {
174 | .container {
175 | font-size: 0.9em;
176 | padding: 3em;
177 | }
178 |
179 | .subtitle {
180 | margin-top: 2em;
181 | }
182 | }
183 |
184 | @media screen and (max-width: 420px) {
185 | .container {
186 | font-size: 0.75em;
187 | padding: 1.7em;
188 | }
189 | }
190 |
191 | @keyframes loading {
192 | 0% {
193 | width: 1.5%;
194 | }
195 | 5% {
196 | width: 30%;
197 | }
198 | 30% {
199 | width: 80%;
200 | }
201 | 100% {
202 | width: 98%;
203 | }
204 | }
205 |
206 | @keyframes wiggle {
207 | 0%,
208 | 2% {
209 | transform: rotateZ(0);
210 | }
211 | 5% {
212 | transform: rotateZ(-15deg);
213 | }
214 | 7% {
215 | transform: rotateZ(10deg);
216 | }
217 | 9% {
218 | transform: rotateZ(-10deg);
219 | }
220 | 10% {
221 | transform: rotateZ(6deg);
222 | }
223 | 12% {
224 | transform: rotateZ(-4deg);
225 | }
226 | 14%,
227 | 100% {
228 | transform: rotateZ(0);
229 | }
230 | }
231 |
232 | :global(.hidden) {
233 | display: none;
234 | }
235 |
--------------------------------------------------------------------------------
/src/get-the-code/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Classes, Dialog, H4, Tab, Tabs } from '@blueprintjs/core'
2 | import clipboardCopy from 'clipboard-copy'
3 | import * as React from 'react'
4 | import { Observable } from 'rxjs'
5 | import { css$, html$, jsxCSSModules$, jsxPlain$, styledComponents$ } from '../preview/state'
6 | import { actionsShell } from '../_generic/actions'
7 | import { Btn } from '../_generic/ui/Btn'
8 | import { MapElement } from '../_generic/ui/MapElement'
9 | import $ from './style.scss'
10 |
11 | const modalProps = {
12 | autoFocus: true,
13 | canEscapeKeyClose: true,
14 | canOutsideClickClose: true,
15 | enforceFocus: true,
16 | usePortal: true,
17 | style: {
18 | width: '90%',
19 | minWidth: '600px',
20 | maxWidth: '1100px',
21 | minHeight: '82vh',
22 | },
23 | }
24 |
25 | export class GetTheCode extends React.PureComponent<{}, { isOpen: boolean }> {
26 | state = {
27 | isOpen: false,
28 | }
29 |
30 | private handleOpen = () => {
31 | actionsShell.getCode()
32 | this.setState({ isOpen: true })
33 | }
34 | private handleClose = () => this.setState({ isOpen: false })
35 |
36 | render() {
37 | return (
38 | <>
39 |
40 |
58 | >
59 | )
60 | }
61 | }
62 |
63 | const General = () => (
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | )
73 |
74 | const JSX = () => (
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | )
84 |
85 | const StyledComponents = () =>
86 |
87 | class Code extends React.PureComponent<{ lang: string; code$: Observable }> {
88 | private ref = React.createRef()
89 | private toClipboard = () => {
90 | const el = this.ref.current
91 | const code = el && (el.textContent || el.innerText)
92 | if (code) {
93 | clipboardCopy(code)
94 | }
95 | }
96 | render() {
97 | return (
98 | <>
99 | {this.props.lang.toUpperCase()}
100 |
108 |
109 | {(__html) => }
110 |
111 | >
112 | )
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/get-the-code/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .split {
4 | display: flex;
5 | > div + div {
6 | margin-left: 2em;
7 | }
8 | }
9 |
10 | .code {
11 | position: relative;
12 | text-transform: none;
13 | font-family: $font-mono-stack;
14 | display: block;
15 | margin: 1em 0;
16 | border-radius: 0.6em;
17 | padding: 1.2em 1em 1.2em 4em;
18 | line-height: 1.3;
19 | word-break: break-all;
20 | word-wrap: break-word;
21 | box-shadow: inset 0 0 0 1px rgba(16, 22, 26, 0.4);
22 | background: rgba(16, 22, 26, 0.3);
23 | color: #90a29c;
24 | tab-size: 4;
25 | counter-reset: line;
26 | .line:before {
27 | display: inline-block;
28 | position: absolute;
29 | left: 0;
30 | width: 2em;
31 | padding: 0 0.5em;
32 | color: #4a5161;
33 | border-right: 1px solid #3b3f48;
34 | text-align: right;
35 | counter-increment: line;
36 | content: counter(line);
37 | }
38 | .hl1 {
39 | color: #fff;
40 | }
41 | .hl2 {
42 | color: #e75eff;
43 | }
44 | .hl3 {
45 | color: #65d81c;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/grid/Highlighter.tsx:
--------------------------------------------------------------------------------
1 | import { Atom } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { merge, Observable, Subscription } from 'rxjs'
4 | import { map, withLatestFrom } from 'rxjs/operators'
5 | import { ca } from '../_generic/supply/action-helpers'
6 | import { MapElement } from '../_generic/ui/MapElement'
7 | import $ from './style.scss'
8 |
9 | export const HLLeave = ca()
10 | export const HLAddRow = ca()
11 | export const HLAddCol = ca()
12 | export const HLRemoveRow = ca<[number, number]>()
13 | export const HLRemoveCol = ca<[number, number]>()
14 |
15 | export class Highlighter extends React.PureComponent<{
16 | colsLength$: Observable
17 | rowsLength$: Observable
18 | }> {
19 | sub: Subscription
20 | style$ = Atom.create(undefined)
21 | gColsLength$ = this.props.colsLength$.map((v) => v + 2)
22 | gRowsLength$ = this.props.rowsLength$.map((v) => v + 2)
23 | componentDidMount() {
24 | this.sub = merge(
25 | this.props.colsLength$.map(() => undefined),
26 | this.props.rowsLength$.map(() => undefined),
27 | HLLeave.$.pipe(
28 | withLatestFrom(this.style$),
29 | map(([, s]) => ({ ...s, opacity: 0 }))
30 | ),
31 | HLRemoveCol.$.pipe(
32 | withLatestFrom(this.gRowsLength$),
33 | map(([[start, end], gRowsLength]) => {
34 | return {
35 | opacity: 0.5,
36 | gridColumnStart: start,
37 | gridColumnEnd: end,
38 | gridRowStart: 1,
39 | gridRowEnd: gRowsLength,
40 | background: '#ff3100',
41 | } as React.CSSProperties
42 | })
43 | ),
44 | HLRemoveRow.$.pipe(
45 | withLatestFrom(this.gColsLength$),
46 | map(([[start, end], gColsLength]) => {
47 | return {
48 | opacity: 0.5,
49 | gridColumnStart: 1,
50 | gridColumnEnd: gColsLength,
51 | gridRowStart: start,
52 | gridRowEnd: end,
53 | background: '#ff3100',
54 | } as React.CSSProperties
55 | })
56 | ),
57 | HLAddCol.$.pipe(
58 | withLatestFrom(this.gColsLength$, this.gRowsLength$),
59 | map(([pos, gColsLength, gRowsLength]) => {
60 | const isLast = gColsLength === pos
61 | return {
62 | opacity: isLast ? 1 : 0.6,
63 | gridColumnStart: pos - 1,
64 | gridColumnEnd: pos,
65 | gridRowStart: 1,
66 | gridRowEnd: gRowsLength,
67 | height: 'auto',
68 | width: `${isLast ? 1 : 2}em`,
69 | justifySelf: 'flex-end',
70 | marginRight: isLast ? undefined : '-1em',
71 | background: isLast
72 | ? 'linear-gradient(to left, #62ff00 0%, rgba(0, 0, 0, 0) 100%)'
73 | : 'linear-gradient(to left, rgba(0, 0, 0, 0) 0%, #62ff00 50%, rgba(0, 0, 0, 0) 100%)',
74 | } as React.CSSProperties
75 | })
76 | ),
77 | HLAddRow.$.pipe(
78 | withLatestFrom(this.gColsLength$, this.gRowsLength$),
79 | map(([pos, gColsLength, gRowsLength]) => {
80 | const isLast = gRowsLength === pos
81 | return {
82 | opacity: isLast ? 1 : 0.6,
83 | gridColumnStart: 1,
84 | gridColumnEnd: gColsLength,
85 | gridRowStart: pos - 1,
86 | gridRowEnd: pos,
87 | height: `${isLast ? 1 : 2}em`,
88 | width: 'auto',
89 | alignSelf: 'flex-end',
90 | marginBottom: isLast ? undefined : '-1em',
91 | background: isLast
92 | ? 'linear-gradient(to top, #62ff00 0%, rgba(0, 0, 0, 0) 100%)'
93 | : 'linear-gradient(to top, rgba(0, 0, 0, 0) 0%, #62ff00 50%, rgba(0, 0, 0, 0) 100%)',
94 | } as React.CSSProperties
95 | })
96 | )
97 | ).subscribe((s) => this.style$.set(s))
98 | }
99 | componentWillUnmount() {
100 | this.sub.unsubscribe()
101 | }
102 | render() {
103 | return (
104 |
105 | {(style) => }
106 |
107 | )
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/grid/TrackOverlay.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { merge } from 'rxjs'
3 | import { shareReplay, startWith } from 'rxjs/operators'
4 | import { TrackSettings } from '../kit-sections/TrackSettings'
5 | import { actionsGrid } from '../_generic/actions'
6 | import { ITrackOverlay } from '../_generic/types/common'
7 | import { MapElement } from '../_generic/ui/MapElement'
8 | import { Overlay } from '../_generic/ui/Overlay'
9 |
10 | const state$ = merge(
11 | actionsGrid.openTrackSettings.$,
12 | merge(actionsGrid.removeCol.$, actionsGrid.removeRow.$, actionsGrid.closeTrackSettings.$).map(
13 | () => null
14 | )
15 | ).pipe(
16 | startWith(null),
17 | shareReplay(1)
18 | )
19 |
20 | const isOpen$ = state$.map(Boolean)
21 |
22 | export const trackOverlayID$ = state$.map((s) => s && s.track$.get().id)
23 |
24 | export const TrackOverlay = () => (
25 |
26 | {(payload) => {
27 | if (payload) {
28 | const { rect, ...props } = payload
29 | return (
30 | }
37 | >
38 |
48 |
49 | )
50 | } else {
51 | return null
52 | }
53 | }}
54 |
55 | )
56 |
--------------------------------------------------------------------------------
/src/grid/Tracks.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, classes, F, Lens, reactiveList } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { actionsGrid } from '../_generic/actions'
4 | import { ITrack } from '../_generic/types/common'
5 | import { MapElement } from '../_generic/ui/MapElement'
6 | import { getTrackByID, tracksPosition, trackTitle } from './state'
7 | import $ from './style.scss'
8 | import { trackOverlayID$ } from './TrackOverlay'
9 |
10 | type TProps = {
11 | tracks$: Atom
12 | repeat: boolean
13 | row: boolean
14 | }
15 |
16 | export const Tracks = ({ tracks$, repeat, row }: TProps) => {
17 | const tracksPosition$ = tracks$.view(tracksPosition)
18 | return (
19 | tracks.map(({ id }) => id)), (id) => {
21 | const track$ = getTrackByID(tracks$, id)
22 | const title$ = track$.view(trackTitle)
23 | const position$ = tracksPosition$
24 | .view(Lens.key(id))
25 | .view(Lens.withDefault<[number, number]>([0, 0]))
26 | return (
27 | tid && tid === id && $.active)
32 | )}
33 | style={position$.view(([start, end]) => ({
34 | [row ? 'gridRowStart' : 'gridColumnStart']: start,
35 | [row ? 'gridRowEnd' : 'gridColumnEnd']: end,
36 | }))}
37 | onClick={(e) => {
38 | actionsGrid.openTrackSettings({
39 | track$,
40 | position$,
41 | row,
42 | repeat,
43 | rect: e.currentTarget.getBoundingClientRect(),
44 | })
45 | }}
46 | >
47 | {title$}
48 |
49 | )
50 | })}
51 | />
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/src/grid/index.tsx:
--------------------------------------------------------------------------------
1 | import { Atom } from '@grammarly/focal'
2 | import cc from 'classcat'
3 | import * as React from 'react'
4 | import { Subscription } from 'rxjs'
5 | import { AreaSelector } from '../area-selector'
6 | import { actionsGrid } from '../_generic/actions'
7 | import { IGrid } from '../_generic/types/common'
8 | import { ShowIf } from '../_generic/ui/ShowIf'
9 | import { Highlighter } from './Highlighter'
10 | import { calcLength, createTrack, removeTrack } from './state'
11 | import $ from './style.scss'
12 | import { TrackOverlay } from './TrackOverlay'
13 | import { Tracks } from './Tracks'
14 |
15 | type TProps = {
16 | grid$: Atom
17 | repeat: boolean
18 | }
19 |
20 | // tslint:disable:max-line-length
21 | export class Grid extends React.PureComponent {
22 | subs: Subscription[]
23 | cols$ = this.props.grid$.lens('cols')
24 | rows$ = this.props.grid$.lens('rows')
25 | componentDidMount() {
26 | this.subs = [
27 | actionsGrid.removeRow.$.subscribe(removeTrack(this.rows$)),
28 | actionsGrid.addRow.$.subscribe(createTrack(this.rows$, true, false)),
29 | actionsGrid.addAfterRow.$.subscribe(createTrack(this.rows$, true, true)),
30 | actionsGrid.addBeforeRow.$.subscribe(createTrack(this.rows$, true, false)),
31 | actionsGrid.removeCol.$.subscribe(removeTrack(this.cols$)),
32 | actionsGrid.addCol.$.subscribe(createTrack(this.cols$, false, false)),
33 | actionsGrid.addAfterCol.$.subscribe(createTrack(this.cols$, false, true)),
34 | actionsGrid.addBeforeCol.$.subscribe(createTrack(this.cols$, false, false)),
35 | ]
36 | }
37 | componentWillUnmount() {
38 | this.subs.forEach((sub) => sub.unsubscribe())
39 | }
40 | render() {
41 | const repeat = this.props.repeat
42 | const colsLength$ = this.cols$.view(calcLength)
43 | const rowsLength$ = this.rows$.view(calcLength)
44 | return (
45 |
46 | {/*
}
50 | >
51 | isGridConfigOpen$.set(true)}
54 | >
55 |
58 |
59 | */}
60 |
61 | {() => (
62 |
67 | Add Column
68 |
69 | )}
70 |
71 |
72 | {() => (
73 |
78 | Add Row
79 |
80 | )}
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | )
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/grid/state.ts:
--------------------------------------------------------------------------------
1 | import { Atom, Lens } from '@grammarly/focal'
2 | import { NTA } from '../_generic/supply/utils'
3 | import {
4 | defaultGridSettings,
5 | defaultTrack,
6 | IGrid,
7 | IGridSettings,
8 | ITrack,
9 | } from '../_generic/types/common'
10 |
11 | export const gridSettings$ = Atom.create(defaultGridSettings)
12 |
13 | export const explicitGrid$ = Atom.create({
14 | cols: [
15 | { ...defaultTrack, id: 'p-col-1', value: '1fr' },
16 | { ...defaultTrack, id: 'p-col-2', value: '1fr' },
17 | ],
18 | rows: [
19 | { ...defaultTrack, id: 'p-row-1', value: '1fr' },
20 | { ...defaultTrack, id: 'p-row-2', value: '1fr' },
21 | ],
22 | })
23 |
24 | export const implicitGrid$ = Atom.create({
25 | cols: [],
26 | rows: [],
27 | })
28 |
29 | export const calcLength = (v: ITrack[]) =>
30 | v.reduce((sum, { repeat }) => sum + (typeof repeat === 'string' ? 1 : repeat || 1), 0)
31 |
32 | export const tracksPosition = (v: ITrack[]) => {
33 | let prev = 2
34 | return v.reduce<{ [trackID: string]: [number, number] }>((acc, { id, repeat }) => {
35 | acc[id] = [prev, (prev = prev + (typeof repeat === 'string' ? 1 : repeat || 1))]
36 | return acc
37 | }, {})
38 | }
39 |
40 | export const trackTitle = ({ value, min, max, minmax, fitContent, repeat }: ITrack) => {
41 | let title: string
42 | if (minmax) {
43 | title = `${NTA(min)}→${NTA(max)}`
44 | } else if (fitContent) {
45 | title = `fit(${NTA(value)})`
46 | } else {
47 | title = NTA(value)
48 | }
49 | if (repeat) {
50 | title = `${repeat}${typeof repeat === 'number' ? '×' : ''} ${title}`
51 | }
52 | return title
53 | }
54 |
55 | export const getTrackByIndex = (tracks$: Atom, index: number) =>
56 | tracks$.lens(Lens.index(index)).lens(Lens.withDefault(defaultTrack))
57 |
58 | export const getTrackByID = (tracks$: Atom, id: string) =>
59 | tracks$.lens(
60 | Lens.create(
61 | (tracks: ITrack[]) => tracks.find((track) => track.id === id) || defaultTrack,
62 | (track, tracks) => {
63 | const tID = track.id
64 | const index = tracks.findIndex((t) => t.id === tID)
65 | if (index === -1) {
66 | return tracks
67 | } else {
68 | const nextTracks = [...tracks]
69 | nextTracks.splice(index, 1, track)
70 | return nextTracks
71 | }
72 | }
73 | )
74 | )
75 |
76 | let counter = 1
77 | export const createTrack = (tracks$: Atom, row: boolean, after: boolean) => (
78 | track$: Atom
79 | ) => {
80 | tracks$.modify((tracks) => {
81 | const nextTrack = [...tracks]
82 | const pos = track$.get ? tracks.indexOf(track$.get()) + (after ? 1 : 0) : tracks.length
83 | nextTrack.splice(pos, 0, {
84 | ...defaultTrack,
85 | id: `${row ? 'row' : 'col'}-${counter++}`,
86 | value: row ? null : '1fr',
87 | })
88 | return nextTrack
89 | })
90 | }
91 |
92 | export const removeTrack = (tracks$: Atom) => (track$: Atom) => {
93 | tracks$.modify((track) => {
94 | const nextTrack = [...track]
95 | const pos = track.indexOf(track$.get())
96 | nextTrack.splice(pos, 1)
97 | return nextTrack
98 | })
99 | }
100 |
--------------------------------------------------------------------------------
/src/grid/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .container {
4 | flex-grow: 1;
5 | display: grid;
6 | grid-auto-rows: minmax(3em, 1fr);
7 | grid-auto-columns: minmax(3em, 1fr);
8 | grid-gap: 2px;
9 | grid-template-columns: 3em;
10 | grid-template-rows: 3em;
11 | }
12 |
13 | .col {
14 | grid-row-start: 1;
15 | border-radius: 0 0 0.2em 0.2em;
16 | }
17 |
18 | .row {
19 | grid-column-start: 1;
20 | border-radius: 0 0.2em 0.2em 0;
21 | span {
22 | transform: rotate(-90deg);
23 | overflow: visible !important;
24 | }
25 | }
26 |
27 | .config {
28 | grid-row-start: 1;
29 | grid-column-start: 1;
30 | border-radius: 0 0 0.2em 0;
31 | }
32 |
33 | .col,
34 | .row,
35 | .config {
36 | @extend %tappable;
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | background: #4c4a56;
41 | color: #a1c7d0;
42 | overflow: hidden;
43 | transition-duration: 200ms;
44 | transition-property: background, color;
45 | span {
46 | font-size: 1.1em;
47 | font-weight: 700;
48 | white-space: nowrap;
49 | text-overflow: ellipsis;
50 | overflow: hidden;
51 | }
52 | svg {
53 | width: 2em;
54 | height: 2em;
55 | transition-duration: 200ms;
56 | transition-property: fill;
57 | }
58 | &.active {
59 | background: #ffad8a;
60 | color: #242424;
61 | }
62 | &:not(.active):hover {
63 | color: #ff9b70;
64 | }
65 | }
66 |
67 | .spring {
68 | animation-name: glowing;
69 | animation-duration: 10s;
70 | animation-iteration-count: infinite;
71 | animation-timing-function: cubic-bezier(0, 1.13, 0.83, 1.11);
72 | }
73 |
74 | @keyframes glowing {
75 | 0% {
76 | box-shadow: 0 0 0 0 rgba(47, 227, 255, 0.94);
77 | }
78 | 20% {
79 | box-shadow: 0 0 0px 2em rgba(0, 0, 0, 0);
80 | }
81 | }
82 |
83 | .highlighter {
84 | grid-area: 1 / 1 / 1 / 1;
85 | pointer-events: none;
86 | opacity: 0;
87 | transition-duration: 120ms;
88 | transition-property: opacity;
89 | }
90 |
--------------------------------------------------------------------------------
/src/items/Item/index.tsx:
--------------------------------------------------------------------------------
1 | import { lift, F, classes } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { Draggable } from 'react-beautiful-dnd'
4 | import { actionsItems } from '../../_generic/actions'
5 | import { IItem } from '../../_generic/types/common'
6 | import { Btn } from '../../_generic/ui/Btn'
7 | import { selectedID$ } from '../state'
8 | import $ from './style.scss'
9 |
10 | type TProps = {
11 | index: number
12 | item: IItem
13 | }
14 |
15 | export const Item = lift(({ index, item }: TProps) => {
16 | const { id, name, isHidden } = item
17 | const del = actionsItems.del(index)
18 | const select = () => actionsItems.select(id)
19 | const activeClass$ = selectedID$.view((sid) => sid === id && $.active)
20 | const toggleVision = actionsItems.toggleVision(index)
21 | const onMouseEnter = actionsItems.highlight(name)
22 | const onMouseLeave = actionsItems.dropHighlight
23 | return (
24 |
25 | {(provided, snapshot) => (
26 |
34 |
35 |
41 |
42 |
43 | {name}
44 |
45 |
46 |
47 |
48 |
49 | )}
50 |
51 | )
52 | })
53 |
--------------------------------------------------------------------------------
/src/items/Item/style.scss:
--------------------------------------------------------------------------------
1 | .item {
2 | display: flex;
3 | align-items: center;
4 | margin-top: 0.2em;
5 | user-select: none;
6 | border-radius: 0.2em;
7 | transition-duration: 120ms;
8 | transition-property: background;
9 |
10 | .name {
11 | flex-grow: 1;
12 | padding: 0.65em 0;
13 | word-break: break-all;
14 | }
15 |
16 | .vision {
17 | flex-shrink: 0;
18 | font-size: 0.7em;
19 | }
20 |
21 | .remove {
22 | flex-shrink: 0;
23 | font-size: 0.8em;
24 | opacity: 0;
25 | transition-duration: 120ms;
26 | transition-property: opacity;
27 | }
28 |
29 | &.active {
30 | background: rgba(226, 227, 232, 0.17);
31 | }
32 |
33 | &:hover {
34 | background: rgba(226, 227, 232, 0.17);
35 | .remove {
36 | opacity: 1;
37 | }
38 | }
39 |
40 | &.dragging {
41 | background: #fff;
42 | color: #2f2f2f;
43 | box-shadow: 0 0 10px #000;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/items/index.tsx:
--------------------------------------------------------------------------------
1 | import { F, reactiveList } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'
4 | import { shareReplay } from 'rxjs/operators'
5 | import { actionsItems } from '../_generic/actions'
6 | import { Btn } from '../_generic/ui/Btn'
7 | import { Item } from './Item'
8 | import { getItemByIndex, itemsIDs$ } from './state'
9 | import $ from './style.scss'
10 |
11 | const list$ = reactiveList(itemsIDs$, (index) => {
12 | const item$ = getItemByIndex(index)
13 | return
14 | }).pipe(
15 | // shareReplay need to be here to do not rerender the list while dragging
16 | shareReplay(1)
17 | )
18 |
19 | export const Items = () => (
20 |
21 |
22 |
23 |
24 | {(provided) => (
25 |
26 | {list$}
27 | {provided.placeholder}
28 |
29 | )}
30 |
31 |
32 |
33 | )
34 |
35 | const onDragEnd = (result: DropResult) => {
36 | if (result.destination) {
37 | actionsItems.reorder({
38 | currentIndex: result.source.index,
39 | nextIndex: result.destination.index,
40 | })
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/items/state.ts:
--------------------------------------------------------------------------------
1 | import { Atom, Lens } from '@grammarly/focal'
2 | import { getColor } from '../_generic/supply/get-color'
3 | import { defaultItem, IItem } from '../_generic/types/common'
4 | import { actionsItems } from '../_generic/actions'
5 |
6 | type TState = {
7 | selectedID: null | string
8 | items: IItem[]
9 | }
10 |
11 | const state$ = Atom.create({
12 | selectedID: null,
13 | items: [],
14 | })
15 |
16 | export const selectedID$ = state$.lens('selectedID')
17 |
18 | export const items$ = state$.lens('items')
19 |
20 | export const itemsIDs$ = items$.view((v) => v.map((_, index) => index))
21 |
22 | export const itemsReversed$ = items$.lens(
23 | Lens.create((items: IItem[]) => [...items].reverse(), (nextItems) => [...nextItems].reverse())
24 | )
25 |
26 | export const getItemByIndex = (index: number) =>
27 | items$.lens(Lens.index(index)).lens(Lens.withDefault(defaultItem))
28 |
29 | export const selectedItem$ = state$.lens(
30 | Lens.create(
31 | ({ selectedID, items }: TState) => {
32 | return (selectedID && items.find(({ id }) => id === selectedID)) || createItem()
33 | },
34 | (item, { items }) => {
35 | const id = item.id
36 | const index = items.findIndex((i) => i.id === id)
37 | const nextItems = [...items]
38 | if (index === -1) {
39 | nextItems.unshift(item)
40 | } else {
41 | nextItems.splice(index, 1, item)
42 | }
43 | return { selectedID: id, items: nextItems }
44 | }
45 | )
46 | )
47 |
48 | let counter = 1
49 | const createItem = () => {
50 | const id = `item-${counter++}`
51 | const newItem: IItem = {
52 | ...defaultItem,
53 | id,
54 | name: id,
55 | color: getColor(),
56 | }
57 | return newItem
58 | }
59 |
60 | actionsItems.add.$.subscribe(() => {
61 | state$.modify(({ items }) => {
62 | const item = createItem()
63 | return {
64 | selectedID: item.id,
65 | items: [item, ...items],
66 | }
67 | })
68 | })
69 |
70 | actionsItems.del.$.subscribe((index) => {
71 | state$.modify(({ selectedID, items }) => {
72 | const nextItems = [...items]
73 | const deletedID = nextItems.splice(index, 1)[0].id
74 | return {
75 | selectedID: selectedID === deletedID ? null : selectedID,
76 | items: nextItems,
77 | }
78 | })
79 | })
80 |
81 | actionsItems.reorder.$.subscribe(({ currentIndex, nextIndex }) => {
82 | items$.modify((items) => {
83 | if (currentIndex === nextIndex) {
84 | return items
85 | } else {
86 | const nextItems = [...items]
87 | const [removed] = nextItems.splice(currentIndex, 1)
88 | nextItems.splice(nextIndex, 0, removed)
89 | return nextItems
90 | }
91 | })
92 | })
93 |
94 | actionsItems.select.$.subscribe((itemID) => {
95 | selectedID$.set(itemID)
96 | })
97 |
98 | actionsItems.toggleVision.$.subscribe((index) => {
99 | items$.modify((items) => {
100 | const item = items[index]
101 | const nextItems = [...items]
102 | nextItems.splice(index, 1, { ...item, isHidden: !item.isHidden })
103 | return nextItems
104 | })
105 | })
106 |
--------------------------------------------------------------------------------
/src/items/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .container {
4 | display: flex;
5 | flex-direction: column;
6 | padding: 0.2em;
7 | }
8 |
9 | .list {
10 | overflow-y: auto;
11 | min-height: 10em;
12 | }
13 |
--------------------------------------------------------------------------------
/src/kit-sections/Characters.tsx:
--------------------------------------------------------------------------------
1 | import { Slider } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const ref = 'ab.cde fghi!jkl mno.pqrs tuvw?xyz'
7 | const makeTextLine = (characters: number) => {
8 | let res = ''
9 | const length = ref.length
10 | while (characters--) {
11 | res += ref.charAt(Math.floor(Math.random() * length))
12 | }
13 | return res.trim()
14 | }
15 |
16 | export const Characters = ({ v$ }: { v$: Atom }) => {
17 | return (
18 |
19 | {(v) => {
20 | const amount$ = Atom.create(v.length)
21 | return (
22 |
23 |
24 | {(amount) => {
25 | return (
26 | amount$.set(nextA)}
29 | onRelease={(nextV) => v$.set(makeTextLine(nextV))}
30 | value={amount}
31 | min={0}
32 | max={2000}
33 | stepSize={10}
34 | />
35 | )
36 | }}
37 |
38 |
39 | )
40 | }}
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/src/kit-sections/Check.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { MapElement } from '../_generic/ui/MapElement'
3 | import { Atom } from '@grammarly/focal'
4 | import { Switch } from '@blueprintjs/core'
5 |
6 | export const Check = ({ v$, label }: { v$: Atom; label?: string }) => {
7 | return (
8 |
9 | {(v) => (
10 | v$.set((e.target as HTMLInputElement).checked)}
14 | />
15 | )}
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/src/kit-sections/Color/index.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, F } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { COLORS } from '../../_generic/supply/get-color'
4 | import $ from './style.scss'
5 |
6 | export const Color = ({ v$ }: { v$: Atom }) => {
7 | return (
8 |
9 | {COLORS.map((color) => (
10 | `${color} 0px 0px 0px ${v === color ? 0.2 : 1}em inset`
16 | ),
17 | }}
18 | onClick={() => v$.set(color)}
19 | />
20 | ))}
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/src/kit-sections/Color/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .container {
4 | display: grid;
5 | grid-template-columns: repeat(6, 2em);
6 | grid-template-rows: repeat(3, 2em);
7 | grid-gap: 0.4em 0.4em;
8 | justify-content: space-between;
9 | }
10 |
11 | .color {
12 | @extend %tappable;
13 | background: transparent;
14 | height: 100%;
15 | width: 100%;
16 | border-radius: 50%;
17 | transition: box-shadow 100ms ease 0s;
18 | }
19 |
--------------------------------------------------------------------------------
/src/kit-sections/GridSettings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { gridSettings$ } from '../grid/state'
3 | import { Check } from './Check'
4 | import { Control, Section } from './Kit'
5 | import { Size } from './Size'
6 | import { Select } from './Select'
7 | import { ShowIf } from '../_generic/ui/ShowIf'
8 | import {
9 | tipContainerFlexGrow,
10 | tipInlineGrid,
11 | tipJustifyItems,
12 | tipAlignItems,
13 | tipJustifyContent,
14 | tipAlignContent,
15 | tipAutoFlow,
16 | tipGap,
17 | } from '../tips'
18 |
19 | const IAJ_OPTIONS = ['start', 'end', 'center', 'stretch']
20 | const CAJ_OPTIONS = [...IAJ_OPTIONS, 'space-around', 'space-between', 'space-evenly']
21 | const AF_OPTIONS = ['row', 'row dense', 'column', 'column dense']
22 |
23 | export const GridSettings = () => {
24 | const isGrow$ = gridSettings$.lens('isGrow')
25 | return (
26 | <>
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {() => (
36 | <>
37 |
38 |
39 |
40 |
41 |
42 |
43 | >
44 | )}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
76 | >
77 | )
78 | }
79 |
--------------------------------------------------------------------------------
/src/kit-sections/ItemSettings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { selectedItem$ } from '../items/state'
3 | import { tipAlignSelf, tipJustifySelf } from '../tips'
4 | import { Characters } from './Characters'
5 | import { Color } from './Color'
6 | import { Control, Section } from './Kit'
7 | import { Location } from './Location'
8 | import { Name } from './Name'
9 | import { Select } from './Select'
10 | import { Size } from './Size'
11 |
12 | const SAJ_OPTIONS = ['start', 'end', 'center', 'stretch']
13 |
14 | export const ItemSettings = () => {
15 | return (
16 | <>
17 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 | `${rowStart} / ${colStart} / ${rowEnd} / ${colEnd}`
35 | )}
36 | >
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | >
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/src/kit-sections/Kit/index.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, classes, F } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { Observable } from 'rxjs'
4 | import { Btn } from '../../_generic/ui/Btn'
5 | import { Overlay } from '../../_generic/ui/Overlay'
6 | import { ShowIf } from '../../_generic/ui/ShowIf'
7 | import $ from './style.scss'
8 |
9 | type TSectionProps = {
10 | title?: string
11 | subtitle?: Observable
12 | children: React.ReactNode
13 | }
14 |
15 | export const Section = ({ title, subtitle, children }: TSectionProps) => {
16 | if (title) {
17 | const isOpen$ = Atom.create(false)
18 | return (
19 |
20 |
v && $.active))}
22 | onClick={() => isOpen$.modify((v) => !v)}
23 | >
24 | {title}
25 | {subtitle && {subtitle}}
26 |
27 |
28 | {() => {children}
}
29 |
30 |
31 | )
32 | } else {
33 | return (
34 |
37 | )
38 | }
39 | }
40 |
41 | type TControlProps = {
42 | label?: string
43 | tip?: () => React.ReactNode
44 | children: React.ReactNode
45 | }
46 |
47 | export const Control = ({ children, label, tip }: TControlProps) => {
48 | let tipCtrl = null
49 | if (tip) {
50 | const isOpen$ = Atom.create(false)
51 | const props = {
52 | isOpen$,
53 | content: tip,
54 | stopPropagation: true,
55 | position: ['top', 'bottom'] as any,
56 | close: () => isOpen$.set(false),
57 | }
58 | tipCtrl = (
59 | v && $.active))}>
60 |
61 | isOpen$.modify((v) => !v)} />
62 |
63 |
64 | )
65 | }
66 | return (
67 |
68 |
69 | {Boolean(label) && }
70 | {children}
71 |
72 | {tipCtrl}
73 |
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/src/kit-sections/Kit/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .section {
4 | flex-shrink: 0;
5 | display: flex;
6 | flex-direction: column;
7 | margin-top: 0.4em;
8 | background: #3b3c4c;
9 | .content {
10 | padding: 1em;
11 | }
12 | .header {
13 | @extend %tappable;
14 | position: relative;
15 | padding: 0.5em 1em;
16 | background: #2e2f40;
17 | transition-duration: 120ms;
18 | transition-property: background, transform;
19 | .title {
20 | color: #7b7690;
21 | font-size: 1.1em;
22 | font-weight: 700;
23 | }
24 | .subtitle {
25 | font-size: 0.8em;
26 | font-weight: 700;
27 | color: #7e90a0;
28 | }
29 | & + .content {
30 | box-shadow: inset 0 1px 0 0 #000, inset 0 2px 0 0 #504c65;
31 | }
32 | &:before {
33 | content: '';
34 | display: block;
35 | position: absolute;
36 | left: 0.2em;
37 | top: 1.15em;
38 | width: 0;
39 | height: 0;
40 | font-size: 0.9em;
41 | border-width: 0.3076923076923077em 0.3076923076923077em 0;
42 | border-style: solid;
43 | border-color: #858296 transparent transparent;
44 | transform: rotate(-90deg);
45 | transition-duration: 120ms;
46 | transition-property: border, transform;
47 | }
48 | &.active {
49 | .title {
50 | color: #9695a2;
51 | }
52 | &:before {
53 | border-color: #a59eb9 transparent transparent;
54 | transform: rotate(0);
55 | }
56 | }
57 | &:hover {
58 | background: #33344a;
59 | }
60 | }
61 | }
62 |
63 | .control {
64 | display: flex;
65 | & + & {
66 | margin-top: 1em;
67 | }
68 | label {
69 | display: block;
70 | margin: 0 0 0.4em 0;
71 | font-size: 0.9em;
72 | font-weight: 700;
73 | color: #7e90a0 !important;
74 | }
75 | h1,
76 | h2,
77 | h3 {
78 | margin: 0;
79 | }
80 | .ctrl {
81 | flex-grow: 1;
82 | align-self: flex-end;
83 | display: flex;
84 | flex-direction: column;
85 | }
86 | .tip {
87 | flex-shrink: 0;
88 | align-self: flex-start;
89 | font-size: 0.65em;
90 | color: #7b7690;
91 | &.active,
92 | &:hover {
93 | color: #ff9b70;
94 | }
95 | }
96 | }
97 |
98 | .btnPanel {
99 | display: flex;
100 | svg {
101 | fill: #8ebec7;
102 | transition-duration: 120ms;
103 | transition-property: fill;
104 | }
105 | button:hover svg {
106 | fill: #ff9b70;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/kit-sections/Location.tsx:
--------------------------------------------------------------------------------
1 | import { ControlGroup, HTMLSelect, NumericInput } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const AUTO = 'auto'
7 | const SPAN = 'span'
8 | const LINE = 'line'
9 | const MB_OPTIONS = [LINE, SPAN, AUTO]
10 |
11 | export const Location = ({ v$ }: { v$: Atom }) => {
12 | return (
13 |
14 | {(v) => {
15 | let isSpan: boolean
16 | let num: number
17 | let option: string
18 | if (v === AUTO) {
19 | isSpan = false
20 | num = 0
21 | option = AUTO
22 | } else if (typeof v === 'string') {
23 | isSpan = true
24 | num = Number(v.replace(SPAN, ''))
25 | option = SPAN
26 | } else {
27 | isSpan = false
28 | num = v
29 | option = LINE
30 | }
31 | return (
32 |
33 |
39 | v$.set(
40 | !nextNum || nextNum < 1
41 | ? AUTO
42 | : isSpan
43 | ? `${SPAN} ${nextNum}`
44 | : nextNum.toString()
45 | )
46 | }
47 | min={0}
48 | selectAllOnFocus={true}
49 | clampValueOnBlur={true}
50 | style={{ width: '2em' }}
51 | />
52 | {
55 | const nextOption = e.target.value
56 | const nextNum = num || 1
57 | v$.set(
58 | nextOption === AUTO
59 | ? AUTO
60 | : nextOption === SPAN
61 | ? `${SPAN} ${nextNum}`
62 | : nextNum
63 | )
64 | }}
65 | value={option}
66 | />
67 |
68 | )
69 | }}
70 |
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/src/kit-sections/Name.tsx:
--------------------------------------------------------------------------------
1 | import { EditableText, H3 } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const identify = (ugly: string) => {
7 | const step1 = ugly.replace(/^[^-_a-zA-Z]+/, '').replace(/^-(?:[-0-9]+)/, '-')
8 | const step2 = step1 && step1.replace(/[^-_a-zA-Z0-9]+/g, '-')
9 | return step2
10 | }
11 |
12 | export const Name = ({ v$ }: { v$: Atom }) => {
13 | return (
14 |
15 | {(v) => {
16 | return (
17 |
18 | {
25 | nextName = identify(nextName)
26 | if (nextName) {
27 | v$.set(nextName)
28 | }
29 | }}
30 | />
31 |
32 | )
33 | }}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/kit-sections/Repeat.tsx:
--------------------------------------------------------------------------------
1 | import { ControlGroup, HTMLSelect, NumericInput } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const DISABLED = 'disabled'
7 | const INTEGER = 'integer'
8 | const AUTO_FILL = 'auto-fill'
9 | const AUTO_FIT = 'auto-fit'
10 |
11 | const MB_OPTIONS = [DISABLED, INTEGER, AUTO_FILL, AUTO_FIT]
12 |
13 | export const Repeat = ({ v$ }: { v$: Atom }) => {
14 | return (
15 |
16 | {(repeat) => {
17 | const isNumber = typeof repeat === 'number'
18 | const option = isNumber ? (repeat ? INTEGER : DISABLED) : repeat
19 | return (
20 |
21 | v$.set(v)}
28 | selectAllOnFocus={true}
29 | clampValueOnBlur={true}
30 | onBlur={() => v$.modify((v) => (v > 1 ? v : 0))}
31 | style={{ width: '2em' }}
32 | />
33 | {
36 | const nextOption = e.target.value
37 | v$.set(
38 | nextOption === DISABLED ? 0 : nextOption === INTEGER ? 2 : nextOption
39 | )
40 | }}
41 | value={option}
42 | />
43 |
44 | )
45 | }}
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/src/kit-sections/Select.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLSelect } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const NONE = 'default'
7 |
8 | export const Select = ({ v$, options }: { v$: Atom; options: string[] }) => {
9 | return (
10 |
11 | {(v) => {
12 | const allOptions = [NONE, ...options]
13 | const value = v === null || !options.includes(v) ? NONE : v
14 | return (
15 | {
19 | const nextValue = e.target.value
20 | v$.set(nextValue === NONE ? null : nextValue)
21 | }}
22 | value={value}
23 | />
24 | )
25 | }}
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/kit-sections/Size.tsx:
--------------------------------------------------------------------------------
1 | import { ControlGroup, HTMLSelect, NumericInput } from '@blueprintjs/core'
2 | import { Atom } from '@grammarly/focal'
3 | import * as React from 'react'
4 | import { MapElement } from '../_generic/ui/MapElement'
5 |
6 | const FR = 'fr'
7 |
8 | const PX = 'px'
9 | const EM = 'em'
10 | const REM = 'rem'
11 | const LENGTH = [PX, EM, REM]
12 |
13 | const VW = 'vw'
14 | const VH = 'vh'
15 | const PERCENT = '%'
16 | const RANGED = [VW, VH, PERCENT]
17 |
18 | const MIN_C = 'min-content'
19 | const MAX_C = 'max-content'
20 | const SPECIAL = [MIN_C, MAX_C]
21 |
22 | const AUTO = 'auto'
23 | const DEFAULT = 'default'
24 |
25 | const MB_OPTIONS = [...LENGTH, ...RANGED]
26 |
27 | export const Size = ({
28 | v$,
29 | special = false,
30 | flex = false,
31 | }: {
32 | v$: Atom
33 | special?: boolean
34 | flex?: boolean
35 | }) => {
36 | const D_OPTION = flex ? AUTO : DEFAULT
37 | const OPTIONS = [D_OPTION, ...MB_OPTIONS]
38 | if (special) {
39 | OPTIONS.push(...SPECIAL)
40 | }
41 | if (flex) {
42 | OPTIONS.splice(1, 0, FR)
43 | }
44 | return (
45 |
46 | {(v) => {
47 | const isD = v === null
48 | const NAN = isD || (special && SPECIAL.includes(v as string))
49 | const num = NAN ? 0 : parseFloat(v as string)
50 | const option = NAN ? v || D_OPTION : (v as string).replace(String(num), '')
51 | return (
52 |
53 | v$.set(nextNum + (NAN ? EM : option))}
59 | selectAllOnFocus={true}
60 | clampValueOnBlur={true}
61 | style={{ width: '2em' }}
62 | />
63 | {
66 | const nextOption = e.target.value
67 | v$.set(
68 | nextOption === D_OPTION
69 | ? null
70 | : special && SPECIAL.includes(nextOption)
71 | ? nextOption
72 | : num + nextOption
73 | )
74 | }}
75 | value={option}
76 | />
77 |
78 | )
79 | }}
80 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/src/kit-sections/TrackSettings.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, Lens, ReadOnlyAtom } from '@grammarly/focal'
2 | import * as React from 'react'
3 | import { HLAddCol, HLAddRow, HLLeave, HLRemoveCol, HLRemoveRow } from '../grid/Highlighter'
4 | import { tipFitContent, tipMinmax, tipRepeat } from '../tips'
5 | import { actionsGrid } from '../_generic/actions'
6 | import { ITrack } from '../_generic/types/common'
7 | import { Btn } from '../_generic/ui/Btn'
8 | import { ShowIf } from '../_generic/ui/ShowIf'
9 | import { Check } from './Check'
10 | import { Control, Section } from './Kit'
11 | import $ from './Kit/style.scss'
12 | import { Repeat } from './Repeat'
13 | import { Size } from './Size'
14 |
15 | type TProps = {
16 | track$: Atom
17 | position$: ReadOnlyAtom<[number, number]>
18 | row: boolean
19 | repeat: boolean
20 | }
21 |
22 | export const TrackSettings = ({ track$, position$, row, repeat }: TProps) => {
23 | const value$ = track$.lens('value')
24 | const min$ = track$.lens('min')
25 | const max$ = track$.lens('max')
26 | const minmax$ = track$.lens(makeMonoLens('minmax', 'fitContent'))
27 | const fitContent$ = track$.lens(makeMonoLens('fitContent', 'minmax'))
28 | const repeat$ = track$.lens('repeat')
29 | const addBefore = row ? actionsGrid.addBeforeRow : actionsGrid.addBeforeCol
30 | const addAfter = row ? actionsGrid.addAfterRow : actionsGrid.addAfterCol
31 | const remove = row ? actionsGrid.removeRow : actionsGrid.removeCol
32 | const HLAdd = row ? HLAddRow : HLAddCol
33 | const HLRemove = row ? HLRemoveRow : HLRemoveCol
34 | return (
35 | <>
36 |
37 |
38 |
39 | HLAdd(position$.get()[0])}
43 | onMouseOut={HLLeave}
44 | onClick={addBefore(track$)}
45 | />
46 | HLRemove(position$.get())}
50 | onMouseOut={HLLeave}
51 | onClick={remove(track$)}
52 | />
53 | HLAdd(position$.get()[1])}
57 | onMouseOut={HLLeave}
58 | onClick={addAfter(track$)}
59 | />
60 |
61 |
62 |
63 |
64 |
65 | {() => (
66 |
67 |
68 |
69 | )}
70 |
71 |
72 | {() => (
73 | <>
74 |
75 |
76 |
77 |
78 |
79 |
80 | >
81 | )}
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | {repeat && (
91 |
96 | )}
97 | >
98 | )
99 | }
100 |
101 | const makeMonoLens = (orig: string, reflection: string) =>
102 | Lens.create(
103 | (track: ITrack) => track[orig] as boolean,
104 | (value, track) => ({ ...track, [orig]: value, [reflection]: track[reflection] && false })
105 | )
106 |
--------------------------------------------------------------------------------
/src/preview/index.tsx:
--------------------------------------------------------------------------------
1 | import { Atom, classes, F, lift } from '@grammarly/focal'
2 | import cc from 'classcat'
3 | import * as React from 'react'
4 | import { gridSettings$ } from '../grid/state'
5 | import { itemsReversed$, selectedID$ } from '../items/state'
6 | import { actionsItems } from '../_generic/actions'
7 | import { defaultItem } from '../_generic/types/common'
8 | import { ReactiveList } from '../_generic/ui/ReactiveList'
9 | import { cssPure$, cssHighlighter$ } from './state'
10 | import $ from './style.scss'
11 |
12 | const CSS = lift(({ css, cssHighlighter }: { css: string; cssHighlighter: string }) => (
13 |
14 | ))
15 |
16 | const isGrowClass$ = gridSettings$.view(({ isGrow }) => isGrow && $.flexed)
17 |
18 | const items$ = itemsReversed$.view((items) => items.filter((item) => !item.isHidden))
19 |
20 | export const Preview = () => {
21 | return (
22 | <>
23 |
24 |
25 |
26 |
27 | {(item$, index) => {
28 | const id$ = item$.view('id')
29 | const select = () => actionsItems.select(id$.get())
30 | const activeClass$ = Atom.combine(
31 | id$,
32 | selectedID$,
33 | (id, sid) => id === sid && $.active
34 | )
35 | return (
36 |
42 | {item$.view('characters')}
43 |
44 | )
45 | }}
46 |
47 |
48 |
49 | >
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/src/preview/state.ts:
--------------------------------------------------------------------------------
1 | import { combineLatest, merge, Observable } from 'rxjs'
2 | import { shareReplay, startWith, map } from 'rxjs/operators'
3 | import { gridSettings$, explicitGrid$, implicitGrid$ } from '../grid/state'
4 | import { itemsReversed$ } from '../items/state'
5 | import { actionsItems } from '../_generic/actions'
6 | import { ITrack } from '../_generic/types/common'
7 | import { NTA } from '../_generic/supply/utils'
8 | import $ from '../get-the-code/style.scss'
9 |
10 | const span = (className: string) => (value: string) => `${value}`
11 | const line = span($.line)
12 | const hl1 = span($.hl1)
13 | const hl2 = span($.hl2)
14 | const hl3 = span($.hl3)
15 |
16 | const DIV = hl1('div')
17 | const CONTAINER_NAME = 'container'
18 |
19 | const trackSize = ({ value, min, max, minmax, fitContent, repeat }: ITrack) => {
20 | let size: string
21 | if (minmax) {
22 | size = `minmax(${NTA(min)}, ${NTA(max)})`
23 | } else if (fitContent) {
24 | size = `fit-content(${NTA(value)})`
25 | } else {
26 | size = NTA(value)
27 | }
28 | if (repeat) {
29 | size = `repeat(${repeat}, ${size})`
30 | }
31 | return size
32 | }
33 |
34 | const tracksHaveSize = (tracks: ITrack[]) => {
35 | return (
36 | Boolean(tracks.length) &&
37 | tracks.some((track) => track.minmax || Boolean(track.repeat) || Boolean(track.value))
38 | )
39 | }
40 |
41 | type TClass = { [rule: string]: string }
42 |
43 | const containerCSS$ = combineLatest(gridSettings$, explicitGrid$, implicitGrid$).pipe(
44 | map(
45 | ([
46 | {
47 | isInline,
48 | isGrow,
49 | width,
50 | height,
51 | colGap,
52 | rowGap,
53 | justifyItems,
54 | alignItems,
55 | justifyContent,
56 | alignContent,
57 | autoFlow,
58 | },
59 | { cols, rows },
60 | { cols: autoCols, rows: autoRows },
61 | ]) => {
62 | const rules: TClass = {
63 | display: isInline ? 'inline-grid' : 'grid',
64 | }
65 | if (!isGrow) {
66 | if (width) {
67 | rules.width = width
68 | }
69 | if (height) {
70 | rules.height = height
71 | }
72 | }
73 | if (tracksHaveSize(cols)) {
74 | rules['grid-template-columns'] = cols.map(trackSize).join(' ')
75 | }
76 | if (tracksHaveSize(rows)) {
77 | rules['grid-template-rows'] = rows.map(trackSize).join(' ')
78 | }
79 | if (tracksHaveSize(autoCols)) {
80 | rules['grid-auto-columns'] = autoCols.map(trackSize).join(' ')
81 | }
82 | if (tracksHaveSize(autoRows)) {
83 | rules['grid-auto-rows'] = autoRows.map(trackSize).join(' ')
84 | }
85 | if (colGap && rowGap) {
86 | rules['grid-gap'] = rowGap + ' ' + colGap
87 | } else if (colGap) {
88 | rules['grid-column-gap'] = colGap
89 | } else if (rowGap) {
90 | rules['grid-row-gap'] = rowGap
91 | }
92 | if (justifyItems && alignItems) {
93 | rules['place-items'] = alignItems + ' ' + justifyItems
94 | } else if (justifyItems) {
95 | rules['justify-items'] = justifyItems
96 | } else if (alignItems) {
97 | rules['align-items'] = alignItems
98 | }
99 | if (justifyContent && alignContent) {
100 | rules['place-content'] = alignContent + ' ' + justifyContent
101 | } else if (justifyContent) {
102 | rules['justify-content'] = justifyContent
103 | } else if (alignContent) {
104 | rules['align-content'] = alignContent
105 | }
106 | if (autoFlow) {
107 | rules['grid-auto-flow'] = autoFlow
108 | }
109 | return rules
110 | }
111 | ),
112 | shareReplay(1)
113 | )
114 |
115 | const itemsCSS$ = itemsReversed$.pipe(
116 | map((items) => {
117 | return items.reduce<{ [name: string]: TClass }>((acc, item) => {
118 | const {
119 | name,
120 | width,
121 | height,
122 | colStart,
123 | rowStart,
124 | colEnd,
125 | rowEnd,
126 | justifySelf,
127 | alignSelf,
128 | } = item
129 | const rules: TClass = {
130 | 'grid-area': `${rowStart} / ${colStart} / ${rowEnd} / ${colEnd}`,
131 | }
132 | if (width) {
133 | rules.width = width
134 | }
135 | if (height) {
136 | rules.height = height
137 | }
138 | if (justifySelf && alignSelf) {
139 | rules['place-self'] = alignSelf + ' ' + justifySelf
140 | } else if (justifySelf) {
141 | rules['justify-self'] = justifySelf
142 | } else if (alignSelf) {
143 | rules['align-self'] = alignSelf
144 | }
145 | acc[name] = rules
146 | return acc
147 | }, {})
148 | }),
149 | shareReplay(1)
150 | )
151 |
152 | export const cssHighlighter$: Observable = merge(
153 | actionsItems.dropHighlight.$.map(() => ''),
154 | actionsItems.highlight.$.map(
155 | (name) => `.${name} {
156 | z-index: 99;
157 | box-shadow:
158 | inset 0px 0px 0px 2px #fff,
159 | inset 0px 0px 1em 2px #000;
160 | }`
161 | )
162 | ).pipe(
163 | startWith(''),
164 | shareReplay(1)
165 | )
166 |
167 | export const cssPure$ = combineLatest(containerCSS$, itemsCSS$, (containerCSS, itemsCSS) =>
168 | [
169 | toPureCSS(CONTAINER_NAME, containerCSS),
170 | ...Object.entries(itemsCSS).map(([name, itemCSS]) => toPureCSS(name, itemCSS)),
171 | ].join('\n\n')
172 | )
173 |
174 | const toPureCSS = (className: string, rules: TClass) => {
175 | return [
176 | `.${className} {`,
177 | ...Object.entries(rules).map(([name, rule]) => `\t${name}: ${rule};`),
178 | '}',
179 | ].join('\n')
180 | }
181 |
182 | export const css$ = combineLatest(containerCSS$, itemsCSS$, (containerCSS, itemsCSS) =>
183 | [
184 | ...toCSS(CONTAINER_NAME, containerCSS),
185 | ...Object.entries(itemsCSS).reduce(
186 | (res, [name, itemCSS]) => res.concat('', toCSS(name, itemCSS)),
187 | []
188 | ),
189 | ]
190 | .map(line)
191 | .join('\n')
192 | )
193 |
194 | export const styledComponents$ = combineLatest(containerCSS$, itemsCSS$, (containerCSS, itemsCSS) =>
195 | [
196 | `${hl2('import')} styled ${hl2('from')} ${hl3("'styled-components'")}`,
197 | '',
198 | ...toStyledComponent(CONTAINER_NAME, containerCSS),
199 | ...Object.entries(itemsCSS).reduce(
200 | (res, [name, itemCSS]) => res.concat('', toStyledComponent(name, itemCSS)),
201 | []
202 | ),
203 | ]
204 | .map(line)
205 | .join('\n')
206 | )
207 |
208 | export const html$ = itemsReversed$.map((items) => {
209 | return [
210 | `<${DIV} class=${hl3(`"${CONTAINER_NAME}"`)}>`,
211 | ...items.map(
212 | ({ name }) => `\t<${DIV} class=${hl3(`"${name}"`)}></${DIV}>`
213 | ),
214 | `</${DIV}>`,
215 | ]
216 | .map(line)
217 | .join('\n')
218 | })
219 |
220 | export const jsxPlain$ = itemsReversed$.map((items) => {
221 | return [
222 | `<${DIV} className=${hl3(`"${CONTAINER_NAME}"`)}>`,
223 | ...items.map(
224 | ({ name }) => `\t<${DIV} className=${hl3(`"${name}"`)}></${DIV}>`
225 | ),
226 | `</${DIV}>`,
227 | ]
228 | .map(line)
229 | .join('\n')
230 | })
231 |
232 | export const jsxCSSModules$ = itemsReversed$.map((items) => {
233 | return [
234 | `${hl2('import')} $ ${hl2('from')} ${hl3("'./style.css'")}`,
235 | '',
236 | `<${DIV} className={$.${hl3(CONTAINER_NAME)}}>`,
237 | ...items.map(({ name }) => {
238 | const className = name.includes('-') ? `[${hl3(`'${name}'`)}]` : `.${hl3(name)}`
239 | return `\t<${DIV} className={$${className}}></${DIV}>`
240 | }),
241 | `</${DIV}>`,
242 | ]
243 | .map(line)
244 | .join('\n')
245 | })
246 |
247 | const toCSS = (className: string, rules: TClass) => {
248 | return [
249 | `.${hl1(className)} ${hl2('{')}`,
250 | ...Object.entries(rules).map(([name, rule]) => `\t${name}: ${hl3(rule)};`),
251 | hl2('}'),
252 | ]
253 | }
254 |
255 | const toStyledComponent = (className: string, rules: TClass) => {
256 | return [
257 | `${hl2('const')} ${hl1(toPascalCase(className))} = styled.div${hl3('`')}`,
258 | ...Object.entries(rules).map(([name, rule]) => `\t${name}: ${hl3(rule)};`),
259 | hl3('`'),
260 | ]
261 | }
262 |
263 | const toPascalCase = (className: string) => {
264 | return className
265 | .split('-')
266 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
267 | .join('')
268 | }
269 |
--------------------------------------------------------------------------------
/src/preview/style.scss:
--------------------------------------------------------------------------------
1 | @import 'utils';
2 |
3 | .preview {
4 | flex-grow: 1;
5 | &.flexed {
6 | display: flex;
7 | }
8 | }
9 |
10 | .container {
11 | flex-grow: 1;
12 | }
13 |
14 | .guide {
15 | background: rgba(255, 255, 255, 0.1);
16 | }
17 |
18 | .item {
19 | @extend %tappable;
20 | font-size: 1.2em;
21 | color: #000;
22 | word-break: break-all;
23 | transition-property: all;
24 | transition-duration: 120ms;
25 |
26 | &.active {
27 | box-shadow: inset 0 0 0 2px #fff, inset 0 0 0 3px #000;
28 | background-image: url('data:image/svg+xml;charset=utf-8,');
29 | background-size: 20px 20px;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/tips/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import $ from './style.scss'
3 |
4 | export const tipAlignSelf = () => (
5 |
6 | Aligns a grid item inside a cell along the block (column) axis (as opposed to{' '}
7 | justify-self
which aligns along the inline (row) axis). This value
8 | applies to the content inside a single grid item.
9 |
10 | )
11 |
12 | export const tipJustifySelf = () => (
13 |
14 | Aligns a grid item inside a cell along the inline (row) axis (as opposed to{' '}
15 | align-self
which aligns along the block (column) axis). This value
16 | applies to a grid item inside a single cell.
17 |
18 | )
19 |
20 | export const tipGap = () => (
21 |
22 | Specifies the size of the grid lines. You can think of it like setting the width of the
23 | gutters between the columns/rows.
24 |
25 | )
26 |
27 | export const tipAutoFlow = () => (
28 |
29 | If you have grid items that you don't explicitly place on the grid, the{' '}
30 | auto-placement algorithm kicks in to automatically place the items. This property
31 | controls how the auto-placement algorithm works.
32 |
33 | dense
- tells the auto-placement algorithm to attempt to fill in holes earlier in
34 | the grid if smaller items come up later.
35 |
36 | )
37 |
38 | export const tipAlignContent = () => (
39 |
40 | Sometimes the total size of your grid might be less than the size of its grid container. This
41 | could happen if all of your grid items are sized with non-flexible units like px
.
42 | In this case you can set the alignment of the grid within the grid container. This property
43 | aligns the grid along the block (column) axis (as opposed to{' '}
44 | justify-content
which aligns the grid along the inline (row) axis).
45 |
46 | )
47 |
48 | export const tipJustifyContent = () => (
49 |
50 | Sometimes the total size of your grid might be less than the size of its grid container. This
51 | could happen if all of your grid items are sized with non-flexible units like px
.
52 | In this case you can set the alignment of the grid within the grid container. This property
53 | aligns the grid along the inline (row) axis (as opposed to align-content
{' '}
54 | which aligns the grid along the block (column) axis).
55 |
56 | )
57 |
58 | export const tipAlignItems = () => (
59 |
60 | Aligns grid items along the block (column) axis (as opposed to{' '}
61 | justify-items
which aligns along the inline (row) axis). This value
62 | applies to all grid items inside the container.
63 |
64 | )
65 |
66 | export const tipJustifyItems = () => (
67 |
68 | Aligns grid items along the inline (row) axis (as opposed to align-items
{' '}
69 | which aligns along the block (column) axis). This value applies to all grid items
70 | inside the container.
71 |
72 | )
73 |
74 | export const tipInlineGrid = () => (
75 |
76 | Generates an inline-level grid. The inside of an inline-grid is formatted as a block-level
77 | grid container, and the element itself is formatted as an atomic inline-level box.
78 |
79 | )
80 |
81 | export const tipContainerFlexGrow = () => (
82 |
83 | If enabled, the container tends to fill the entire space of the preview
84 | window. If disabled, the size of the container can be customized manually.
85 |
86 | )
87 |
88 | export const tipRepeat = () => (
89 |
90 | The repeat()
CSS function represents a repeated fragment of the track list,
91 | allowing a large number of columns or rows that exhibit a recurring pattern to be written in a
92 | more compact form.
93 |
94 | )
95 |
96 | export const tipFitContent = () => (
97 |
98 | ⚠ This is an experimental technology
99 |
100 | Represents the formula min(max-content, max(auto, argument))
, which is calculated
101 | similar to auto
(i.e. minmax(auto, max-content)
), except that the
102 | track size is clamped at argument if it is greater than the auto
{' '}
103 | minimum.
104 |
105 | )
106 |
107 | export const tipMinmax = () => (
108 |
109 | The minmax()
CSS function defines a size range greater than or equal to{' '}
110 | min and less than or equal to max.
111 |
112 | )
113 |
114 | type TProps = {
115 | url?: string
116 | children: React.ReactNode
117 | }
118 |
119 | const Tip = ({ children, url }: TProps) => {
120 | return (
121 |
122 |
{children}
123 | {Boolean(url) && (
124 |
130 | )}
131 |
132 | )
133 | }
134 |
--------------------------------------------------------------------------------
/src/tips/style.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | width: 18em;
3 | padding: 0.8em;
4 | }
5 |
6 | .tip {
7 | font-size: 1.1em;
8 | em {
9 | color: #fff;
10 | font-weight: 700;
11 | }
12 | code {
13 | background-color: #3b3c4c;
14 | border: 1px solid #807c94;
15 | font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
16 | font-size: 0.75rem;
17 | line-height: 0.75rem;
18 | color: #ffccb6;
19 | padding: 1px 3px 2px;
20 | white-space: nowrap;
21 | font-variant-ligatures: none;
22 | tab-size: 4;
23 | border-radius: 0.3em;
24 | }
25 | }
26 |
27 | .url {
28 | margin-top: 0.8em;
29 | label {
30 | display: block;
31 | font-size: 0.9em;
32 | font-weight: 700;
33 | color: #8d8e9a;
34 | }
35 | a {
36 | font-size: 0.8em;
37 | padding: 0.2em 0;
38 | display: block;
39 | text-overflow: ellipsis;
40 | white-space: nowrap;
41 | overflow: hidden;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "target": "es6"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "lib": ["es6", "esnext", "dom"],
8 | "jsx": "react",
9 | "allowJs": true,
10 | "suppressImplicitAnyIndexErrors": true,
11 | "experimentalDecorators": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "sourceMap": false,
15 | "strictFunctionTypes": true,
16 | "plugins": [
17 | {
18 | "name": "typescript-plugin-css-modules",
19 | "options": {
20 | "customMatcher": "\\.(sc|c)ss$",
21 | "camelCase": true
22 | }
23 | }
24 | ]
25 | },
26 | "include": ["src/**/*.ts", "src/**/*.tsx"]
27 | }
28 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react"],
3 | "rules": {
4 | "indent": [true, "tabs", 2],
5 | "align": false,
6 | "array-type": [true, "array"],
7 | "arrow-parens": false,
8 | "interface-name": [true, "always-prefix"],
9 | "interface-over-type-literal": false,
10 | "jsx-curly-spacing": false,
11 | "jsx-no-bind": true,
12 | "jsx-no-lambda": false,
13 | "jsx-no-multiline-js": false,
14 | "jsx-boolean-value": ["always", { "never": ["personal"] }],
15 | "no-consecutive-blank-lines": [false],
16 | "no-empty": false,
17 | "no-empty-interface": false,
18 | "no-implicit-dependencies": false,
19 | "no-switch-case-fall-through": true,
20 | "no-object-literal-type-assertion": false,
21 | "no-submodule-imports": [true, "rxjs", "firebase", "@grammarly/focal"],
22 | "no-var-requires": false,
23 | "no-unnecessary-initializer": false,
24 | "max-classes-per-file": [false],
25 | "member-access": false,
26 | "member-ordering": false,
27 | "no-trailing-whitespace": [true, "ignore-template-strings"],
28 | "object-literal-key-quotes": false,
29 | "object-literal-shorthand": false,
30 | "object-literal-sort-keys": false,
31 | "one-line": false,
32 | "no-namespace": false,
33 | "one-variable-per-declaration": false,
34 | "prefer-conditional-expression": false,
35 | "only-arrow-functions": [true, "allow-named-functions"],
36 | "ordered-imports": false,
37 | "quotemark": [true, "single", "jsx-double", "avoid-escape"],
38 | "semicolon": false,
39 | "switch-default": true,
40 | "trailing-comma": false,
41 | "variable-name": [true, "ban-keywords"],
42 | "no-misused-new": false
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const TARGET = process.env.npm_lifecycle_event
2 |
3 | if (!TARGET || TARGET === 'dev:app') {
4 | module.exports = require('./config/webpack.dev')
5 | console.info('--> ./config/webpack.dev.js')
6 | } else if (TARGET === 'build:app') {
7 | module.exports = require('./config/webpack.prod')
8 | console.info('--> ./config/webpack.prod.js')
9 | }
10 |
--------------------------------------------------------------------------------