@gogitcms/design-system 0.15.0-next.3

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.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/css/components.css +312 -0
  3. package/css/tokens.css +212 -0
  4. package/package.json +65 -0
  5. package/src/ThemeProvider.tsx +86 -0
  6. package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
  7. package/src/__tests__/BranchImport.test.tsx +46 -0
  8. package/src/__tests__/Button.test.tsx +45 -0
  9. package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
  10. package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
  11. package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
  12. package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
  13. package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
  14. package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
  15. package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
  16. package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
  17. package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
  18. package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
  19. package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
  20. package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
  21. package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
  22. package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
  23. package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
  24. package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
  25. package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
  26. package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
  27. package/src/__tests__/MediaBrowser.test.tsx +353 -0
  28. package/src/__tests__/MediaField.test.tsx +185 -0
  29. package/src/__tests__/Notifications.test.tsx +69 -0
  30. package/src/__tests__/Onboarding.test.tsx +287 -0
  31. package/src/__tests__/cssTokens.test.ts +201 -0
  32. package/src/__tests__/fieldComponents.test.ts +43 -0
  33. package/src/__tests__/reorder.test.ts +58 -0
  34. package/src/components/ApplyChangesModal.tsx +348 -0
  35. package/src/components/BranchImport.tsx +157 -0
  36. package/src/components/BranchMenu.tsx +192 -0
  37. package/src/components/Button.tsx +131 -0
  38. package/src/components/ChangeDetail.tsx +472 -0
  39. package/src/components/ChangeRequestSummary.tsx +173 -0
  40. package/src/components/CollabField.tsx +388 -0
  41. package/src/components/ContentBrowser.tsx +5073 -0
  42. package/src/components/Icon.tsx +28 -0
  43. package/src/components/Icon.web.tsx +31 -0
  44. package/src/components/Input.tsx +106 -0
  45. package/src/components/MediaBrowser.tsx +766 -0
  46. package/src/components/MediaField.tsx +670 -0
  47. package/src/components/MediaPreview.tsx +91 -0
  48. package/src/components/MediaPreview.web.tsx +169 -0
  49. package/src/components/NavRow.tsx +105 -0
  50. package/src/components/Notifications.tsx +301 -0
  51. package/src/components/Onboarding.tsx +751 -0
  52. package/src/components/ProjectMenu.tsx +124 -0
  53. package/src/components/Segment.tsx +87 -0
  54. package/src/components/Skeleton.tsx +216 -0
  55. package/src/components/Spinner.tsx +44 -0
  56. package/src/components/Text.tsx +85 -0
  57. package/src/components/documentDrafts.ts +213 -0
  58. package/src/components/layout.tsx +284 -0
  59. package/src/components/primitives.tsx +143 -0
  60. package/src/components/reorder.ts +40 -0
  61. package/src/fieldComponents.ts +63 -0
  62. package/src/icons.ts +102 -0
  63. package/src/index.ts +198 -0
  64. package/src/media.ts +229 -0
  65. package/src/theme.ts +116 -0
  66. package/src/web/Button.tsx +110 -0
  67. package/src/web/Icon.tsx +52 -0
  68. package/src/web/Input.tsx +39 -0
  69. package/src/web/index.ts +43 -0
  70. package/src/web/primitives.tsx +119 -0
package/src/theme.ts ADDED
@@ -0,0 +1,116 @@
1
+ import { Platform } from "react-native";
2
+
3
+ // Go Git CMS design system — monochrome, terminal-adjacent, desktop-first.
4
+ // A single neutral ink ramp carries the entire UI; the only chromatic tokens
5
+ // are the git diff signals (add/remove). Light and dark are the same semantic
6
+ // mapping over an inverted ink ramp.
7
+
8
+ export type ThemeMode = "light" | "dark";
9
+
10
+ type Ink = Record<0 | 50 | 100 | 150 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 1000, string>;
11
+
12
+ const inkLight: Ink = {
13
+ 0: "#ffffff", 50: "#f7f7f7", 100: "#ededed", 150: "#e4e4e4", 200: "#dcdcdc",
14
+ 300: "#c9c9c9", 400: "#a6a6a6", 500: "#808080", 600: "#5c5c5c", 700: "#3d3d3d",
15
+ 800: "#242424", 900: "#151515", 1000: "#0a0a0a",
16
+ };
17
+
18
+ const inkDark: Ink = {
19
+ 0: "#0b0b0b", 50: "#151515", 100: "#1e1e1e", 150: "#242424", 200: "#2d2d2d",
20
+ 300: "#3a3a3a", 400: "#585858", 500: "#808080", 600: "#a2a2a2", 700: "#c4c4c4",
21
+ 800: "#dcdcdc", 900: "#ececec", 1000: "#f4f4f4",
22
+ };
23
+
24
+ // Font stacks: Geist / Geist Mono on web (loaded via <link> in the host page),
25
+ // falling back to platform system fonts on native.
26
+ const fontSans = Platform.select({
27
+ web: "Geist, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif",
28
+ default: "System",
29
+ }) as string;
30
+
31
+ const fontMono = Platform.select({
32
+ web: "'Geist Mono', ui-monospace, 'SF Mono', Menlo, Consolas, monospace",
33
+ ios: "Menlo",
34
+ android: "monospace",
35
+ default: "monospace",
36
+ }) as string;
37
+
38
+ export function makeTheme(mode: ThemeMode) {
39
+ const ink = mode === "dark" ? inkDark : inkLight;
40
+ const dark = mode === "dark";
41
+ return {
42
+ mode,
43
+ ink,
44
+ color: {
45
+ // Text
46
+ textPrimary: ink[1000],
47
+ textSecondary: ink[600],
48
+ textTertiary: ink[500],
49
+ textDisabled: ink[400],
50
+ textInverted: ink[0],
51
+ // Surfaces
52
+ surfacePage: ink[0],
53
+ surfaceRaised: dark ? ink[50] : ink[0],
54
+ // The top bar is a permanent dark chrome in both modes: near-black in
55
+ // light, a slightly-raised charcoal in dark so it reads above the page.
56
+ // Its inline contents render under the dark theme to stay legible.
57
+ surfaceTopBar: dark ? "#1e1e1e" : "#1a1a1a",
58
+ surfaceSunken: ink[50],
59
+ surfaceHover: ink[50],
60
+ surfaceActive: ink[100],
61
+ surfaceInverted: ink[1000],
62
+ overlay: "rgba(10,10,10,0.45)",
63
+ // Borders — hairlines carry structure
64
+ borderSubtle: ink[100],
65
+ borderDefault: ink[200],
66
+ borderHover: ink[400],
67
+ borderStrong: ink[1000],
68
+ borderFocus: ink[1000],
69
+ // Active-column/tab accent rule. A mid-gray in both modes: the rule sits
70
+ // directly beneath the dark top bar, so pure black (light) merges into it
71
+ // and pure white (dark) glares — ink[600] reads as an accent against both
72
+ // the bar above and the column below.
73
+ borderActive: ink[600],
74
+ // Git diff signals — the only permitted color
75
+ diffAddFg: dark ? "#3fb950" : "#1a7f37",
76
+ diffAddBg: dark ? "#12261a" : "#e6f4ea",
77
+ diffDelFg: dark ? "#f85149" : "#b42318",
78
+ diffDelBg: dark ? "#2a1615" : "#fbeae8",
79
+ },
80
+ // 4px base grid
81
+ space: (n: number) => n * 4,
82
+ radius: { sm: 3, md: 5, lg: 8, pill: 999 },
83
+ control: { sm: 28, md: 34, lg: 40 },
84
+ layout: { sidebar: 260, column: 300, topbar: 48, container: 1200 },
85
+ font: { sans: fontSans, mono: fontMono },
86
+ type: {
87
+ display: { size: 40, line: 44 },
88
+ h1: { size: 28, line: 34 },
89
+ h2: { size: 22, line: 28 },
90
+ h3: { size: 17, line: 24 },
91
+ body: { size: 14, line: 21 },
92
+ bodyLg: { size: 16, line: 24 },
93
+ sm: { size: 13, line: 18 },
94
+ xs: { size: 11, line: 16 },
95
+ mono: { size: 13, line: 20 },
96
+ monoSm: { size: 12, line: 18 },
97
+ },
98
+ weight: {
99
+ light: "300" as const,
100
+ regular: "400" as const,
101
+ medium: "500" as const,
102
+ semibold: "600" as const,
103
+ bold: "700" as const,
104
+ },
105
+ letterSpacing: { tight: -0.4, normal: 0, wide: 0.3, label: 0.9 },
106
+ duration: { fast: 90, base: 150, slow: 240 },
107
+ };
108
+ }
109
+
110
+ export type Theme = ReturnType<typeof makeTheme>;
111
+ export const lightTheme = makeTheme("light");
112
+ export const darkTheme = makeTheme("dark");
113
+
114
+ // Responsive breakpoint: at/above this width the desktop multi-pane layout is
115
+ // used; below it, the mobile drill-down layout.
116
+ export const DESKTOP_BREAKPOINT = 900;
@@ -0,0 +1,110 @@
1
+ import React from "react";
2
+ import { Icon } from "./Icon";
3
+ import type { IconName } from "../icons";
4
+
5
+ export type ButtonVariant = "primary" | "default" | "ghost" | "danger";
6
+ export type ButtonSize = "sm" | "md" | "lg";
7
+
8
+ type Common = {
9
+ variant?: ButtonVariant;
10
+ size?: ButtonSize;
11
+ iconLeft?: IconName | React.ReactNode;
12
+ fullWidth?: boolean;
13
+ className?: string;
14
+ children?: React.ReactNode;
15
+ };
16
+
17
+ export type ButtonProps = Common &
18
+ Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children" | "className">;
19
+
20
+ export type ButtonLinkProps = Common &
21
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "children" | "className">;
22
+
23
+ function classes({ variant = "default", size = "md", fullWidth, className }: Common) {
24
+ return [
25
+ "ds-btn",
26
+ `ds-btn--${variant}`,
27
+ size !== "md" && `ds-btn--${size}`,
28
+ fullWidth && "ds-btn--full",
29
+ className,
30
+ ]
31
+ .filter(Boolean)
32
+ .join(" ");
33
+ }
34
+
35
+ function leading(iconLeft: Common["iconLeft"], size: ButtonSize) {
36
+ if (iconLeft == null) return null;
37
+ if (typeof iconLeft !== "string") return iconLeft;
38
+ return <Icon name={iconLeft as IconName} size={size === "sm" ? 14 : 16} />;
39
+ }
40
+
41
+ /** The monochrome action button, mirroring the native Button's variant x size. */
42
+ export function Button({
43
+ variant = "default",
44
+ size = "md",
45
+ iconLeft,
46
+ fullWidth,
47
+ className,
48
+ children,
49
+ type = "button",
50
+ ...rest
51
+ }: ButtonProps) {
52
+ return (
53
+ <button type={type} className={classes({ variant, size, fullWidth, className })} {...rest}>
54
+ {leading(iconLeft, size)}
55
+ {children != null && <span>{children}</span>}
56
+ </button>
57
+ );
58
+ }
59
+
60
+ /** The same button rendered as an anchor, for navigation rather than an action. */
61
+ export function ButtonLink({
62
+ variant = "default",
63
+ size = "md",
64
+ iconLeft,
65
+ fullWidth,
66
+ className,
67
+ children,
68
+ ...rest
69
+ }: ButtonLinkProps) {
70
+ return (
71
+ <a className={classes({ variant, size, fullWidth, className })} {...rest}>
72
+ {leading(iconLeft, size)}
73
+ {children != null && <span>{children}</span>}
74
+ </a>
75
+ );
76
+ }
77
+
78
+ export type IconButtonProps = {
79
+ name: IconName;
80
+ /** Required — an icon-only control has no visible text to name it. */
81
+ label: string;
82
+ size?: "sm" | "md";
83
+ /** Drop the border and surface, leaving just the glyph. */
84
+ bare?: boolean;
85
+ className?: string;
86
+ } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "className">;
87
+
88
+ export function IconButton({
89
+ name,
90
+ label,
91
+ size = "md",
92
+ bare,
93
+ className,
94
+ type = "button",
95
+ ...rest
96
+ }: IconButtonProps) {
97
+ return (
98
+ <button
99
+ type={type}
100
+ title={label}
101
+ aria-label={label}
102
+ className={["ds-iconbtn", size === "sm" && "ds-iconbtn--sm", bare && "ds-iconbtn--bare", className]
103
+ .filter(Boolean)
104
+ .join(" ")}
105
+ {...rest}
106
+ >
107
+ <Icon name={name} size={size === "sm" ? 14 : 16} />
108
+ </button>
109
+ );
110
+ }
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { icons, type IconName } from "../icons";
3
+
4
+ export type IconProps = {
5
+ name: IconName;
6
+ size?: number;
7
+ /** Any CSS color, including a token: `var(--text-secondary)`. Defaults to the
8
+ * inherited text color so an icon takes on the ink of whatever contains it. */
9
+ color?: string;
10
+ strokeWidth?: number;
11
+ /** Icons are decorative by default; pass a label when one carries meaning. */
12
+ label?: string;
13
+ className?: string;
14
+ };
15
+
16
+ /**
17
+ * The product icon set rendered as a plain <svg>, for web surfaces that don't
18
+ * run react-native-svg. Same paths as the native Icon — both read ../icons.
19
+ */
20
+ export function Icon({
21
+ name,
22
+ size = 18,
23
+ color = "currentColor",
24
+ strokeWidth = 1.5,
25
+ label,
26
+ className,
27
+ }: IconProps) {
28
+ return (
29
+ <svg
30
+ className={className}
31
+ width={size}
32
+ height={size}
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke={color}
36
+ strokeWidth={strokeWidth}
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ role={label ? "img" : undefined}
40
+ aria-label={label}
41
+ aria-hidden={label ? undefined : true}
42
+ focusable="false"
43
+ style={{ display: "block", flexShrink: 0 }}
44
+ >
45
+ {icons[name].map((d, i) => (
46
+ <path key={i} d={d} />
47
+ ))}
48
+ </svg>
49
+ );
50
+ }
51
+
52
+ export type { IconName };
@@ -0,0 +1,39 @@
1
+ import React from "react";
2
+
3
+ type Shared = {
4
+ /** Mono for anything technical: paths, branches, hashes, config keys. */
5
+ mono?: boolean;
6
+ size?: "sm" | "md";
7
+ className?: string;
8
+ };
9
+
10
+ export type InputProps = Shared &
11
+ Omit<React.InputHTMLAttributes<HTMLInputElement>, "className" | "size">;
12
+
13
+ export type TextareaProps = Shared &
14
+ Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "className">;
15
+
16
+ export type SelectProps = Shared &
17
+ Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "className" | "size">;
18
+
19
+ function classes({ mono, size, className }: Shared) {
20
+ return ["ds-input", mono && "ds-input--mono", size === "sm" && "ds-input--sm", className]
21
+ .filter(Boolean)
22
+ .join(" ");
23
+ }
24
+
25
+ export function Input({ mono, size, className, ...rest }: InputProps) {
26
+ return <input className={classes({ mono, size, className })} {...rest} />;
27
+ }
28
+
29
+ export function Textarea({ mono, size, className, ...rest }: TextareaProps) {
30
+ return <textarea className={classes({ mono, size, className })} {...rest} />;
31
+ }
32
+
33
+ export function Select({ mono, size, className, children, ...rest }: SelectProps) {
34
+ return (
35
+ <select className={classes({ mono, size, className })} {...rest}>
36
+ {children}
37
+ </select>
38
+ );
39
+ }
@@ -0,0 +1,43 @@
1
+ // @gogitcms/design-system/web — the design system for surfaces that render to
2
+ // plain DOM instead of react-native-web: the Astro docs site, marketing pages,
3
+ // anything server-rendered.
4
+ //
5
+ // Same tokens, same icon paths, same visual decisions as the native components
6
+ // next door in ../components; the difference is that these emit HTML elements
7
+ // and read their styling from ../../css/components.css, so a button looks right
8
+ // in server HTML with no hydration. Import both stylesheets once in the host
9
+ // page:
10
+ //
11
+ // import "@gogitcms/design-system/css/tokens.css";
12
+ // import "@gogitcms/design-system/css/components.css";
13
+
14
+ export { Icon } from "./Icon";
15
+ export type { IconProps, IconName } from "./Icon";
16
+
17
+ export { Button, ButtonLink, IconButton } from "./Button";
18
+ export type {
19
+ ButtonProps,
20
+ ButtonLinkProps,
21
+ ButtonVariant,
22
+ ButtonSize,
23
+ IconButtonProps,
24
+ } from "./Button";
25
+
26
+ export { Input, Textarea, Select } from "./Input";
27
+ export type { InputProps, TextareaProps, SelectProps } from "./Input";
28
+
29
+ export { Badge, Avatar, Card, CardLink, SectionLabel, Divider, Meta } from "./primitives";
30
+ export type {
31
+ BadgeProps,
32
+ BadgeTone,
33
+ AvatarProps,
34
+ CardProps,
35
+ CardLinkProps,
36
+ SectionLabelProps,
37
+ DividerProps,
38
+ MetaProps,
39
+ } from "./primitives";
40
+
41
+ // Nothing here re-exports ../theme: it imports react-native for Platform.select,
42
+ // which a plain-DOM consumer has no reason to install. Web surfaces read token
43
+ // values from the CSS custom properties instead.
@@ -0,0 +1,119 @@
1
+ import React from "react";
2
+
3
+ export type BadgeTone = "neutral" | "add" | "del" | "strong";
4
+
5
+ export type BadgeProps = {
6
+ tone?: BadgeTone;
7
+ /** Badges are mono by default — they carry counts and git status letters. */
8
+ mono?: boolean;
9
+ className?: string;
10
+ children?: React.ReactNode;
11
+ } & Omit<React.HTMLAttributes<HTMLSpanElement>, "className" | "children">;
12
+
13
+ export function Badge({ tone = "neutral", mono = true, className, children, ...rest }: BadgeProps) {
14
+ return (
15
+ <span
16
+ className={["ds-badge", `ds-badge--${tone}`, !mono && "ds-badge--sans", className]
17
+ .filter(Boolean)
18
+ .join(" ")}
19
+ {...rest}
20
+ >
21
+ {children}
22
+ </span>
23
+ );
24
+ }
25
+
26
+ export type AvatarProps = {
27
+ initials: string;
28
+ size?: number;
29
+ className?: string;
30
+ } & Omit<React.HTMLAttributes<HTMLSpanElement>, "className">;
31
+
32
+ /** Square initials tile — the product has no uploaded-avatar surface. */
33
+ export function Avatar({ initials, size = 28, className, style, ...rest }: AvatarProps) {
34
+ return (
35
+ <span
36
+ className={["ds-avatar", className].filter(Boolean).join(" ")}
37
+ style={{ width: size, height: size, ...style }}
38
+ {...rest}
39
+ >
40
+ {initials}
41
+ </span>
42
+ );
43
+ }
44
+
45
+ export type CardProps = {
46
+ /** Sunken cards sit on --surface-sunken; use for asides, not content rows. */
47
+ sunken?: boolean;
48
+ className?: string;
49
+ children?: React.ReactNode;
50
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "children">;
51
+
52
+ export function Card({ sunken, className, children, ...rest }: CardProps) {
53
+ return (
54
+ <div
55
+ className={["ds-card", sunken && "ds-card--sunken", className].filter(Boolean).join(" ")}
56
+ {...rest}
57
+ >
58
+ {children}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export type CardLinkProps = {
64
+ href: string;
65
+ className?: string;
66
+ children?: React.ReactNode;
67
+ } & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "className" | "children" | "href">;
68
+
69
+ /** A card that is itself the link target — the docs home grid is built of these. */
70
+ export function CardLink({ href, className, children, ...rest }: CardLinkProps) {
71
+ return (
72
+ <a
73
+ href={href}
74
+ className={["ds-card", "ds-card--interactive", className].filter(Boolean).join(" ")}
75
+ {...rest}
76
+ >
77
+ {children}
78
+ </a>
79
+ );
80
+ }
81
+
82
+ export type SectionLabelProps = {
83
+ className?: string;
84
+ children?: React.ReactNode;
85
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, "className" | "children">;
86
+
87
+ /** The uppercase mono micro-label that heads a group. */
88
+ export function SectionLabel({ className, children, ...rest }: SectionLabelProps) {
89
+ return (
90
+ <div className={["ds-label", className].filter(Boolean).join(" ")} {...rest}>
91
+ {children}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export type DividerProps = {
97
+ vertical?: boolean;
98
+ className?: string;
99
+ style?: React.CSSProperties;
100
+ };
101
+
102
+ export function Divider({ vertical, className, style }: DividerProps) {
103
+ const cls = ["ds-divider", vertical && "ds-divider--vertical", className].filter(Boolean).join(" ");
104
+ return vertical ? <span className={cls} style={style} /> : <hr className={cls} style={style} />;
105
+ }
106
+
107
+ export type MetaProps = {
108
+ className?: string;
109
+ children?: React.ReactNode;
110
+ } & Omit<React.HTMLAttributes<HTMLSpanElement>, "className" | "children">;
111
+
112
+ /** Mono metadata: paths, hashes, branch names, timestamps, counts. */
113
+ export function Meta({ className, children, ...rest }: MetaProps) {
114
+ return (
115
+ <span className={["ds-meta", className].filter(Boolean).join(" ")} {...rest}>
116
+ {children}
117
+ </span>
118
+ );
119
+ }