graphen 2.0.3 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css.js +1 -1
- package/dist/scripts.js +1 -1
- package/package.json +1 -1
- package/src/components/CoverEmpty/index.tsx +19 -0
- package/src/components/CoverEmpty/styles/_styles.scss +17 -0
- package/src/components/Icons/index.tsx +74 -0
- package/src/components/SegmentedControl/index.tsx +45 -0
- package/src/components/SegmentedControl/styles/_styles.scss +26 -0
- package/src/components/Stat/index.tsx +25 -0
- package/src/components/Stat/styles/_styles.scss +51 -0
- package/src/example/styles/_docs.scss +98 -0
- package/src/example.scss +3 -1
- package/src/example.tsx +185 -0
- package/src/index.ts +11 -2
- package/src/style.scss +3 -1
- package/src/components/Image/index.tsx +0 -26
- package/src/components/Image/styles/_styles.scss +0 -20
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { IconImg } from "../Icons";
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
6
|
+
className?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function CoverEmpty({ className = "" }: Props) {
|
|
10
|
+
const classes = classNames("gc-cover-empty", className);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<span className={classes}>
|
|
14
|
+
<IconImg className="gc-cover-empty__icon" />
|
|
15
|
+
</span>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default CoverEmpty;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.gc-cover-empty {
|
|
2
|
+
display: grid;
|
|
3
|
+
place-items: center;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
color: $color-silver;
|
|
7
|
+
|
|
8
|
+
&__icon {
|
|
9
|
+
width: 38px;
|
|
10
|
+
height: 38px;
|
|
11
|
+
stroke: currentcolor;
|
|
12
|
+
fill: none;
|
|
13
|
+
stroke-width: 1.4;
|
|
14
|
+
stroke-linecap: round;
|
|
15
|
+
stroke-linejoin: round;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type IconProps = {
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const IconSearch = ({ className }: IconProps) => (
|
|
8
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
9
|
+
<circle cx="11" cy="11" r="7" />
|
|
10
|
+
<path d="M21 21l-4.3-4.3" />
|
|
11
|
+
</svg>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const IconEdit = ({ className }: IconProps) => (
|
|
15
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
16
|
+
<path d="M12 20h9" />
|
|
17
|
+
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" />
|
|
18
|
+
</svg>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export const IconTrash = ({ className }: IconProps) => (
|
|
22
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
23
|
+
<path d="M3 6h18" />
|
|
24
|
+
<path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2" />
|
|
25
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const IconPlus = ({ className }: IconProps) => (
|
|
30
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
31
|
+
<path d="M12 5v14M5 12h14" />
|
|
32
|
+
</svg>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const IconRefresh = ({ className }: IconProps) => (
|
|
36
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
37
|
+
<path d="M21 12a9 9 0 1 1-2.64-6.36" />
|
|
38
|
+
<path d="M21 3v6h-6" />
|
|
39
|
+
</svg>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const IconClock = ({ className }: IconProps) => (
|
|
43
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
44
|
+
<circle cx="12" cy="12" r="9" />
|
|
45
|
+
<path d="M12 7v5l3 2" />
|
|
46
|
+
</svg>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export const IconImg = ({ className }: IconProps) => (
|
|
50
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
51
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
52
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
53
|
+
<path d="M21 15l-5-5L5 21" />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const IconMail = ({ className }: IconProps) => (
|
|
58
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
59
|
+
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
60
|
+
<path d="M3 7l9 6 9-6" />
|
|
61
|
+
</svg>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
export const IconCheck = ({ className }: IconProps) => (
|
|
65
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
66
|
+
<path d="M20 6L9 17l-5-5" />
|
|
67
|
+
</svg>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const IconChevron = ({ className }: IconProps) => (
|
|
71
|
+
<svg className={className} viewBox="0 0 24 24">
|
|
72
|
+
<path d="M6 9l6 6 6-6" />
|
|
73
|
+
</svg>
|
|
74
|
+
);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
export type SegmentedControlOption<TValue extends string = string> = {
|
|
5
|
+
value: TValue;
|
|
6
|
+
label: React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type Props<TValue extends string = string> = {
|
|
10
|
+
options: ReadonlyArray<SegmentedControlOption<TValue>>;
|
|
11
|
+
value: TValue;
|
|
12
|
+
onChange: (value: TValue) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function SegmentedControl<TValue extends string = string>({
|
|
17
|
+
options,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
className = "",
|
|
21
|
+
}: Props<TValue>) {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={classNames("gc-segmented-control", className)}
|
|
25
|
+
role="tablist"
|
|
26
|
+
>
|
|
27
|
+
{options.map((option) => (
|
|
28
|
+
<button
|
|
29
|
+
key={option.value}
|
|
30
|
+
type="button"
|
|
31
|
+
role="tab"
|
|
32
|
+
aria-selected={value === option.value}
|
|
33
|
+
className={classNames("gc-segmented-control__option", {
|
|
34
|
+
"gc-segmented-control__option--on": value === option.value,
|
|
35
|
+
})}
|
|
36
|
+
onClick={() => onChange(option.value)}
|
|
37
|
+
>
|
|
38
|
+
{option.label}
|
|
39
|
+
</button>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.gc-segmented-control {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
background: $gb-color-component;
|
|
4
|
+
border: 1px solid $gb-color-component-dark;
|
|
5
|
+
border-radius: 999px;
|
|
6
|
+
padding: 3px;
|
|
7
|
+
|
|
8
|
+
&__option {
|
|
9
|
+
border: 0;
|
|
10
|
+
background: none;
|
|
11
|
+
padding: 7px 16px;
|
|
12
|
+
border-radius: 999px;
|
|
13
|
+
font-size: 13.5px;
|
|
14
|
+
font-weight: 600;
|
|
15
|
+
color: $color-boulder;
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
|
|
18
|
+
&--on {
|
|
19
|
+
background: $color-white;
|
|
20
|
+
color: $gb-color-primary;
|
|
21
|
+
box-shadow:
|
|
22
|
+
0 1px 2px rgb(33 43 54 / 5%),
|
|
23
|
+
0 1px 1px rgb(33 43 54 / 3%);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
|
|
4
|
+
export type StatModifier = "gray" | "blue" | "green" | "amber" | "red";
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
modifier?: StatModifier;
|
|
8
|
+
label: React.ReactNode;
|
|
9
|
+
value: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function Stat({ modifier = "gray", label, value, className = "" }: Props) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={classNames("gc-stat", `gc-stat--${modifier}`, className)}>
|
|
16
|
+
<div className="gc-stat__key">
|
|
17
|
+
<span className="gc-stat__dot" />
|
|
18
|
+
{label}
|
|
19
|
+
</div>
|
|
20
|
+
<div className="gc-stat__value">{value}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Stat;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
.gc-stat {
|
|
2
|
+
background: $color-white;
|
|
3
|
+
border: 1px solid $gb-color-component-dark;
|
|
4
|
+
border-radius: $radius-large;
|
|
5
|
+
padding: 16px 18px;
|
|
6
|
+
box-shadow:
|
|
7
|
+
0 1px 2px rgb(33 43 54 / 5%),
|
|
8
|
+
0 1px 1px rgb(33 43 54 / 3%);
|
|
9
|
+
|
|
10
|
+
&__key {
|
|
11
|
+
font-family: $gb-font-mono;
|
|
12
|
+
font-size: 11px;
|
|
13
|
+
text-transform: uppercase;
|
|
14
|
+
letter-spacing: 0.04em;
|
|
15
|
+
color: $color-boulder;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 7px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&__dot {
|
|
22
|
+
width: 7px;
|
|
23
|
+
height: 7px;
|
|
24
|
+
border-radius: 50%;
|
|
25
|
+
background: $color-silver;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&__value {
|
|
29
|
+
font-size: 27px;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
color: $color-mine-shaft;
|
|
32
|
+
margin-top: 8px;
|
|
33
|
+
letter-spacing: -0.02em;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&--blue &__dot {
|
|
37
|
+
background: $gb-color-primary;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
&--green &__dot {
|
|
41
|
+
background: $gb-color-success;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
&--amber &__dot {
|
|
45
|
+
background: #c77b12;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&--red &__dot {
|
|
49
|
+
background: $gb-color-danger;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -573,6 +573,38 @@ body.docs-body {
|
|
|
573
573
|
svg { width: 18px; height: 18px; color: var(--text); }
|
|
574
574
|
}
|
|
575
575
|
|
|
576
|
+
.docs-iconset-cell__svg {
|
|
577
|
+
width: 18px;
|
|
578
|
+
height: 18px;
|
|
579
|
+
fill: none;
|
|
580
|
+
stroke: currentcolor;
|
|
581
|
+
stroke-width: 1.7;
|
|
582
|
+
stroke-linecap: round;
|
|
583
|
+
stroke-linejoin: round;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.docs-subsection-title {
|
|
587
|
+
margin: 28px 0 6px;
|
|
588
|
+
font-size: 15px;
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
color: var(--text);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.docs-stat-grid {
|
|
594
|
+
display: grid;
|
|
595
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
596
|
+
gap: 12px;
|
|
597
|
+
width: 100%;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.docs-cover-empty-stage {
|
|
601
|
+
width: 160px;
|
|
602
|
+
height: 120px;
|
|
603
|
+
border: 1px solid var(--border);
|
|
604
|
+
border-radius: var(--radius-lg);
|
|
605
|
+
overflow: hidden;
|
|
606
|
+
}
|
|
607
|
+
|
|
576
608
|
.docs-type-row {
|
|
577
609
|
display: grid;
|
|
578
610
|
grid-template-columns: 80px 80px 1fr;
|
|
@@ -613,6 +645,72 @@ body.docs-body {
|
|
|
613
645
|
.swatch-radius { width: 36px; height: 24px; background: var(--surface-2); border: 1px solid var(--border-strong); }
|
|
614
646
|
}
|
|
615
647
|
|
|
648
|
+
.docs-bp-grid {
|
|
649
|
+
background: var(--bg-elev);
|
|
650
|
+
border: 1px solid var(--border);
|
|
651
|
+
border-radius: var(--radius-lg);
|
|
652
|
+
overflow: hidden;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
.docs-bp-row {
|
|
656
|
+
display: grid;
|
|
657
|
+
grid-template-columns: 180px 120px 1fr;
|
|
658
|
+
gap: 16px;
|
|
659
|
+
align-items: center;
|
|
660
|
+
padding: 10px 18px;
|
|
661
|
+
border-top: 1px solid var(--border);
|
|
662
|
+
|
|
663
|
+
&:first-child { border-top: none; }
|
|
664
|
+
|
|
665
|
+
.mixin,
|
|
666
|
+
.range { font-family: var(--font-mono); font-size: 12.5px; }
|
|
667
|
+
.range { color: var(--text-muted); }
|
|
668
|
+
.vars { font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); }
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.docs-bp-demo {
|
|
672
|
+
display: flex;
|
|
673
|
+
flex-wrap: wrap;
|
|
674
|
+
justify-content: center;
|
|
675
|
+
gap: 12px;
|
|
676
|
+
|
|
677
|
+
.pill {
|
|
678
|
+
display: flex;
|
|
679
|
+
flex-direction: column;
|
|
680
|
+
align-items: center;
|
|
681
|
+
gap: 2px;
|
|
682
|
+
min-width: 128px;
|
|
683
|
+
padding: 10px 18px;
|
|
684
|
+
border: 1px dashed var(--border-strong);
|
|
685
|
+
border-radius: var(--radius);
|
|
686
|
+
opacity: 0.35;
|
|
687
|
+
transition: opacity 0.15s, border-color 0.15s, background 0.15s;
|
|
688
|
+
|
|
689
|
+
.nm { font-family: var(--font-mono); font-size: 12.5px; color: var(--text); }
|
|
690
|
+
.rg { font-family: var(--font-mono); font-size: 11px; color: var(--text-subtle); }
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/* The live part of the Responsiveness demo — powered by the
|
|
695
|
+
library's own gx-* mixins, not by docs-local media queries. */
|
|
696
|
+
@mixin docs-bp-pill-active {
|
|
697
|
+
background: var(--accent-soft);
|
|
698
|
+
border: 1px solid var(--accent);
|
|
699
|
+
opacity: 1;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
@include gx-phone {
|
|
703
|
+
.docs-bp-demo .pill--phone { @include docs-bp-pill-active; }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
@include gx-tablet {
|
|
707
|
+
.docs-bp-demo .pill--tablet { @include docs-bp-pill-active; }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
@include gx-desktop {
|
|
711
|
+
.docs-bp-demo .pill--desktop { @include docs-bp-pill-active; }
|
|
712
|
+
}
|
|
713
|
+
|
|
616
714
|
.docs-comp-index {
|
|
617
715
|
display: grid;
|
|
618
716
|
grid-template-columns: repeat(4, 1fr);
|
package/src/example.scss
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
@import "components/Flex/styles/styles";
|
|
21
21
|
@import "components/FlexItem/styles/styles";
|
|
22
22
|
@import "components/Input/styles/styles";
|
|
23
|
-
@import "components/Image/styles/styles";
|
|
24
23
|
@import "components/Joystick/styles/styles";
|
|
25
24
|
@import "components/Led/styles/mixins";
|
|
26
25
|
@import "components/Led/styles/styles";
|
|
@@ -47,6 +46,9 @@
|
|
|
47
46
|
@import "components/Badge/styles/styles";
|
|
48
47
|
@import "components/Dropdown/styles/styles";
|
|
49
48
|
@import "components/Skeleton/styles/styles";
|
|
49
|
+
@import "components/CoverEmpty/styles/styles";
|
|
50
|
+
@import "components/SegmentedControl/styles/styles";
|
|
51
|
+
@import "components/Stat/styles/styles";
|
|
50
52
|
@import "example/styles/splash";
|
|
51
53
|
@import "example/styles/styles";
|
|
52
54
|
@import "example/styles/docs";
|
package/src/example.tsx
CHANGED
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
Badge,
|
|
6
6
|
Button,
|
|
7
7
|
Card,
|
|
8
|
+
CoverEmpty,
|
|
8
9
|
Dialog,
|
|
9
10
|
Dropdown,
|
|
11
|
+
Icons,
|
|
10
12
|
Input,
|
|
11
13
|
Link,
|
|
12
14
|
Logo,
|
|
15
|
+
SegmentedControl,
|
|
13
16
|
Separator,
|
|
17
|
+
Stat,
|
|
14
18
|
constants,
|
|
15
19
|
} from "./index";
|
|
16
20
|
|
|
@@ -100,6 +104,16 @@ const SPACING = [
|
|
|
100
104
|
["$gb-sp-16", "64 px", "4rem", 64],
|
|
101
105
|
] as const;
|
|
102
106
|
|
|
107
|
+
const BREAKPOINTS = [
|
|
108
|
+
["@include gx-phone", "< 768 px", "up to $g-tablet-width − 1px"],
|
|
109
|
+
[
|
|
110
|
+
"@include gx-tablet",
|
|
111
|
+
"768 – 1023 px",
|
|
112
|
+
"$g-tablet-width … $g-desktop-width − 1px",
|
|
113
|
+
],
|
|
114
|
+
["@include gx-desktop", "≥ 1024 px", "$g-desktop-width and up"],
|
|
115
|
+
] as const;
|
|
116
|
+
|
|
103
117
|
const RADIUS = [
|
|
104
118
|
["$gb-r-sm", "4 px", 4],
|
|
105
119
|
["$gb-r", "6 px", 6],
|
|
@@ -124,6 +138,29 @@ const ICONS: ReadonlyArray<readonly [string, string]> = [
|
|
|
124
138
|
["plus", "M12 5v14M5 12h14"],
|
|
125
139
|
];
|
|
126
140
|
|
|
141
|
+
const ICON_SET: ReadonlyArray<
|
|
142
|
+
readonly [string, (props: { className?: string }) => React.ReactElement]
|
|
143
|
+
> = [
|
|
144
|
+
["IconSearch", Icons.IconSearch],
|
|
145
|
+
["IconEdit", Icons.IconEdit],
|
|
146
|
+
["IconTrash", Icons.IconTrash],
|
|
147
|
+
["IconPlus", Icons.IconPlus],
|
|
148
|
+
["IconRefresh", Icons.IconRefresh],
|
|
149
|
+
["IconClock", Icons.IconClock],
|
|
150
|
+
["IconImg", Icons.IconImg],
|
|
151
|
+
["IconMail", Icons.IconMail],
|
|
152
|
+
["IconCheck", Icons.IconCheck],
|
|
153
|
+
["IconChevron", Icons.IconChevron],
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const SEGMENTED_OPTIONS = [
|
|
157
|
+
{ value: "all", label: "All" },
|
|
158
|
+
{ value: "active", label: "Active" },
|
|
159
|
+
{ value: "archived", label: "Archived" },
|
|
160
|
+
] as const;
|
|
161
|
+
|
|
162
|
+
type SegmentedValue = (typeof SEGMENTED_OPTIONS)[number]["value"];
|
|
163
|
+
|
|
127
164
|
const NAV = [
|
|
128
165
|
{
|
|
129
166
|
title: "Getting started",
|
|
@@ -139,6 +176,7 @@ const NAV = [
|
|
|
139
176
|
{ id: "typography", label: "Typography" },
|
|
140
177
|
{ id: "spacing", label: "Spacing & radius" },
|
|
141
178
|
{ id: "iconography", label: "Iconography", count: "14" },
|
|
179
|
+
{ id: "responsiveness", label: "Responsiveness" },
|
|
142
180
|
],
|
|
143
181
|
},
|
|
144
182
|
{
|
|
@@ -155,6 +193,9 @@ const NAV = [
|
|
|
155
193
|
{ id: "card", label: "Card" },
|
|
156
194
|
{ id: "badge", label: "Badge" },
|
|
157
195
|
{ id: "navigation", label: "Navigation" },
|
|
196
|
+
{ id: "segmented-control", label: "Segmented control" },
|
|
197
|
+
{ id: "stat", label: "Stat" },
|
|
198
|
+
{ id: "cover-empty", label: "Cover empty" },
|
|
158
199
|
],
|
|
159
200
|
},
|
|
160
201
|
];
|
|
@@ -165,6 +206,7 @@ const TOC = [
|
|
|
165
206
|
["typography", "Typography"],
|
|
166
207
|
["spacing", "Spacing & radius"],
|
|
167
208
|
["iconography", "Iconography"],
|
|
209
|
+
["responsiveness", "Responsiveness"],
|
|
168
210
|
["logo", "Logo"],
|
|
169
211
|
["button", "Button"],
|
|
170
212
|
["link", "Link"],
|
|
@@ -176,6 +218,9 @@ const TOC = [
|
|
|
176
218
|
["card", "Card"],
|
|
177
219
|
["badge", "Badge"],
|
|
178
220
|
["navigation", "Navigation"],
|
|
221
|
+
["segmented-control", "Segmented control"],
|
|
222
|
+
["stat", "Stat"],
|
|
223
|
+
["cover-empty", "Cover empty"],
|
|
179
224
|
] as const;
|
|
180
225
|
|
|
181
226
|
type IconProps = {
|
|
@@ -368,6 +413,7 @@ function App() {
|
|
|
368
413
|
const [installCopied, setInstallCopied] = useState(false);
|
|
369
414
|
const [dropdownValue, setDropdownValue] = useState("red");
|
|
370
415
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
416
|
+
const [segmentedValue, setSegmentedValue] = useState<SegmentedValue>("all");
|
|
371
417
|
|
|
372
418
|
useEffect(() => {
|
|
373
419
|
document.documentElement.setAttribute("data-theme", theme);
|
|
@@ -663,6 +709,74 @@ function App() {
|
|
|
663
709
|
<IconCell key={name} name={name} d={d} />
|
|
664
710
|
))}
|
|
665
711
|
</div>
|
|
712
|
+
<h3 className="docs-subsection-title">Icon set</h3>
|
|
713
|
+
<p className="docs-section-desc">
|
|
714
|
+
Ready-to-use React icon components exported from{" "}
|
|
715
|
+
<code>Icons</code>. They inherit <code>currentColor</code>; size
|
|
716
|
+
and stroke are set by the consumer via a class name.
|
|
717
|
+
</p>
|
|
718
|
+
<div className="docs-icon-grid">
|
|
719
|
+
{ICON_SET.map(([name, IconComponent]) => (
|
|
720
|
+
<div className="docs-icon-cell docs-iconset-cell" key={name}>
|
|
721
|
+
<IconComponent className="docs-iconset-cell__svg" />
|
|
722
|
+
<span>{name}</span>
|
|
723
|
+
</div>
|
|
724
|
+
))}
|
|
725
|
+
</div>
|
|
726
|
+
</section>
|
|
727
|
+
|
|
728
|
+
<section className="docs-section" id="responsiveness">
|
|
729
|
+
<div className="docs-section-eyebrow">Foundations / 05</div>
|
|
730
|
+
<h2 className="docs-section-title">Responsiveness</h2>
|
|
731
|
+
<p className="docs-section-desc">
|
|
732
|
+
Breakpoints are exposed as Sass variables and wrapped by three
|
|
733
|
+
mixins — <code>gx-phone</code>, <code>gx-tablet</code> and{" "}
|
|
734
|
+
<code>gx-desktop</code>. Each mixin scopes its block to exactly
|
|
735
|
+
one range, so the three never overlap. The demo below is styled
|
|
736
|
+
with the real mixins — resize the window to see the active range
|
|
737
|
+
light up.
|
|
738
|
+
</p>
|
|
739
|
+
<div className="docs-bp-grid" role="table" aria-label="Breakpoints">
|
|
740
|
+
{BREAKPOINTS.map(([mixin, range, vars]) => (
|
|
741
|
+
<div className="docs-bp-row" key={mixin}>
|
|
742
|
+
<span className="mixin">{mixin}</span>
|
|
743
|
+
<span className="range">{range}</span>
|
|
744
|
+
<span className="vars">{vars}</span>
|
|
745
|
+
</div>
|
|
746
|
+
))}
|
|
747
|
+
</div>
|
|
748
|
+
<div className="docs-spacer-y" />
|
|
749
|
+
<Demo
|
|
750
|
+
stageClass="center"
|
|
751
|
+
code={`.docs-bp-demo .pill { opacity: 0.35; }
|
|
752
|
+
|
|
753
|
+
@include gx-phone {
|
|
754
|
+
.docs-bp-demo .pill--phone { opacity: 1; }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@include gx-tablet {
|
|
758
|
+
.docs-bp-demo .pill--tablet { opacity: 1; }
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
@include gx-desktop {
|
|
762
|
+
.docs-bp-demo .pill--desktop { opacity: 1; }
|
|
763
|
+
}`}
|
|
764
|
+
>
|
|
765
|
+
<div className="docs-bp-demo">
|
|
766
|
+
<div className="pill pill--phone">
|
|
767
|
+
<span className="nm">gx-phone</span>
|
|
768
|
+
<span className="rg">< 768 px</span>
|
|
769
|
+
</div>
|
|
770
|
+
<div className="pill pill--tablet">
|
|
771
|
+
<span className="nm">gx-tablet</span>
|
|
772
|
+
<span className="rg">768 – 1023 px</span>
|
|
773
|
+
</div>
|
|
774
|
+
<div className="pill pill--desktop">
|
|
775
|
+
<span className="nm">gx-desktop</span>
|
|
776
|
+
<span className="rg">≥ 1024 px</span>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
</Demo>
|
|
666
780
|
</section>
|
|
667
781
|
|
|
668
782
|
<section className="docs-section" id="logo">
|
|
@@ -1025,6 +1139,77 @@ function App() {
|
|
|
1025
1139
|
</Demo>
|
|
1026
1140
|
</section>
|
|
1027
1141
|
|
|
1142
|
+
<section className="docs-section" id="segmented-control">
|
|
1143
|
+
<div className="docs-section-eyebrow">Components / 12</div>
|
|
1144
|
+
<h2 className="docs-section-title">Segmented control</h2>
|
|
1145
|
+
<p className="docs-section-desc">
|
|
1146
|
+
A pill-shaped toggle for switching between a few mutually
|
|
1147
|
+
exclusive views. Prefer it over a dropdown when there are two to
|
|
1148
|
+
four options that benefit from being visible at once.
|
|
1149
|
+
</p>
|
|
1150
|
+
<Demo
|
|
1151
|
+
stageClass="center"
|
|
1152
|
+
code={`<SegmentedControl
|
|
1153
|
+
options={[
|
|
1154
|
+
{ value: "all", label: "All" },
|
|
1155
|
+
{ value: "active", label: "Active" },
|
|
1156
|
+
{ value: "archived", label: "Archived" },
|
|
1157
|
+
]}
|
|
1158
|
+
value="${segmentedValue}"
|
|
1159
|
+
onChange={setSegmentedValue}
|
|
1160
|
+
/>`}
|
|
1161
|
+
>
|
|
1162
|
+
<SegmentedControl<SegmentedValue>
|
|
1163
|
+
options={SEGMENTED_OPTIONS}
|
|
1164
|
+
value={segmentedValue}
|
|
1165
|
+
onChange={setSegmentedValue}
|
|
1166
|
+
/>
|
|
1167
|
+
</Demo>
|
|
1168
|
+
</section>
|
|
1169
|
+
|
|
1170
|
+
<section className="docs-section" id="stat">
|
|
1171
|
+
<div className="docs-section-eyebrow">Components / 13</div>
|
|
1172
|
+
<h2 className="docs-section-title">Stat</h2>
|
|
1173
|
+
<p className="docs-section-desc">
|
|
1174
|
+
A single key figure with a labelled, color-coded dot. Group a few
|
|
1175
|
+
of them to summarise the state of a page. The dot color is set via
|
|
1176
|
+
the <code>modifier</code> prop.
|
|
1177
|
+
</p>
|
|
1178
|
+
<Demo
|
|
1179
|
+
stageClass="column"
|
|
1180
|
+
code={`<Stat modifier="blue" label="Posts" value="128" />
|
|
1181
|
+
<Stat modifier="green" label="Published" value="94" />
|
|
1182
|
+
<Stat modifier="amber" label="Drafts" value="26" />
|
|
1183
|
+
<Stat modifier="red" label="Flagged" value="8" />`}
|
|
1184
|
+
>
|
|
1185
|
+
<div className="docs-stat-grid">
|
|
1186
|
+
<Stat modifier="blue" label="Posts" value="128" />
|
|
1187
|
+
<Stat modifier="green" label="Published" value="94" />
|
|
1188
|
+
<Stat modifier="amber" label="Drafts" value="26" />
|
|
1189
|
+
<Stat modifier="red" label="Flagged" value="8" />
|
|
1190
|
+
</div>
|
|
1191
|
+
</Demo>
|
|
1192
|
+
</section>
|
|
1193
|
+
|
|
1194
|
+
<section className="docs-section" id="cover-empty">
|
|
1195
|
+
<div className="docs-section-eyebrow">Components / 14</div>
|
|
1196
|
+
<h2 className="docs-section-title">Cover empty</h2>
|
|
1197
|
+
<p className="docs-section-desc">
|
|
1198
|
+
A neutral placeholder for a missing image or empty media slot. It
|
|
1199
|
+
fills its container, so give the parent a fixed size.
|
|
1200
|
+
</p>
|
|
1201
|
+
<Demo
|
|
1202
|
+
stageClass="center"
|
|
1203
|
+
code={`<div style={{ width: 160, height: 120 }}>
|
|
1204
|
+
<CoverEmpty />
|
|
1205
|
+
</div>`}
|
|
1206
|
+
>
|
|
1207
|
+
<div className="docs-cover-empty-stage">
|
|
1208
|
+
<CoverEmpty />
|
|
1209
|
+
</div>
|
|
1210
|
+
</Demo>
|
|
1211
|
+
</section>
|
|
1212
|
+
|
|
1028
1213
|
<footer className="docs-footer">
|
|
1029
1214
|
<div>graphen · {VERSION} · MIT</div>
|
|
1030
1215
|
<div>Built by the CODA_ team</div>
|