guide.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import app_hero_planets from "../public/illustrations/app_hero_planets.png"
  2. import app_hero_festival from "../public/illustrations/app_hero_festival.png"
  3. import { withDefaultStaticProps } from "../utils/defaultStaticProps"
  4. import { IconCard } from "../components/IconCard"
  5. import SelectMenu from "../components/SelectMenu"
  6. import { FormattedMessage, useIntl } from "react-intl"
  7. import AppHero from "../components/AppHero"
  8. import React, { useState } from "react"
  9. import Image from "next/legacy/image"
  10. import Hero from "../components/Hero"
  11. import LinkButton from "../components/LinkButton"
  12. import { theme, safelist } from "../tailwind.config.js"
  13. import { AppCard } from "../components/AppCard"
  14. import Layout from "../components/Layout"
  15. import tusky from "../public/apps/tusky.png"
  16. import SkeletonText from "../components/SkeletonText"
  17. import ServerCard from "../components/ServerCard"
  18. import classNames from "classnames"
  19. import TwoUpFeature from "../components/TwoUpFeature"
  20. import ProgressiveWebIcon from "../public/icons/progressive-web.svg?inline"
  21. import ApiGearIcon from "../public/icons/api-gear.svg?inline"
  22. const GuideSection = ({
  23. title,
  24. controls,
  25. children,
  26. }: {
  27. title: React.ReactNode
  28. controls?: React.ReactNode
  29. children: React.ReactNode
  30. }) => {
  31. return (
  32. <section className="">
  33. <h2 className="h5 mb-4">{title}</h2>
  34. {controls && (
  35. <div className="mb-4 flex min-h-[3rem] items-center rounded border border-gray-3 bg-gray-5 px-4">
  36. {controls}
  37. </div>
  38. )}
  39. {children}
  40. </section>
  41. )
  42. }
  43. /** This page does not require translations */
  44. function Guide(props) {
  45. const intl = useIntl()
  46. const [altAppHero, setAltAppHero] = useState(false)
  47. const [skeletonTextSize, setSkeletonTextSize] = useState("b2")
  48. const [serverCardLoading, setServerCardLoading] = useState(false)
  49. return (
  50. <Layout>
  51. <Hero>
  52. <h1 className="h1 mb-4">Style Guide</h1>
  53. <p className="sh1">
  54. A reference of components and design elements, along with their usage
  55. </p>
  56. </Hero>
  57. <div className="mt-16 flex flex-col gap-16">
  58. <h2 className="h2">Styles</h2>
  59. <GuideSection title="Type Scale">
  60. {Object.keys(theme.fontSize).map((name) => (
  61. <div key={name} className="flex items-baseline gap-4">
  62. <div className="b4 flex-0 w-4">{name}</div>
  63. <div className={name}>Find your perfect community</div>
  64. </div>
  65. ))}
  66. </GuideSection>
  67. <GuideSection title="Colors">
  68. <div className="grid grid-cols-1 gap-8">
  69. {Object.keys(theme.colors).map((color) => (
  70. <div key={color}>
  71. <div className="flex space-x-4">
  72. <div className="w-24 shrink-0">
  73. <div className="flex h-10 flex-col justify-center">
  74. <div className="font-semibold">{color}</div>
  75. </div>
  76. </div>
  77. <div className="flex-0 grid min-w-0 grid-cols-6 gap-x-4 gap-y-3">
  78. {(typeof theme.colors[color] === "string"
  79. ? [""]
  80. : Object.keys(theme.colors[color])
  81. ).map((stage) => (
  82. <div key={stage} className="relative flex">
  83. <div className="space-y-1.5">
  84. <div
  85. className={`h-10 w-full rounded bg-${color}${
  86. stage === "" ? "" : `-${stage}`
  87. } border-2 border-solid border-[rgba(0,0,0,0.1)] bg-clip-border`}
  88. />
  89. <div className="px-0.5">
  90. <div className="w-20 font-medium">
  91. {stage || "-"}
  92. </div>
  93. <div className="font-mono lowercase text-gray-2">
  94. {theme.colors[color][stage] ||
  95. theme.colors[color]}
  96. </div>
  97. </div>
  98. </div>
  99. </div>
  100. ))}
  101. </div>
  102. </div>
  103. </div>
  104. ))}
  105. </div>
  106. </GuideSection>
  107. <GuideSection title="Icons">
  108. <div className="flex flex-wrap gap-gutter">
  109. {[
  110. `api-gear`,
  111. `api-window`,
  112. `award`,
  113. `decentralized`,
  114. `donate-box`,
  115. `donate`,
  116. `feed`,
  117. `money`,
  118. `move-servers`,
  119. `move`,
  120. `open-source`,
  121. `price-tag`,
  122. `privacy`,
  123. `progressive-web`,
  124. `safe`,
  125. `safety-1`,
  126. `safety`,
  127. `screen`,
  128. `servers`,
  129. ].map((name) => (
  130. <figure
  131. key={name}
  132. className="relative flex flex-col items-baseline gap-4"
  133. >
  134. <Image
  135. src={require(`../public/icons/${name}.svg`)}
  136. className="aspect-square"
  137. width="120"
  138. height="120"
  139. alt=""
  140. />
  141. <figcaption className="b2">{name}</figcaption>
  142. </figure>
  143. ))}
  144. </div>
  145. </GuideSection>
  146. <h2 className="h2">Components</h2>
  147. <GuideSection title={<code>IconCard</code>}>
  148. <div className="grid grid-cols-[repeat(auto-fill,minmax(300px,1fr))] gap-gutter">
  149. <IconCard
  150. title="Decentralized"
  151. icon="decentralized"
  152. copy={
  153. "Not controlled by a single website or company, Mastodon is a network of completely independent service providers forming a global, cohesive social media platform. "
  154. }
  155. />
  156. </div>
  157. </GuideSection>
  158. <GuideSection title={<code>LinkButton</code>}>
  159. <h4 className="h6">Sizes</h4>
  160. <div className="flex gap-gutter rounded bg-gray-5 p-4">
  161. <LinkButton href="/" size="large">
  162. Large
  163. </LinkButton>
  164. <LinkButton href="/" size="medium">
  165. Medium
  166. </LinkButton>
  167. <LinkButton href="/" size="small">
  168. Small
  169. </LinkButton>
  170. </div>
  171. <h4 className="h6">Colors</h4>
  172. <div className="flex gap-gutter rounded bg-gray-5 p-4">
  173. <LinkButton href="/" size="medium">
  174. Default
  175. </LinkButton>
  176. <LinkButton href="/" size="medium" light>
  177. Light
  178. </LinkButton>
  179. <LinkButton href="/" size="medium" light borderless>
  180. Light, Borderless
  181. </LinkButton>
  182. </div>
  183. </GuideSection>
  184. <GuideSection title={<code>SelectMenu</code>}>
  185. <SelectMenu
  186. label={
  187. <FormattedMessage id="sorting.sort_by" defaultMessage="Sort" />
  188. }
  189. value="all"
  190. onChange={() => {}}
  191. options={[
  192. {
  193. label: intl.formatMessage({
  194. id: "sorting.recently_added",
  195. defaultMessage: "Recently Added",
  196. }),
  197. value: "all",
  198. },
  199. {
  200. label: intl.formatMessage({
  201. id: "sorting.free",
  202. defaultMessage: "Free",
  203. }),
  204. value: "x",
  205. },
  206. {
  207. label: intl.formatMessage({
  208. id: "sorting.alphabetical",
  209. defaultMessage: "A–Z",
  210. }),
  211. value: "y",
  212. },
  213. ]}
  214. />
  215. </GuideSection>
  216. <GuideSection title={<code>AppCard</code>}>
  217. <div className="max-w-sm">
  218. <AppCard
  219. {...{
  220. released_on: "Mar 15, 2017",
  221. name: "Tusky",
  222. icon: tusky,
  223. url: "https://play.google.com/store/apps/details?id=com.keylesspalace.tusky",
  224. paid: false,
  225. category: "android",
  226. categoryLabel: intl.formatMessage({
  227. id: "browse_apps.android",
  228. defaultMessage: "Android",
  229. }),
  230. }}
  231. />
  232. </div>
  233. </GuideSection>
  234. <GuideSection
  235. title={<code>SkeletonText</code>}
  236. controls={
  237. <SelectMenu
  238. label="Size"
  239. value={skeletonTextSize}
  240. onChange={(value) => setSkeletonTextSize(value)}
  241. options={Object.keys(theme.fontSize).map((typeClass) => ({
  242. label: typeClass,
  243. value: typeClass,
  244. }))}
  245. />
  246. }
  247. >
  248. <p className={classNames(skeletonTextSize, "max-w-[20ch]")}>
  249. <SkeletonText className="w-[20ch]" />
  250. <SkeletonText className="w-[20ch]" />
  251. <SkeletonText className="w-[16ch]" />
  252. </p>
  253. </GuideSection>
  254. <GuideSection title={<code>TwoUpFeature</code>}>
  255. <TwoUpFeature
  256. features={[
  257. {
  258. Icon: ProgressiveWebIcon,
  259. title: (
  260. <FormattedMessage
  261. id="browse_apps.progressive_web_app"
  262. defaultMessage="Progressive web app"
  263. />
  264. ),
  265. copy: (
  266. <FormattedMessage
  267. id="browse_apps.you_can_use_it_from_desktop"
  268. defaultMessage="You can always use Mastodon from the browser on your desktop or phone! It can be added to your home screen and some browsers even support push notifications, just like a native app!"
  269. />
  270. ),
  271. cta: (
  272. <FormattedMessage
  273. id="browse_apps.pwa_feature.cta"
  274. defaultMessage="Join a server"
  275. />
  276. ),
  277. cta_link: "/servers",
  278. },
  279. {
  280. Icon: ApiGearIcon,
  281. title: (
  282. <FormattedMessage
  283. id="browse_apps.open_api"
  284. defaultMessage="Open API"
  285. />
  286. ),
  287. copy: (
  288. <FormattedMessage
  289. id="browse_apps.make_your_own"
  290. defaultMessage="Mastodon is open-source and has an elegant, well-documented API that is available to everyone. Make your own app, or use one of the many third-party apps made by other developers!"
  291. />
  292. ),
  293. cta: (
  294. <FormattedMessage
  295. id="browse_apps.api_docs"
  296. defaultMessage="API documentation"
  297. />
  298. ),
  299. cta_link: "https://docs.joinmastodon.org/client/intro/",
  300. },
  301. ]}
  302. />
  303. </GuideSection>
  304. <GuideSection
  305. title={<code>ServerCard</code>}
  306. controls={
  307. <label>
  308. <input
  309. type="checkbox"
  310. onChange={(e) => setServerCardLoading(e.target.checked)}
  311. />{" "}
  312. Loading
  313. </label>
  314. }
  315. >
  316. <div className="max-w-xs">
  317. <ServerCard
  318. server={
  319. serverCardLoading
  320. ? undefined
  321. : {
  322. domain: "mastodon.social",
  323. version: "3.5.3",
  324. description:
  325. "The original server maintained by the Mastodon gGmbH non-profit.",
  326. languages: ["en"],
  327. region: "",
  328. categories: ["general"],
  329. proxied_thumbnail:
  330. "https://proxy.joinmastodon.org/d7d02f9615184131475feeb95ab8cd01e6575448/68747470733a2f2f66696c65732e6d6173746f646f6e2e736f6369616c2f736974655f75706c6f6164732f66696c65732f3030302f3030302f3030312f6f726967696e616c2f766c63736e61702d323031382d30382d32372d31366834336d3131733132372e706e67",
  331. total_users: 805131,
  332. last_week_users: 88052,
  333. approval_required: false,
  334. language: "en",
  335. category: "general",
  336. }
  337. }
  338. />
  339. </div>
  340. </GuideSection>
  341. <GuideSection
  342. title={<code>AppHero</code>}
  343. controls={
  344. <label>
  345. <input
  346. type="checkbox"
  347. onChange={(e) => setAltAppHero(e.target.checked)}
  348. checked={altAppHero}
  349. />
  350. {" Alternate AppHero"}
  351. </label>
  352. }
  353. >
  354. <div>
  355. {altAppHero ? (
  356. <AppHero backgroundImage={app_hero_planets} />
  357. ) : (
  358. <AppHero
  359. backgroundImage={app_hero_festival}
  360. backgroundImagePosition={"center left"}
  361. />
  362. )}
  363. </div>
  364. </GuideSection>
  365. </div>
  366. </Layout>
  367. )
  368. }
  369. export default Guide
  370. export const getStaticProps = withDefaultStaticProps()