frontend client for gemstone. decentralised workplace app
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(facet): internal theming library

serenity 422ed0b2 d6db8f6b

+493
+24
src/lib/facet/README.md
··· 1 + # facet 2 + 3 + `facet` is our internal library for making client apps. Think of it as our version of Tailwind/Nativewind. Yes, it is confusingly clashing with an ATProto text facet, but you can always import it as something else. 4 + 5 + ## Usage 6 + 7 + `facet` provides the following hooks to help make building a consistent design easier. 8 + 9 + - `useFacet()`, which provides the entire Facet library object, containing all the values listed below. 10 + - `useVariant()`, which provides a `FacetVariant` object containing the colours defined for our application based on a `FacetPalette` object. 11 + - `useAtoms()`, which provides a `FacetAtoms` object, containing consistent values for shadows, borders, radii, layout, and positioning. 12 + - `useTypography()`, which provides a `FacetTypography` object, containing consistent values for all things text-related. 13 + 14 + `facet` also exports a provider, the `FacetProvider`, which must be wrapped around the root of your application (or similar) to access the values. 15 + 16 + ## Setup 17 + 18 + 1. Create a new `Facet` by calling the generator and providing any options in the shape of a `FacetOpts` object. `const facet = generateFacet()`. 19 + 2. Provide the `Facet` as a prop to the `FacetProvider`. `<FacetProvider facet={facet}>...</FacetProvider>` 20 + 3. Use the hooks elsewhere in your application. 21 + 22 + ## Configuration 23 + 24 + If no options object is passed to the `Facet` constructor, it will use the values defined for Gemstone's client app. In general, any value not passed in the initial constructor object will be replaced with these values as well.
+8
src/lib/facet/index.ts
··· 1 + export { generateFacet } from "./src/facet"; 2 + export { 3 + FacetProvider, 4 + useFacet, 5 + useAtoms, 6 + useTypography, 7 + useVariant, 8 + } from "./src/providers";
+55
src/lib/facet/src/atoms.ts
··· 1 + export interface FacetAtomRadii { 2 + none: number; 3 + xs: number; 4 + sm: number; 5 + md: number; 6 + lg: number; 7 + xl: number; 8 + "2xl": number; 9 + "3xl": number; 10 + "4xl": number; 11 + full: number; 12 + } 13 + 14 + const DEFAULT_RADII: FacetAtomRadii = { 15 + none: 0, 16 + xs: 2, 17 + sm: 4, 18 + md: 6, 19 + lg: 8, 20 + xl: 12, 21 + "2xl": 16, 22 + "3xl": 24, 23 + "4xl": 32, 24 + full: 999999999, // supposedly it's infinity * 1px but bwegh 25 + }; 26 + 27 + export interface FacetAtomBoxShadows { 28 + "2xs": string; 29 + xs: string; 30 + sm: string; 31 + md: string; 32 + lg: string; 33 + xl: string; 34 + "2xl": string; 35 + } 36 + 37 + const DEFAULT_BOX_SHADOWS: FacetAtomBoxShadows = { 38 + "2xs": "0 1px rgb(0 0 0 / 0.05)", 39 + xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)", 40 + sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1)", 41 + md: "0 4px 6px -1px rgba(0, 0, 0, 0.1)", 42 + lg: "0 10px 15px -3px rgba(0, 0, 0, 0.1)", 43 + xl: "0 20px 25px -5px rgba(0, 0, 0, 0.1)", 44 + "2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)", 45 + }; 46 + 47 + export interface FacetAtoms { 48 + radii: FacetAtomRadii; 49 + boxShadows: FacetAtomBoxShadows; 50 + } 51 + 52 + export const DEFAULT_FACET_ATOMS: FacetAtoms = { 53 + radii: DEFAULT_RADII, 54 + boxShadows: DEFAULT_BOX_SHADOWS, 55 + };
+53
src/lib/facet/src/facet.ts
··· 1 + import { DEFAULT_FACET_ATOMS, type FacetAtoms } from "@/lib/facet/src/atoms"; 2 + import { 3 + DEFAULT_FACET_TYPOGRAPHY, 4 + type FacetTypography, 5 + } from "@/lib/facet/src/typography"; 6 + import { 7 + DEFAULT_FACET_VARIANTS, 8 + type FacetVariants, 9 + } from "@/lib/facet/src/variants"; 10 + 11 + export interface Facet { 12 + variants: FacetVariants; 13 + typography: FacetTypography; 14 + atoms: FacetAtoms; 15 + } 16 + 17 + export const DEFAULT_FACET: Facet = { 18 + variants: DEFAULT_FACET_VARIANTS, 19 + typography: DEFAULT_FACET_TYPOGRAPHY, 20 + atoms: DEFAULT_FACET_ATOMS, 21 + }; 22 + 23 + function deepMerge<T extends object>(defaults: T, overrides: Partial<T>): T { 24 + const result = { ...defaults }; 25 + 26 + for (const key in overrides) { 27 + if (Object.prototype.hasOwnProperty.call(overrides, key)) { 28 + const overrideValue = overrides[key]; 29 + const defaultValue = result[key]; 30 + 31 + if ( 32 + overrideValue !== undefined && 33 + overrideValue !== null && 34 + typeof overrideValue === "object" && 35 + !Array.isArray(overrideValue) && 36 + defaultValue && 37 + typeof defaultValue === "object" && 38 + !Array.isArray(defaultValue) 39 + ) { 40 + result[key] = deepMerge(defaultValue, overrideValue); 41 + } else if (overrideValue !== undefined) { 42 + // @ts-expect-error We are doing funky type magicks 43 + result[key] = overrideValue; 44 + } 45 + } 46 + } 47 + 48 + return result; 49 + } 50 + 51 + export const generateFacet = (cfg: Partial<Facet> = {}) => { 52 + return deepMerge<Facet>(DEFAULT_FACET, cfg); 53 + };
+78
src/lib/facet/src/palette.ts
··· 1 + export type HexCode = `#${string}`; 2 + 3 + // I do actually like Catppuccin's spread, but we'll want to define our own 4 + // Especially to indicate differences in the strength of our UI signals. 5 + // But doing it this way means that we can support easier re-theming in the future. 6 + export interface FacetPaletteSemantics { 7 + primary: HexCode; 8 + primaryLight?: HexCode; 9 + primaryDark?: HexCode; 10 + 11 + secondary: HexCode; 12 + secondaryLight?: HexCode; 13 + secondaryDark?: HexCode; 14 + 15 + background: HexCode; 16 + surface: HexCode; 17 + surfaceVariant: HexCode; 18 + 19 + text: HexCode; 20 + textSecondary: HexCode; 21 + textTertiary: HexCode; 22 + textInverse: HexCode; 23 + 24 + positive: HexCode; 25 + positiveLight?: HexCode; 26 + positiveDark?: HexCode; 27 + 28 + negative: HexCode; 29 + negativeLight?: HexCode; 30 + negativeDark?: HexCode; 31 + 32 + success: HexCode; 33 + successLight?: HexCode; 34 + error: HexCode; 35 + errorLight?: HexCode; 36 + warning: HexCode; 37 + warningLight?: HexCode; 38 + info: HexCode; 39 + infoLight?: HexCode; 40 + } 41 + 42 + // Basically Catppuccin tbh 43 + export interface FacetPaletteFundemantals { 44 + crust: HexCode; 45 + mantle: HexCode; 46 + base: HexCode; 47 + surface0: HexCode; 48 + surface1: HexCode; 49 + surface2: HexCode; 50 + overlay0: HexCode; 51 + overlay1: HexCode; 52 + overlay2: HexCode; 53 + subtext0: HexCode; 54 + subtext1: HexCode; 55 + text: HexCode; 56 + iolite: HexCode; // lavender 57 + lapis: HexCode; //blue 58 + sapphire: HexCode; 59 + aquamarine: HexCode; // sky 60 + apatite: HexCode; // teal 61 + emerald: HexCode; // green 62 + citrine: HexCode; // yellow 63 + sunstone: HexCode; // peach 64 + garnet: HexCode; // maroon 65 + ruby: HexCode; // red 66 + amethyst: HexCode; // mauve 67 + morganite: HexCode; // pink 68 + coral: HexCode; // flamingo 69 + moonstone: HexCode; // rosewater 70 + } 71 + 72 + /** 73 + * Generally, prefer to use `semantic`, as they are based on the `colors` already. 74 + */ 75 + export interface FacetPalette { 76 + colors: FacetPaletteFundemantals; 77 + semantic: FacetPaletteSemantics; 78 + }
+44
src/lib/facet/src/providers.tsx
··· 1 + import type { Facet } from "@/lib/facet/src/facet"; 2 + import type { ReactNode } from "react"; 3 + import { createContext, useContext } from "react"; 4 + 5 + const FacetContext = createContext<Facet | null>(null); 6 + 7 + export const useFacet = () => { 8 + const value = useContext(FacetContext); 9 + if (!value) 10 + throw new Error( 11 + "Facet provider failed to initialise. Did you access this out of tree somehow? Tried to access facet values before it was initialised.", 12 + ); 13 + return value; 14 + }; 15 + 16 + export const useAtoms = () => { 17 + const { atoms } = useFacet(); 18 + return atoms; 19 + }; 20 + 21 + export const useVariant = (variantName: string) => { 22 + const { variants } = useFacet(); 23 + const variant = variants[variantName]; 24 + if (!variant) 25 + throw new Error( 26 + `Provided variant ${variantName} does not exist in the configured Facet. Check the configuration init object.`, 27 + ); 28 + return variant; 29 + }; 30 + 31 + export const useTypography = () => { 32 + const { typography } = useFacet(); 33 + return typography; 34 + }; 35 + 36 + export const FacetProvider = ({ 37 + children, 38 + facet, 39 + }: { 40 + children: ReactNode; 41 + facet: Facet; 42 + }) => { 43 + return <FacetContext value={facet}>{children}</FacetContext>; 44 + };
+160
src/lib/facet/src/typography.ts
··· 1 + export interface FacetFontFamilies { 2 + primary: string; 3 + secondary?: string; 4 + tertiary?: string; 5 + serif?: string; 6 + sansSerif?: string; 7 + monospace: string; 8 + } 9 + 10 + const DEFAULT_FONT_FAMILIES: FacetFontFamilies = { 11 + primary: "Lexend", 12 + monospace: "Maple Mono", 13 + }; 14 + 15 + export interface FacetFontSize { 16 + fontSize: number; 17 + lineHeight: number; 18 + } 19 + 20 + export interface FacetFontSizes { 21 + xs: FacetFontSize; 22 + sm: FacetFontSize; 23 + base: FacetFontSize; 24 + lg: FacetFontSize; 25 + xl: FacetFontSize; 26 + "2xl": FacetFontSize; 27 + "3xl": FacetFontSize; 28 + "4xl": FacetFontSize; 29 + } 30 + 31 + const DEFAULT_FONT_SIZES: FacetFontSizes = { 32 + xs: { 33 + fontSize: 12, 34 + lineHeight: 16, 35 + }, 36 + sm: { 37 + fontSize: 14, 38 + lineHeight: 20, 39 + }, 40 + base: { 41 + fontSize: 16, 42 + lineHeight: 24, 43 + }, 44 + lg: { 45 + fontSize: 18, 46 + lineHeight: 28, 47 + }, 48 + xl: { 49 + fontSize: 20, 50 + lineHeight: 28, 51 + }, 52 + "2xl": { 53 + fontSize: 24, 54 + lineHeight: 32, 55 + }, 56 + "3xl": { 57 + fontSize: 30, 58 + lineHeight: 36, 59 + }, 60 + "4xl": { 61 + fontSize: 36, 62 + lineHeight: 40, 63 + }, 64 + }; 65 + 66 + export interface FacetFontStyles { 67 + italic: string; 68 + notItalic: string; 69 + } 70 + 71 + const DEFAULT_FONT_STYLES: FacetFontStyles = { 72 + italic: "italic", 73 + notItalic: "normal", 74 + }; 75 + 76 + export interface FacetFontWeights { 77 + thin: number; 78 + extralight: number; 79 + light: number; 80 + normal: number; 81 + medium: number; 82 + semibold: number; 83 + bold: number; 84 + extrabold: number; 85 + black: number; 86 + } 87 + 88 + const DEFAULT_FONT_WEIGHTS: FacetFontWeights = { 89 + thin: 100, 90 + extralight: 200, 91 + light: 300, 92 + normal: 400, 93 + medium: 500, 94 + semibold: 600, 95 + bold: 700, 96 + extrabold: 800, 97 + black: 900, 98 + }; 99 + 100 + export interface FacetLetterSpacings { 101 + tighter: number; 102 + tight: number; 103 + normal: number; 104 + wide: number; 105 + wider: number; 106 + widest: number; 107 + } 108 + 109 + const DEFAULT_LETTER_SPACINGS: FacetLetterSpacings = { 110 + tighter: -0.05, 111 + tight: -0.025, 112 + normal: 0, 113 + wide: 0.025, 114 + wider: 0.05, 115 + widest: 0.1, 116 + }; 117 + 118 + export interface TypographySemantic { 119 + size: FacetFontSize; 120 + style: string; 121 + weight: number; 122 + spacing: number; 123 + } 124 + 125 + export interface FacetTypographySemantics { 126 + regular?: TypographySemantic; 127 + [x: string]: TypographySemantic | undefined; 128 + } 129 + 130 + const DEFAULT_TYPOGRAPHY_SEMANTICS: FacetTypographySemantics = { 131 + regular: { 132 + size: DEFAULT_FONT_SIZES.base, 133 + style: DEFAULT_FONT_STYLES.notItalic, 134 + weight: DEFAULT_FONT_WEIGHTS.light, 135 + spacing: DEFAULT_LETTER_SPACINGS.normal, 136 + }, 137 + }; 138 + 139 + /** 140 + * Generally, prefer to use `semantic`, as they are based on the other properties already. 141 + */ 142 + export interface FacetTypography { 143 + families: FacetFontFamilies; 144 + styles: FacetFontStyles; 145 + sizes: FacetFontSizes; 146 + weights: FacetFontWeights; 147 + spacings: FacetLetterSpacings; 148 + tracking: FacetLetterSpacings; 149 + semantic: FacetTypographySemantics; 150 + } 151 + 152 + export const DEFAULT_FACET_TYPOGRAPHY: FacetTypography = { 153 + families: DEFAULT_FONT_FAMILIES, 154 + styles: DEFAULT_FONT_STYLES, 155 + sizes: DEFAULT_FONT_SIZES, 156 + weights: DEFAULT_FONT_WEIGHTS, 157 + spacings: DEFAULT_LETTER_SPACINGS, 158 + tracking: DEFAULT_LETTER_SPACINGS, 159 + semantic: DEFAULT_TYPOGRAPHY_SEMANTICS, 160 + };
+71
src/lib/facet/src/variants.ts
··· 1 + import type { 2 + FacetPalette, 3 + FacetPaletteFundemantals, 4 + } from "@/lib/facet/src/palette"; 5 + 6 + export interface FacetVariants { 7 + pearl?: FacetPalette; 8 + obsidian?: FacetPalette; 9 + [x: string]: FacetPalette | undefined; 10 + } 11 + 12 + // const DEFAULT_LIGHT_MODE: FacetPalette = { 13 + // colors: {}, 14 + // semantic: {}, 15 + // } 16 + // 17 + 18 + const DEFAULT_DARK_PALETTE: FacetPaletteFundemantals = { 19 + crust: "#060116", 20 + mantle: "#0A031F", 21 + base: "#0E0726", 22 + surface0: "#110B24", 23 + surface1: "#241E39", 24 + surface2: "#322D44", 25 + overlay0: "#635F71", 26 + overlay1: "#9D96A8", 27 + overlay2: "#BDB4CB", 28 + subtext0: "#D5CCE3", 29 + subtext1: "#D9CEE8", 30 + text: "#DED5F2", 31 + iolite: "#57E2E5", 32 + lapis: "#125E8A", 33 + sapphire: "#00ABE7", 34 + aquamarine: "#28C2FF", 35 + apatite: "#8EE3EF", 36 + emerald: "#84E296", 37 + citrine: "#E8DB7D", 38 + sunstone: "#E88D67", 39 + garnet: "#B3001B", 40 + ruby: "#D64045", 41 + amethyst: "#BD66D4", 42 + morganite: "#EFBDB1", 43 + coral: "#FFCDB2", 44 + moonstone: "#FFEAD0", 45 + }; 46 + 47 + const DEFAULT_DARK_MODE: FacetPalette = { 48 + colors: DEFAULT_DARK_PALETTE, 49 + semantic: { 50 + primary: DEFAULT_DARK_PALETTE.amethyst, 51 + secondary: DEFAULT_DARK_PALETTE.aquamarine, 52 + background: DEFAULT_DARK_PALETTE.base, 53 + surface: DEFAULT_DARK_PALETTE.surface0, 54 + surfaceVariant: DEFAULT_DARK_PALETTE.surface1, 55 + text: DEFAULT_DARK_PALETTE.text, 56 + textSecondary: DEFAULT_DARK_PALETTE.subtext0, 57 + textTertiary: DEFAULT_DARK_PALETTE.subtext1, 58 + textInverse: DEFAULT_DARK_PALETTE.crust, 59 + positive: DEFAULT_DARK_PALETTE.emerald, 60 + negative: DEFAULT_DARK_PALETTE.ruby, 61 + success: DEFAULT_DARK_PALETTE.emerald, 62 + error: DEFAULT_DARK_PALETTE.ruby, 63 + warning: DEFAULT_DARK_PALETTE.sunstone, 64 + info: DEFAULT_DARK_PALETTE.iolite, 65 + }, 66 + }; 67 + 68 + export const DEFAULT_FACET_VARIANTS: FacetVariants = { 69 + // pearl: DEFAULT_LIGHT_MODE, 70 + obsidian: DEFAULT_DARK_MODE, 71 + };