90 | {['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'gray'].map((color) => (
91 |
114 | {[
115 | 'bg-app-cyan',
116 | 'bg-app-light-blue',
117 | 'bg-app-dark-blue',
118 | 'bg-app-indigo',
119 | 'bg-app-purple',
120 | 'bg-app-pink',
121 | 'bg-app-orange',
122 | 'bg-app-gold',
123 | 'bg-app-yellow',
124 | 'bg-app-lime',
125 | 'bg-app-light-green',
126 | 'bg-app-dark-green',
127 | ].map((className, index) => (
128 |
129 |
copy(className)}
137 | />
138 |
{className.replace('bg-app-', '')}
139 |
140 | ))}
141 |
142 | >
143 | );
144 | }
145 |
--------------------------------------------------------------------------------
/packages/example-web/pages/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Logo,
3 | DocsLogo,
4 | ExpoGoLogo,
5 | SnackLogo,
6 | WordMarkLogo,
7 | RouterLogo,
8 | OrbitLogo,
9 | mergeClasses,
10 | Button,
11 | } from '@expo/styleguide';
12 | import { CheckCircleIcon } from '@expo/styleguide-icons/outline/CheckCircleIcon';
13 | import { PlaceholderIcon } from '@expo/styleguide-icons/outline/PlaceholderIcon';
14 | import { SearchMdIcon } from '@expo/styleguide-icons/outline/SearchMdIcon';
15 | import { createElement, useState } from 'react';
16 |
17 | import * as StyleguideIcons from '@/common/icon-imports';
18 | import { H1, H3 } from '@/components/headers';
19 | import useCopy from '@/hooks/useCopy';
20 |
21 | type IconNames = keyof typeof StyleguideIcons;
22 |
23 | const iconClasses = mergeClasses(
24 | 'flex flex-col items-center justify-center gap-1 rounded-md border border-transparent px-2 py-4 transition',
25 | 'hover:cursor-pointer hocus:border-secondary hocus:shadow-xs',
26 | 'active:scale-98'
27 | );
28 |
29 | export default function IconsPage() {
30 | const [, copy] = useCopy();
31 | const [filters, setFilters] = useState({ outline: true, solid: false, duotone: false });
32 | const [search, setSearch] = useState('');
33 |
34 | const iconNames = Object.keys(StyleguideIcons)
35 | .filter((key) => {
36 | let skip = false;
37 | if (!filters.solid) skip = skip || key.endsWith('SolidIcon');
38 | if (!filters.duotone) skip = skip || key.endsWith('DuotoneIcon');
39 | if (!filters.outline) skip = skip || (!key.endsWith('SolidIcon') && !key.endsWith('DuotoneIcon'));
40 | return !skip;
41 | })
42 | .filter((key) => (search ? key.toLowerCase().includes(search) : true))
43 | .filter((key) => key.endsWith('Icon')) as IconNames[];
44 |
45 | return (
46 |
47 |
Icons
48 |
Logos
49 |
50 | {[
51 | { name: 'WordMarkLogo', element: WordMarkLogo },
52 | { name: 'Logo', element: Logo },
53 | { name: 'DocsLogo', element: DocsLogo },
54 | { name: 'ExpoGoLogo', element: ExpoGoLogo },
55 | { name: 'OrbitLogo', element: OrbitLogo },
56 | { name: 'RouterLogo', element: RouterLogo },
57 | { name: 'SnackLogo', element: SnackLogo },
58 | ].map((logo) => (
59 |
copy(logo.name)} key={logo.name}>
60 | {createElement(logo.element, { className: 'icon-xl text-default' })}
61 | {logo.name}
62 |
63 | ))}
64 |
65 |
Icon set
66 |
67 |
68 |
69 | setSearch(e.target.value)}
72 | />
73 |
74 |
75 |
:
79 | }
80 | onClick={() => setFilters((prevState) => ({ ...prevState, outline: !prevState.outline }))}>
81 | Outline
82 |
83 |
:
}
86 | onClick={() => setFilters((prevState) => ({ ...prevState, solid: !prevState.solid }))}>
87 | Solid
88 |
89 |
:
93 | }
94 | onClick={() => setFilters((prevState) => ({ ...prevState, duotone: !prevState.duotone }))}>
95 | Duotone
96 |
97 |
98 |
99 |
100 | {iconNames.map((iconName) => (
101 |
copy(iconName)} key={iconName}>
102 | {createElement(StyleguideIcons[iconName], { className: 'icon-xl text-default translate-z' })}
103 | {iconName}
104 |
105 | ))}
106 |
107 |
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/packages/example-web/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { WordMarkLogo } from '@expo/styleguide';
2 |
3 | import { H1 } from '@/components/headers';
4 |
5 | export default function HomePage() {
6 | return (
7 | <>
8 |
9 | @expo/styleguide
10 |
11 |
12 | A collection of packages used to share styles and icons across{' '}
13 | websites and projects.
14 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/example-web/pages/layouts.tsx:
--------------------------------------------------------------------------------
1 | import { mergeClasses } from '@expo/styleguide';
2 |
3 | import { H1, H3 } from '@/components/headers';
4 |
5 | const VIEWPORT_CLASS = mergeClasses(
6 | 'mx-auto mt-4 border border-default bg-screen px-6 pt-4 pb-5 rounded-lg shadow-xs'
7 | );
8 | const SCREEN_CLASS = mergeClasses('mx-auto mt-4 min-h-28 border border-secondary bg-default px-4 pt-3 rounded-md');
9 |
10 | export default function LayoutsPage() {
11 | return (
12 | <>
13 |
Layouts
14 |
screen-2xl
15 |
16 | max-2xl-gutters:
scope or{' '}
17 |
18 |
1572px
19 |
20 |
21 | 1524px
22 |
23 |
24 |
screen-xl
25 |
26 | max-xl-gutters:
scope or{' '}
27 |
28 |
1248px
29 |
30 |
31 | 1200px
32 |
33 |
34 |
screen-lg
35 |
36 | max-lg-gutters:
scope or{' '}
37 |
38 |
1008px
39 |
40 |
41 | 960px
42 |
43 |
44 |
screen-md
45 |
46 | max-md-gutters:
scope or{' '}
47 |
48 |
788px
49 |
50 |
51 | 740px
52 |
53 |
54 |
screen-sm
55 |
56 | max-sm-gutters:
scope or{' '}
57 |
58 |
468px
59 |
60 |
61 | 420px
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | function WidthExplanation({ screen }: { screen: string }) {
69 | return (
70 | <>
71 |
(w
/min-w
72 |
/max-w
)-{screen}
73 |
74 | >
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/packages/example-web/pages/typography.tsx:
--------------------------------------------------------------------------------
1 | import { DemoTile } from '@/components/DemoTile';
2 | import { H1, H3 } from '@/components/headers';
3 |
4 | export default function TypographyPage() {
5 | return (
6 | <>
7 |
Typography
8 |
Headings classes
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
Text classes
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | Elements (legacy)
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | >
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/packages/example-web/pages/ui/components.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonBase, Button, Link, LinkBase, type ButtonTheme } from '@expo/styleguide';
2 | import { EasMetadataIcon } from '@expo/styleguide-icons/custom/EasMetadataIcon';
3 | import { AlignTopArrow01Icon } from '@expo/styleguide-icons/outline/AlignTopArrow01Icon';
4 | import { ArrowUpRightIcon } from '@expo/styleguide-icons/outline/ArrowUpRightIcon';
5 | import { BookClosedIcon } from '@expo/styleguide-icons/outline/BookClosedIcon';
6 | import { Diamond01Icon } from '@expo/styleguide-icons/outline/Diamond01Icon';
7 | import { Trash01Icon } from '@expo/styleguide-icons/outline/Trash01Icon';
8 | import { Fragment } from 'react';
9 |
10 | import { ButtonsRow } from '@/components/ButtonsRow';
11 | import { DemoTile } from '@/components/DemoTile';
12 | import { H1, H3 } from '@/components/headers';
13 |
14 | const THEMES = [
15 | 'primary',
16 | 'secondary',
17 | 'tertiary',
18 | 'quaternary',
19 | 'primary-destructive',
20 | 'secondary-destructive',
21 | 'tertiary-destructive',
22 | ] as ButtonTheme[];
23 |
24 | export default function ComponentsPage() {
25 | return (
26 | <>
27 |
UI: Components
28 |
Inline Help
29 |
30 |
31 |
32 | Info text
33 |
34 |
35 |
36 |
37 |
38 | Warning text
39 |
40 |
41 |
42 |
43 |
44 | Danger text
45 |
46 |
47 |
48 |
49 |
50 | Success text
51 |
52 |
53 |
Link Base
54 |
55 | LinkBase
56 |
57 |
58 | LinkBase
59 |
60 |
61 |
62 | LinkBase
63 |
64 |
65 |
66 | LinkBase
67 |
68 |
69 |
70 | LinkBase
71 |
72 |
73 |
Link
74 |
75 | Link
76 |
77 |
Button Base
78 |
79 | alert('ButtonBase clicked')}>ButtonBase
80 |
81 |
82 | {THEMES.map((buttonTheme) => (
83 |
84 |
85 |
86 |
87 | ))}
88 |
89 | {THEMES.map((buttonTheme) => (
90 |
91 |
92 |
93 | ))}
94 |
Link Buttons
95 |
96 | }>
97 | Scroll top
98 |
99 |
100 |
101 | }>
102 | Snack
103 |
104 |
105 |
106 | }
109 | openInNewTab
110 | theme="tertiary">
111 | Docs
112 |
113 |
114 |
115 | }
119 | openInNewTab
120 | theme="secondary-destructive">
121 | Delete
122 |
123 |
124 |
125 |
128 |
129 |
Customized Buttons
130 |
131 | }>
132 | Subscribe
133 |
134 | }>
140 | Subscribe
141 |
142 |
143 |
144 | }>
147 | EAS Metadata
148 |
149 | }>
153 | EAS Metadata
154 |
155 |
156 |
157 | }
161 | testID="test-button"
162 | skipCapitalization>
163 | Check status
164 |
165 | }
170 | testID="test-link"
171 | skipCapitalization>
172 | Check status
173 |
174 |
175 |
176 |
177 |
180 |
183 |
184 |
185 | >
186 | );
187 | }
188 |
--------------------------------------------------------------------------------
/packages/example-web/pages/ui/search.tsx:
--------------------------------------------------------------------------------
1 | import { CommandMenuTrigger } from '@expo/styleguide-search-ui';
2 |
3 | import { useSearchDialogContext } from '@/common/SearchDialogContext';
4 | import { DemoTile } from '@/components/DemoTile';
5 | import { H1, H3 } from '@/components/headers';
6 |
7 | export default function SearchPage() {
8 | const { setOpen } = useSearchDialogContext();
9 | return (
10 | <>
11 |
UI: Search
12 |
@expo/search-ui
13 |
14 |
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/packages/example-web/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-import': {},
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/packages/example-web/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-web/public/favicon.png
--------------------------------------------------------------------------------
/packages/example-web/public/global.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/packages/example-web/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/expo/styleguide/fd85f92dfd9faa2dbf2d0880ffc023dde7fd2312/packages/example-web/public/icon.png
--------------------------------------------------------------------------------
/packages/example-web/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const expoTheme = require('@expo/styleguide/tailwind');
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: [
6 | './common/**/*.{js,ts,jsx,tsx}',
7 | './components/**/*.{js,ts,jsx,tsx}',
8 | './pages/**/*.{js,ts,jsx,tsx}',
9 | `../../node_modules/@expo/styleguide/dist/**/*.{js,ts,jsx,tsx}`,
10 | `../../node_modules/@expo/styleguide-search-ui/dist/**/*.{js,ts}`,
11 | ],
12 | ...expoTheme,
13 | };
14 |
--------------------------------------------------------------------------------
/packages/example-web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "baseUrl": ".",
20 | "paths": {
21 | "@/*": [
22 | "./*"
23 | ]
24 | },
25 | "module": "esnext",
26 | "moduleResolution": "node",
27 | "plugins": [
28 | {
29 | "name": "next"
30 | }
31 | ]
32 | },
33 | "include": [
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | ".next/types/**/*.ts"
38 | ],
39 | "exclude": [
40 | "node_modules"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/packages/search-ui/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Expo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/search-ui/README.md:
--------------------------------------------------------------------------------
1 | # @expo/styleguide-search-ui
2 |
3 | Expo's common search component for use on the web.
4 |
5 | ## Usage
6 |
7 | 1. Install Expo Search UI package:
8 | ```shell
9 | yarn add @expo/styleguide-search-ui
10 | ```
11 | 2. Import global CSS files from the package in your JS(X)/TS(X) code:
12 | ```jsx
13 | import "@expo/styleguide-search-ui/dist/expo-search-ui.css";
14 | ```
15 | or import it the main stylesheet file:
16 | ```css
17 | @import "@expo/styleguide-search-ui/dist/expo-search-ui.css";
18 | ```
19 | 3. Add `'./node_modules/@expo/styleguide-search-ui/dist/**/*.{js,ts,jsx,tsx}'` to the Tailwind `content` paths.
20 |
21 | ## Development
22 |
23 | ### Get started
24 |
25 | 1. Install dependencies with `yarn`.
26 | 2. Build everything with `yarn build`.
27 | 3. Develop with `yarn dev`.
28 |
--------------------------------------------------------------------------------
/packages/search-ui/index.ts:
--------------------------------------------------------------------------------
1 | import { Command } from 'cmdk';
2 |
3 | export { CommandMenu } from './src/components/CommandMenu';
4 | export { CommandMenuTrigger } from './src/components/CommandMenuTrigger';
5 | export { CommandItemBaseWithCopy } from './src/components/CommandItemBaseWithCopy';
6 |
7 | export { addHighlight } from './src/utils';
8 | export * from './src/types';
9 |
10 | export const CommandItemBase = Command.Item;
11 |
--------------------------------------------------------------------------------
/packages/search-ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@expo/styleguide-search-ui",
3 | "version": "2.3.6",
4 | "description": "Expo's common search component for use on the web.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "clean": "rimraf dist",
12 | "bundle": "rollup --config",
13 | "build": "run-s clean bundle",
14 | "dev": "rollup --config --watch"
15 | },
16 | "homepage": "https://github.com/expo/styleguide",
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/expo/styleguide.git",
20 | "directory": "packages/search-ui"
21 | },
22 | "keywords": [
23 | "expo"
24 | ],
25 | "author": "Expo",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/expo/styleguide/issues"
29 | },
30 | "dependencies": {
31 | "@expo/styleguide": "^9.1.2",
32 | "@expo/styleguide-icons": "^2.2.2",
33 | "@sanity/client": "^6.28.3",
34 | "@sanity/image-url": "^1.1.0",
35 | "cmdk": "^0.2.1",
36 | "lodash.groupby": "^4.6.0"
37 | },
38 | "devDependencies": {
39 | "@types/lodash.groupby": "^4.6.9",
40 | "npm-run-all": "*",
41 | "rimraf": "*",
42 | "rollup": "*",
43 | "tailwindcss": "^3.4.17",
44 | "user-agent-data-types": "^0.4.2"
45 | },
46 | "peerDependencies": {
47 | "next": ">= 13",
48 | "react": ">= 16"
49 | },
50 | "eslintConfig": {
51 | "extends": "universe/web"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/packages/search-ui/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 | import copy from 'rollup-plugin-copy';
4 |
5 | const config = [
6 | {
7 | input: 'index.ts',
8 | output: {
9 | dir: 'dist',
10 | format: 'cjs',
11 | },
12 | plugins: [
13 | typescript(),
14 | terser(),
15 | copy({
16 | targets: [
17 | { src: './src/styles/expo-search-ui.css', dest: 'dist' },
18 | ],
19 | }),
20 | ],
21 | external: [
22 | '@expo/styleguide',
23 | '@expo/styleguide-base',
24 | '@expo/styleguide-icons',
25 | '@sanity/client',
26 | '@sanity/image-url',
27 | 'cmdk',
28 | 'lodash.groupby',
29 | 'react',
30 | 'tailwind-merge'
31 | ],
32 | },
33 | ];
34 |
35 | export default config;
36 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/ExpoBlogItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ExternalLinkIcon } from './icons';
4 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy';
5 | import { ExpoBlogItemType } from '../types';
6 | import { addHighlight, getSanityAsset } from '../utils';
7 |
8 | type Props = {
9 | item: ExpoBlogItemType;
10 | query: string;
11 | onSelect?: () => void;
12 | };
13 |
14 | export const ExpoBlogItem = ({ item, onSelect, query }: Props) => {
15 | return (
16 |
21 |
22 |
})
27 |
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/ExpoDocsItem.tsx:
--------------------------------------------------------------------------------
1 | import { DocsLogo, mergeClasses } from '@expo/styleguide';
2 | import { PlanEnterpriseIcon } from '@expo/styleguide-icons/custom/PlanEnterpriseIcon';
3 | import { BookOpen02Icon } from '@expo/styleguide-icons/outline/BookOpen02Icon';
4 | import { GraduationHat02Icon } from '@expo/styleguide-icons/outline/GraduationHat02Icon';
5 | import { Hash02Icon } from '@expo/styleguide-icons/outline/Hash02Icon';
6 | import { Home02Icon } from '@expo/styleguide-icons/outline/Home02Icon';
7 | import React from 'react';
8 |
9 | import { FootnoteSection } from './FootnoteSection';
10 | import { ExternalLinkIcon, FootnoteArrowIcon } from './icons';
11 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy';
12 | import type { AlgoliaItemType } from '../types';
13 | import {
14 | getContentHighlightHTML,
15 | getHighlightHTML,
16 | isReferencePath,
17 | isEASPath,
18 | isHomePath,
19 | isLearnPath,
20 | } from '../utils';
21 |
22 | type Props = {
23 | item: AlgoliaItemType;
24 | onSelect?: () => void;
25 | isNested?: boolean;
26 | transformUrl?: (url: string) => string;
27 | };
28 |
29 | type ItemIconProps = {
30 | url: string;
31 | className?: string;
32 | isNested?: boolean;
33 | };
34 |
35 | const ItemIcon = ({ url, className, isNested }: ItemIconProps) => {
36 | if (isNested) {
37 | return
;
38 | } else if (isReferencePath(url)) {
39 | return
;
40 | } else if (isEASPath(url)) {
41 | return
;
42 | } else if (isHomePath(url)) {
43 | return
;
44 | } else if (isLearnPath(url)) {
45 | return
;
46 | }
47 | return
;
48 | };
49 |
50 | const getFootnotePrefix = (url: string) => {
51 | if (isReferencePath(url)) {
52 | return 'Reference';
53 | } else if (isEASPath(url)) {
54 | return 'Expo Application Services';
55 | } else if (isHomePath(url)) {
56 | return 'Home';
57 | } else if (isLearnPath(url)) {
58 | return 'Learn';
59 | } else {
60 | return 'Guides';
61 | }
62 | };
63 |
64 | const ItemFootnotePrefix = ({ url, isNested = false }: { url: string; isNested?: boolean }) => {
65 | return isNested ? (
66 | <>
67 |
{getFootnotePrefix(url)}
68 |
69 | >
70 | ) : (
71 |
{getFootnotePrefix(url)}
72 | );
73 | };
74 |
75 | export const ExpoDocsItem = ({ item, onSelect, isNested, transformUrl }: Props) => {
76 | const { lvl0, lvl2, lvl3, lvl4, lvl6 } = item.hierarchy;
77 |
78 | const titleClasses = mergeClasses(isNested ? 'text-2xs' : 'text-xs font-medium');
79 | const hierarchyClasses = mergeClasses('text-3xs text-quaternary', isNested && 'hidden');
80 |
81 | return (
82 |
89 |
90 |
91 |
92 |
93 | {lvl6 && (
94 | <>
95 |
96 | {!isNested && (
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | )}
105 | >
106 | )}
107 | {!lvl6 && lvl4 && (
108 | <>
109 |
110 | {!isNested && (
111 |
112 |
113 |
114 |
115 |
116 |
117 | )}
118 | >
119 | )}
120 | {!lvl6 && !lvl4 && lvl3 && (
121 | <>
122 |
123 | {!isNested && (
124 |
125 |
126 |
127 |
128 |
129 | )}
130 | >
131 | )}
132 | {!lvl6 && !lvl4 && !lvl3 && lvl2 && (
133 | <>
134 |
135 | {!isNested && (
136 |
137 |
138 |
139 |
140 | )}
141 | >
142 | )}
143 | {!lvl6 && !lvl4 && !lvl3 && !lvl2 && lvl0 && (
144 | <>
145 |
146 |
147 | >
148 | )}
149 | {(!isNested || item.content) && (
150 |
154 | )}
155 |
156 |
157 | {!transformUrl &&
}
158 |
159 |
160 | );
161 | };
162 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/FootnoteSection.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FootnoteArrowIcon } from './icons';
4 | import type { AlgoliaItemHierarchy, AlgoliaItemType } from '../types';
5 | import { getHighlightHTML } from '../utils';
6 |
7 | export const FootnoteSection = ({
8 | item,
9 | levelKey = 'lvl0',
10 | }: {
11 | item: AlgoliaItemType;
12 | levelKey: keyof AlgoliaItemHierarchy
;
13 | }) =>
14 | item.hierarchy[levelKey] ? (
15 | <>
16 |
17 |
18 | >
19 | ) : null;
20 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/RNDirectoryItem.tsx:
--------------------------------------------------------------------------------
1 | import { GithubIcon } from '@expo/styleguide-icons/custom/GithubIcon';
2 | import { Download01Icon } from '@expo/styleguide-icons/outline/Download01Icon';
3 | import { Star01Icon } from '@expo/styleguide-icons/outline/Star01Icon';
4 | import React from 'react';
5 |
6 | import { ExternalLinkIcon } from './icons';
7 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy';
8 | import type { RNDirectoryItemType } from '../types';
9 | import { addHighlight } from '../utils';
10 |
11 | type Props = {
12 | item: RNDirectoryItemType;
13 | query: string;
14 | onSelect?: () => void;
15 | };
16 |
17 | const numberFormat = new Intl.NumberFormat();
18 |
19 | export const RNDirectoryItem = ({ item, onSelect, query }: Props) => {
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {numberFormat.format(item.github.stats.stars)} stars
29 | {item.npm.downloads ? (
30 | <>
31 | {' '}
32 | ·
33 | {numberFormat.format(item.npm.downloads)} downloads
34 | >
35 | ) : undefined}
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/RNDocsItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { FootnoteSection } from './FootnoteSection';
4 | import { ExternalLinkIcon, ReactIcon } from './icons';
5 | import { CommandItemBaseWithCopy } from '../components/CommandItemBaseWithCopy';
6 | import type { AlgoliaItemType } from '../types';
7 | import { getContentHighlightHTML, getHighlightHTML } from '../utils';
8 |
9 | type Props = {
10 | item: AlgoliaItemType;
11 | onSelect?: () => void;
12 | };
13 |
14 | export const RNDocsItem = ({ item, onSelect }: Props) => {
15 | const { lvl0, lvl1, lvl2, lvl3, lvl4 } = item.hierarchy;
16 | return (
17 |
18 |
19 |
20 |
21 | {lvl4 && (
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | >
31 | )}
32 | {!lvl4 && lvl3 && (
33 | <>
34 |
35 |
36 |
37 |
38 |
39 |
40 | >
41 | )}
42 | {!lvl3 && lvl2 && (
43 | <>
44 |
45 |
46 |
47 |
48 |
49 | >
50 | )}
51 | {!lvl3 && !lvl2 && lvl1 && (
52 | <>
53 |
54 |
55 | >
56 | )}
57 | {!lvl3 && !lvl2 && !lvl1 && lvl0 &&
}
58 |
59 |
60 |
61 |
62 |
63 | );
64 | };
65 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/icons.tsx:
--------------------------------------------------------------------------------
1 | import { mergeClasses } from '@expo/styleguide';
2 | import { ArrowUpRightIcon } from '@expo/styleguide-icons/outline/ArrowUpRightIcon';
3 | import { ChevronRightIcon } from '@expo/styleguide-icons/outline/ChevronRightIcon';
4 | import React, { HTMLAttributes } from 'react';
5 |
6 | export function FootnoteArrowIcon() {
7 | return ;
8 | }
9 |
10 | export const ExternalLinkIcon = () => ;
11 |
12 | export const ReactIcon = ({ className }: HTMLAttributes) => (
13 |
21 | );
22 |
23 | export const AlgoliaLogo = () => (
24 |
77 | );
78 |
--------------------------------------------------------------------------------
/packages/search-ui/src/Items/index.ts:
--------------------------------------------------------------------------------
1 | export { RNDirectoryItem } from './RNDirectoryItem';
2 | export { RNDocsItem } from './RNDocsItem';
3 | export { ExpoDocsItem } from './ExpoDocsItem';
4 |
--------------------------------------------------------------------------------
/packages/search-ui/src/components/BarLoader.tsx:
--------------------------------------------------------------------------------
1 | import { mergeClasses } from '@expo/styleguide';
2 | import React from 'react';
3 |
4 | type Props = { isLoading?: boolean };
5 |
6 | export const BarLoader = ({ isLoading }: Props) => (
7 |
14 | );
15 |
--------------------------------------------------------------------------------
/packages/search-ui/src/components/CommandFooter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { AlgoliaLogo } from '../Items/icons';
4 |
5 | export const CommandFooter = () => (
6 |
7 |
8 | ↵
9 | to select
10 |
11 |
12 | ↑
13 | ↓
14 | to navigate
15 |
16 |
17 | esc
18 | to close
19 |
20 |
21 | Search by
22 |
25 |
26 |
27 |
28 |
29 | );
30 |
--------------------------------------------------------------------------------
/packages/search-ui/src/components/CommandItemBaseWithCopy.tsx:
--------------------------------------------------------------------------------
1 | import { mergeClasses } from '@expo/styleguide';
2 | import { Command } from 'cmdk';
3 | import React, { PropsWithChildren, useState } from 'react';
4 |
5 | import { openLink } from '../utils';
6 |
7 | type Props = PropsWithChildren<{
8 | url: string;
9 | onSelect?: () => void;
10 | isExternalLink?: boolean;
11 | isNested?: boolean;
12 | className?: string;
13 | value?: string;
14 | }>;
15 |
16 | export const CommandItemBaseWithCopy = ({
17 | children,
18 | url,
19 | isExternalLink,
20 | isNested,
21 | onSelect,
22 | className,
23 | value,
24 | }: Props) => {
25 | const [copyDone, setCopyDone] = useState(false);
26 | const [isMetaClick, setMetaClick] = useState(false);
27 |
28 | const copyUrl = () => {
29 | navigator.clipboard?.writeText(url);
30 | setCopyDone(true);
31 | setTimeout(() => setCopyDone(false), 1500);
32 | };
33 |
34 | return (
35 | {
40 | if (event.metaKey || event.ctrlKey) {
41 | setMetaClick(true);
42 | }
43 | }}
44 | onMouseUp={(event) => {
45 | // note(Keith): middle click (typical *nix copy shortcut), right click (works with Mac trackpads), onAuxClick is not supported in Safari
46 | if (event.button === 1 || event.button === 2) {
47 | copyUrl();
48 | }
49 | }}
50 | onSelect={() => {
51 | openLink(url, isMetaClick ? true : isExternalLink);
52 | if (isMetaClick) {
53 | setMetaClick(false);
54 | } else {
55 | onSelect && onSelect();
56 | }
57 | }}
58 | onContextMenu={(event) => {
59 | event.preventDefault();
60 | }}>
61 | {children}
62 | {copyDone && (
63 |
64 | Copied!
65 |
66 | )}
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/packages/search-ui/src/components/CommandMenu.tsx:
--------------------------------------------------------------------------------
1 | import { SearchSmIcon } from '@expo/styleguide-icons/outline/SearchSmIcon';
2 | import { XIcon } from '@expo/styleguide-icons/outline/XIcon';
3 | import { Command } from 'cmdk';
4 | import groupBy from 'lodash.groupby';
5 | import React, { useEffect, useState, Dispatch, SetStateAction } from 'react';
6 |
7 | import { BarLoader } from './BarLoader';
8 | import { CommandFooter } from './CommandFooter';
9 | import { RNDirectoryItem, RNDocsItem, ExpoDocsItem } from '../Items';
10 | import { ExpoBlogItem } from '../Items/ExpoBlogItem';
11 | import type { RNDirectoryItemType, AlgoliaItemType, CommandMenuConfig, CommandMenuSection } from '../types';
12 | import { ExpoBlogItemType } from '../types';
13 | import {
14 | getExpoDocsResults,
15 | getRNDocsResults,
16 | getDirectoryResults,
17 | getItemsAsync,
18 | isAppleDevice,
19 | getExpoBlogResults,
20 | getSanityItemsAsync,
21 | } from '../utils';
22 |
23 | type Props = {
24 | open: boolean;
25 | setOpen: Dispatch>;
26 | config: CommandMenuConfig;
27 | customSections?: CommandMenuSection[];
28 | };
29 |
30 | export const CommandMenu = ({
31 | config: { docsVersion, docsTransformUrl },
32 | open,
33 | setOpen,
34 | customSections = [],
35 | }: Props) => {
36 | const [initialized, setInitialized] = useState(false);
37 | const [loading, setLoading] = useState(true);
38 | const [query, setQuery] = useState('');
39 | const [isMac, setIsMac] = useState(null);
40 |
41 | const [expoDocsItems, setExpoDocsItems] = useState([]);
42 | const [expoBlogItems, setExpoBlogItems] = useState([]);
43 | const [rnDocsItems, setRnDocsItems] = useState([]);
44 | const [directoryItems, setDirectoryItems] = useState([]);
45 |
46 | const getExpoDocsItems = async () => getItemsAsync(query, getExpoDocsResults, setExpoDocsItems, docsVersion);
47 | const getExpoBlogItems = async () => getSanityItemsAsync(query, getExpoBlogResults, setExpoBlogItems);
48 | const getRNDocsItems = async () => getItemsAsync(query, getRNDocsResults, setRnDocsItems);
49 | const getDirectoryItems = async () => getItemsAsync(query, getDirectoryResults, setDirectoryItems);
50 |
51 | const dismiss = () => setOpen(false);
52 |
53 | const fetchData = (callback: () => void) => {
54 | Promise.all([
55 | getExpoDocsItems(),
56 | getExpoBlogItems(),
57 | getRNDocsItems(),
58 | getDirectoryItems(),
59 | ...customSections?.map((section) => section.getItemsAsync(query)),
60 | ]).then(callback);
61 | };
62 |
63 | const onQueryChange = () => {
64 | if (open) {
65 | setLoading(true);
66 | const inputTimeout = setTimeout(() => fetchData(() => setLoading(false)), 150);
67 | return () => clearTimeout(inputTimeout);
68 | }
69 | };
70 |
71 | const onMenuOpen = () => {
72 | if (open && !initialized) {
73 | fetchData(() => {
74 | setInitialized(true);
75 | setLoading(false);
76 | });
77 | }
78 | };
79 |
80 | useEffect(() => {
81 | setIsMac(typeof navigator !== 'undefined' && isAppleDevice());
82 | }, []);
83 |
84 | useEffect(() => {
85 | if (isMac !== null) {
86 | const keyDownListener = (e: KeyboardEvent) => {
87 | if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
88 | e.preventDefault();
89 | setOpen((open) => !open);
90 | }
91 | };
92 | document.addEventListener('keydown', keyDownListener, false);
93 | return () => document.removeEventListener('keydown', keyDownListener);
94 | }
95 | }, [isMac]);
96 |
97 | useEffect(onMenuOpen, [open]);
98 | useEffect(onQueryChange, [query]);
99 |
100 | const expoDocsGroupedItems = groupBy(
101 | expoDocsItems.map((expoDocsItem: AlgoliaItemType) => ({
102 | ...expoDocsItem,
103 | baseUrl: expoDocsItem.url.replace(/#.+/, ''),
104 | })),
105 | 'baseUrl'
106 | );
107 |
108 | const data = [
109 | expoDocsItems.length > 0 && (
110 |
111 | {Object.values(expoDocsGroupedItems).map((values) =>
112 | values
113 | .sort((a, b) => a.url.localeCompare(a.baseUrl) - b.url.localeCompare(b.baseUrl))
114 | .slice(0, 6)
115 | .map((item, index) => (
116 |
123 | ))
124 | )}
125 |
126 | ),
127 | expoBlogItems.length > 0 && (
128 |
129 | {expoBlogItems.map((item) => (
130 |
131 | ))}
132 |
133 | ),
134 | rnDocsItems.length > 0 && (
135 |
136 | {rnDocsItems.map((item) => (
137 |
138 | ))}
139 |
140 | ),
141 | directoryItems.length > 0 && (
142 |
143 | {directoryItems.map((item) => (
144 |
145 | ))}
146 |
147 | ),
148 | ];
149 |
150 | customSections?.forEach(
151 | ({ items, heading, sectionIndex }) =>
152 | items.length > 0 &&
153 | data.splice(
154 | sectionIndex,
155 | 0,
156 |
157 | {items}
158 |
159 | )
160 | );
161 |
162 | return (
163 |
164 |
165 |
166 | setOpen(false)} />
167 |
168 |
169 |
170 |
171 | {initialized && data}
172 | {data.filter(Boolean).length === 0 && (
173 |
174 | No results found.
175 |
176 | )}
177 |
178 |
179 |
180 | );
181 | };
182 |
--------------------------------------------------------------------------------
/packages/search-ui/src/components/CommandMenuTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { Button, mergeClasses } from '@expo/styleguide';
2 | import { SearchSmIcon } from '@expo/styleguide-icons/outline/SearchSmIcon';
3 | import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
4 |
5 | import { isAppleDevice } from '../utils';
6 |
7 | type Props = {
8 | setOpen: Dispatch>;
9 | className?: string;
10 | };
11 |
12 | export const CommandMenuTrigger = ({ setOpen, className }: Props) => {
13 | const [isMac, setIsMac] = useState(null);
14 |
15 | useEffect(() => {
16 | setIsMac(typeof navigator !== 'undefined' && isAppleDevice());
17 | }, []);
18 |
19 | return (
20 | }
23 | rightSlot={
24 | isMac !== null ? (
25 |
26 | {isMac ? '⌘' : 'Ctrl'}{' '}
27 | K
28 |
29 | ) : undefined
30 | }
31 | className={mergeClasses(
32 | 'cmdk-trigger bg-default pl-2.5 pr-3 border border-default shadow-xs min-h-[40px]',
33 | className
34 | )}
35 | onClick={() => setOpen(true)}>
36 | Search
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/packages/search-ui/src/styles/expo-search-ui.css:
--------------------------------------------------------------------------------
1 | @keyframes searchUIBarLoader {
2 | 0% {
3 | width: 0;
4 | }
5 | 80% {
6 | width: 100%;
7 | opacity: 1;
8 | }
9 | 100% {
10 | width: 100%;
11 | opacity: 0;
12 | }
13 | }
14 |
15 | #__next[aria-hidden] {
16 | filter: blur(3.33px);
17 | }
18 |
19 | [cmdk-overlay] {
20 | background-color: rgba(0, 0, 0, 0.33);
21 | height: 100vh;
22 | left: 0;
23 | position: fixed;
24 | top: 0;
25 | width: 100vw;
26 | z-index: 1000;
27 | }
28 |
29 | [cmdk-root] {
30 | position: fixed;
31 | top: 45%;
32 | left: 50%;
33 | transform: translate(-50%, -50%);
34 | min-height: 75vh;
35 | max-height: 75vh;
36 | background: var(--expo-theme-background-default);
37 | border-radius: 10px;
38 | box-shadow: var(--expo-theme-shadows-sm);
39 | width: 40vw;
40 | min-width: 680px;
41 | border: 1px solid var(--expo-theme-border-default);
42 | z-index: 1001;
43 | }
44 |
45 | @media screen and (max-width: 788px) {
46 | [cmdk-root] {
47 | top: 50%;
48 | width: 96vw;
49 | min-width: 96vw;
50 | min-height: 96dvh;
51 | max-height: 96dvh;
52 | }
53 | }
54 |
55 | [cmdk-root] kbd, .cmdk-trigger kbd {
56 | font-size: 0.8125rem;
57 | line-height: 19px;
58 | letter-spacing: -0.003rem;
59 | background-color: var(--expo-theme-background-subtle);
60 | border: 1px solid var(--expo-theme-border-default);
61 | white-space: pre-wrap;
62 | font-weight: 500;
63 | color: var(--expo-theme-text-secondary);
64 | padding: 0 4px;
65 | box-shadow: 0 0.1rem 0 1px var(--expo-theme-border-default);
66 | border-radius: 4px;
67 | position: relative;
68 | display: inline-flex;
69 | min-width: 22px;
70 | justify-content: center;
71 | top: -1px;
72 | }
73 |
74 | [cmdk-input] {
75 | appearance: none;
76 | background: transparent;
77 | color: var(--expo-theme-text-default);
78 | flex: 1;
79 | font: inherit;
80 | height: 100%;
81 | outline: none;
82 | padding: 0 44px;
83 | min-height: 46px;
84 | margin: 16px 16px 0;
85 | border: 1px solid var(--expo-theme-border-default);
86 | border-radius: 6px;
87 | width: calc(100% - 32px);
88 | box-sizing: border-box;
89 | box-shadow: var(--expo-theme-shadows-xs);
90 | }
91 |
92 | [cmdk-input]::placeholder {
93 | color: var(--expo-theme-icon-secondary);
94 | }
95 |
96 | [cmdk-item] {
97 | content-visibility: auto;
98 | cursor: pointer;
99 | min-height: 52px;
100 | border-radius: 6px;
101 | display: flex;
102 | flex-direction: column;
103 | justify-content: center;
104 | padding: 4px 12px;
105 | color: var(--expo-theme-text-default);
106 | user-select: none;
107 | will-change: background, color;
108 | transition: all 150ms ease;
109 | transition-property: none;
110 | }
111 |
112 | [cmdk-item][aria-selected='true'],
113 | [cmdk-item]:active {
114 | background: var(--expo-theme-background-element);
115 | color: var(--expo-theme-text-default);
116 | }
117 |
118 | [cmdk-item][aria-disabled='true'] {
119 | color: var(--expo-theme-icon-secondary);
120 | cursor: not-allowed;
121 | }
122 |
123 | [cmdk-item] + [cmdk-item] {
124 | margin-top: 4px;
125 | }
126 |
127 | [cmdk-item] mark {
128 | color: var(--blue-12);
129 | background: var(--blue-4);
130 | border-radius: 2px;
131 | opacity: 0.85;
132 | }
133 |
134 | [cmdk-list] {
135 | height: calc(75vh - 50px - 50px - 20px);
136 | max-height: calc(75vh - 50px - 50px - 20px);
137 | overflow: auto;
138 | overscroll-behavior: contain;
139 | border-top: 1px solid var(--expo-theme-border-default);
140 | border-bottom: 1px solid var(--expo-theme-border-default);
141 | padding: 0 16px 12px;
142 | margin: 12px 0 0;
143 | }
144 |
145 | @media screen and (max-width: 788px) {
146 | [cmdk-list] {
147 | height: calc(96dvh - 50px - 50px - 20px);
148 | max-height: calc(96dvh - 50px - 50px - 20px);
149 | }
150 | }
151 |
152 | [cmdk-separator] {
153 | height: 1px;
154 | width: 100%;
155 | background: var(--expo-theme-border-default);
156 | margin: 4px 0;
157 | }
158 |
159 | [cmdk-group-heading] {
160 | font-size: 0.75rem;
161 | line-height: 1.58;
162 | user-select: none;
163 | color: var(--expo-theme-text-secondary);
164 | padding: 16px 8px 8px;
165 | display: flex;
166 | align-items: center;
167 | gap: 4px;
168 | margin: 0 2px;
169 | }
170 |
171 | [cmdk-empty] {
172 | display: flex;
173 | align-items: center;
174 | justify-content: center;
175 | white-space: pre-wrap;
176 | padding: 32px 0;
177 | }
178 |
179 | html.dark-theme [cmdk-item] mark {
180 | background: var(--blue-6);
181 | }
182 |
183 | html.dark-theme [cmdk-item][data-selected='true'] mark {
184 | background: var(--blue-7);
185 | }
186 |
187 | [cmdk-list]::-webkit-scrollbar {
188 | width: 6px;
189 | }
190 |
191 | [cmdk-list]::-webkit-scrollbar-track {
192 | background-color: transparent;
193 | cursor: pointer;
194 | }
195 |
196 | [cmdk-list]::-webkit-scrollbar-thumb {
197 | background-color: var(--slate-5);
198 | border-radius: 10px;
199 | }
200 |
201 | [cmdk-list]::-webkit-scrollbar-thumb:hover {
202 | background-color: var(--slate-6);
203 | }
204 |
--------------------------------------------------------------------------------
/packages/search-ui/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from 'react';
2 |
3 | export type CommandMenuConfig = {
4 | docsVersion: string;
5 | docsTransformUrl?: (url: string) => string;
6 | disableDashboardSection?: boolean;
7 | };
8 |
9 | export type CommandMenuSection = {
10 | heading: string;
11 | getItemsAsync: (query: string) => Promise;
12 | items: ReactNode[];
13 | sectionIndex: number;
14 | };
15 |
16 | export type AlgoliaHighlight = {
17 | value: string;
18 | };
19 |
20 | export type AlgoliaItemHierarchy = {
21 | lvl0?: T | null;
22 | lvl1?: T | null;
23 | lvl2?: T | null;
24 | lvl3?: T | null;
25 | lvl4?: T | null;
26 | lvl5?: T | null;
27 | lvl6?: T | null;
28 | };
29 |
30 | export type AlgoliaItemType = {
31 | url: string;
32 | objectID: string;
33 | anchor: string | null;
34 | content: string | null;
35 | hierarchy: AlgoliaItemHierarchy;
36 | _highlightResult: {
37 | content: AlgoliaHighlight | null;
38 | hierarchy: AlgoliaItemHierarchy;
39 | };
40 | };
41 |
42 | export type RNDirectoryItemType = {
43 | npmPkg: string;
44 | githubUrl: string;
45 | npm: {
46 | downloads: number;
47 | };
48 | github: {
49 | stats: {
50 | stars: number;
51 | };
52 | };
53 | };
54 |
55 | export type ExpoBlogItemType = {
56 | title: string;
57 | tags: string[];
58 | metadataDescription: string;
59 | slug: {
60 | current: string;
61 | };
62 | mainImage: {
63 | altText: string;
64 | image: {
65 | asset: {
66 | _ref: string;
67 | };
68 | };
69 | };
70 | };
71 |
--------------------------------------------------------------------------------
/packages/search-ui/src/utils.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { ClientReturn, createClient } from '@sanity/client';
4 | import imageUrlBuilder from '@sanity/image-url';
5 | import type { Dispatch, SetStateAction } from 'react';
6 |
7 | import type { AlgoliaItemHierarchy, AlgoliaItemType } from './types';
8 |
9 | export const SANITY_CLIENT = createClient({
10 | projectId: 'siias52v',
11 | dataset: 'production',
12 | useCdn: true,
13 | apiVersion: '2024-09-03',
14 | });
15 |
16 | const sanityAssetHelper = imageUrlBuilder({ projectId: 'siias52v', dataset: 'production' });
17 |
18 | export function getSanityAsset(source: string) {
19 | return sanityAssetHelper.image(source).auto('format').height(150).url();
20 | }
21 |
22 | export const getItemsAsync = async (
23 | query: string,
24 | fetcher: (query: string, version?: string) => Promise,
25 | setter: Dispatch>,
26 | version?: string
27 | ) => {
28 | const { hits, libraries } = await fetcher(query, version).then((response) => response.json());
29 | setter(hits || libraries || []);
30 | };
31 |
32 | export const getSanityItemsAsync = async (
33 | query: string,
34 | fetcher: (query: string, version?: string) => Promise>,
35 | setter: Dispatch>
36 | ) => {
37 | setter(await fetcher(query));
38 | };
39 |
40 | const getAlgoliaFetchParams = (
41 | query: string,
42 | appId: string,
43 | apiKey: string,
44 | indexName: string,
45 | hits: number,
46 | additionalParams: object = {}
47 | ): [string, RequestInit] => [
48 | `https://${appId}-dsn.algolia.net/1/indexes/${indexName}/query`,
49 | {
50 | method: 'POST',
51 | headers: {
52 | 'X-Algolia-Application-Id': appId,
53 | 'X-Algolia-API-Key': apiKey,
54 | },
55 | body: JSON.stringify({
56 | params: `query=${query}&hitsPerPage=${hits}`,
57 | highlightPreTag: '',
58 | highlightPostTag: '',
59 | ...additionalParams,
60 | }),
61 | },
62 | ];
63 |
64 | export const getExpoDocsResults = (query: string, version?: string) => {
65 | return fetch(
66 | ...getAlgoliaFetchParams(query, 'QEX7PB7D46', '6652d26570e8628af4601e1d78ad456b', 'expo', 20, {
67 | facetFilters: [['version:none', `version:${version}`]],
68 | })
69 | );
70 | };
71 |
72 | export const getRNDocsResults = (query: string) => {
73 | return fetch(
74 | ...getAlgoliaFetchParams(query, '8TDSE0OHGQ', 'c9c791d9d5fd7f315d7f3859b32c1f3b', 'react-native-v2', 5, {
75 | facetFilters: [['version:current']],
76 | })
77 | );
78 | };
79 |
80 | export const getExpoBlogResults = (query: string) => {
81 | return SANITY_CLIENT.fetch(
82 | `*[_type == "post" && publishAt < now() && (title match "${query}*" || metadataDescription match "${query}*")] | order(publishAt desc)[0...10] {
83 | title,
84 | slug,
85 | tags,
86 | metadataDescription,
87 | mainImage
88 | }`
89 | );
90 | };
91 |
92 | export const getDirectoryResults = (query: string) => {
93 | return fetch(`https://reactnative.directory/api/libraries?search=${encodeURI(query)}&limit=5`);
94 | };
95 |
96 | export const getHighlightHTML = (item: AlgoliaItemType, tag: keyof AlgoliaItemHierarchy) => ({
97 | dangerouslySetInnerHTML: {
98 | __html: item._highlightResult.hierarchy[`${tag}`]?.value || '',
99 | },
100 | });
101 |
102 | const trimContent = (content: string, length = 36) => {
103 | if (!content || !content.length) return '';
104 |
105 | const trimStart = Math.max(content.indexOf('') - length, 0);
106 | const trimEnd = Math.min(content.indexOf('') + length + 6, content.length);
107 |
108 | return `${trimStart !== 0 ? '…' : ''}${content.substring(trimStart, trimEnd).trim()}${
109 | trimEnd !== content.length ? '…' : ''
110 | }`;
111 | };
112 |
113 | export const getContentHighlightHTML = (item: AlgoliaItemType, skipDescription = false) =>
114 | skipDescription
115 | ? {}
116 | : {
117 | dangerouslySetInnerHTML: {
118 | __html: item._highlightResult.content?.value
119 | ? trimContent(item._highlightResult.content?.value)
120 | : trimContent(item._highlightResult.hierarchy.lvl1?.value || '', 82),
121 | },
122 | };
123 |
124 | // note(simek): this code make sure that browser popup blocker
125 | // do not prevent opening links via key press (when it fires windows.open)
126 | export const openLink = (url: string, isExternal: boolean = false) => {
127 | const link = document.createElement('a');
128 | if (isExternal) {
129 | link.target = '_blank';
130 | link.rel = 'noopener noreferrer';
131 | }
132 | link.href = url;
133 | link.click();
134 | };
135 |
136 | const ReferencePathChunks = ['/versions/', '/more/'] as const;
137 |
138 | export const isReferencePath = (url: string) => {
139 | return ReferencePathChunks.some((pathChunk) => url.includes(pathChunk));
140 | };
141 |
142 | const EASPathChunks = [
143 | '/app-signing/',
144 | '/build/',
145 | '/build-reference/',
146 | '/development/',
147 | '/eas/',
148 | '/eas/metadata/',
149 | '/eas-update/',
150 | '/submit/',
151 | ] as const;
152 |
153 | export const isEASPath = (url: string) => {
154 | return EASPathChunks.some((pathChunk) => url.includes(pathChunk));
155 | };
156 |
157 | const HomePathChunks = [
158 | '/get-started/',
159 | '/develop/',
160 | '/deploy/',
161 | '/faq/',
162 | '/core-concepts/',
163 | '/debugging/',
164 | '/config-plugins/',
165 | ] as const;
166 |
167 | export const isHomePath = (url: string) => {
168 | return HomePathChunks.some((pathChunk) => url.includes(pathChunk));
169 | };
170 |
171 | const LearnPathChunks = ['/tutorial', '/ui-programming/', '/additional-resources/'] as const;
172 |
173 | export const isLearnPath = (url: string) => {
174 | return LearnPathChunks.some((pathChunk) => url.includes(pathChunk));
175 | };
176 |
177 | export const isAppleDevice = () => {
178 | return /(Mac|iPhone|iPod|iPad)/i.test(navigator?.platform ?? navigator?.userAgentData?.platform ?? '');
179 | };
180 |
181 | export const addHighlight = (content: string, query: string) => {
182 | if (!content || !content.length) {
183 | return '';
184 | }
185 |
186 | const highlightStart = content.toLowerCase().indexOf(query.toLowerCase());
187 |
188 | if (highlightStart === -1) return content;
189 |
190 | const highlightEnd = highlightStart + query.length;
191 | return (
192 | content.substring(0, highlightStart) +
193 | '' +
194 | content.substring(highlightStart, highlightEnd) +
195 | '' +
196 | content.substring(highlightEnd)
197 | );
198 | };
199 |
--------------------------------------------------------------------------------
/packages/search-ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "strict": true,
5 | "noImplicitAny": true,
6 | "allowSyntheticDefaultImports": true,
7 | "outDir": "dist",
8 | "target": "ES2021",
9 | "moduleResolution": "node",
10 | "esModuleInterop": true,
11 | "declaration": true
12 | },
13 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./index.ts"],
14 | "exclude": ["node_modules"]
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide-base/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Expo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/styleguide-base/README.md:
--------------------------------------------------------------------------------
1 | # @expo/styleguide-base
2 |
3 | Expo's base colors and style values.
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies with `yarn`.
8 | 2. Build everything with `yarn build`.
9 | 3. Develop with `yarn dev`.
--------------------------------------------------------------------------------
/packages/styleguide-base/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/breakpoints';
2 | export * from './src/palette';
3 | export * from './src/sizing';
4 | export * from './src/spacing';
5 | export * from './src/themes';
6 |
--------------------------------------------------------------------------------
/packages/styleguide-base/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@expo/styleguide-base",
3 | "version": "2.0.3",
4 | "description": "Expo's base colors and style values.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "clean": "rimraf dist",
12 | "bundle": "rollup --config",
13 | "build": "run-s clean bundle",
14 | "dev": "rollup --config --watch"
15 | },
16 | "author": "Expo",
17 | "license": "MIT",
18 | "homepage": "https://github.com/expo/styleguide",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/expo/styleguide.git",
22 | "directory": "packages/styleguide"
23 | },
24 | "bugs": {
25 | "url": "https://github.com/expo/styleguide/issues"
26 | },
27 | "dependencies": {
28 | "@radix-ui/colors": "^3.0.0"
29 | },
30 | "devDependencies": {
31 | "npm-run-all": "*",
32 | "rimraf": "*",
33 | "rollup": "*"
34 | },
35 | "peerDependencies": {
36 | "react": ">= 16"
37 | },
38 | "eslintConfig": {
39 | "extends": "universe/node"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/packages/styleguide-base/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 |
4 | const config = [
5 | {
6 | input: 'index.ts',
7 | output: {
8 | dir: 'dist',
9 | format: 'cjs',
10 | },
11 | plugins: [
12 | typescript(),
13 | terser()
14 | ],
15 | external: ['@radix-ui/colors'],
16 | },
17 | ];
18 |
19 | export default config;
20 |
--------------------------------------------------------------------------------
/packages/styleguide-base/src/breakpoints.ts:
--------------------------------------------------------------------------------
1 | export const breakpoints = {
2 | small: 400,
3 | medium: 900,
4 | large: 1200,
5 | };
6 |
--------------------------------------------------------------------------------
/packages/styleguide-base/src/palette.ts:
--------------------------------------------------------------------------------
1 | import {
2 | blue,
3 | blueDark,
4 | red,
5 | redDark,
6 | amber,
7 | amberDark,
8 | green,
9 | greenDark,
10 | orange,
11 | orangeDark,
12 | purple,
13 | purpleDark,
14 | pink,
15 | pinkDark,
16 | slate,
17 | slateDark,
18 | } from '@radix-ui/colors';
19 |
20 | export const palette = {
21 | white: 'hsl(0, 0%, 100%)',
22 | black: 'hsl(0, 0%, 0%)',
23 | light: {
24 | ...blue,
25 | ...red,
26 | ...green,
27 | ...orange,
28 | ...purple,
29 | ...pink,
30 | yellow1: amber.amber1,
31 | yellow2: amber.amber2,
32 | yellow3: amber.amber3,
33 | yellow4: amber.amber4,
34 | yellow5: amber.amber5,
35 | yellow6: amber.amber6,
36 | yellow7: amber.amber7,
37 | yellow8: amber.amber8,
38 | yellow9: amber.amber9,
39 | yellow10: amber.amber10,
40 | yellow11: amber.amber11,
41 | yellow12: amber.amber12,
42 | gray1: slate.slate1,
43 | gray2: slate.slate2,
44 | gray3: slate.slate3,
45 | gray4: slate.slate4,
46 | gray5: slate.slate5,
47 | gray6: slate.slate6,
48 | gray7: slate.slate7,
49 | gray8: slate.slate8,
50 | gray9: slate.slate9,
51 | gray10: slate.slate10,
52 | gray11: slate.slate11,
53 | gray12: slate.slate12,
54 | },
55 | dark: {
56 | ...blueDark,
57 | ...redDark,
58 | ...greenDark,
59 | ...orangeDark,
60 | ...purpleDark,
61 | ...pinkDark,
62 | yellow1: amberDark.amber1,
63 | yellow2: amberDark.amber2,
64 | yellow3: amberDark.amber3,
65 | yellow4: amberDark.amber4,
66 | yellow5: amberDark.amber5,
67 | yellow6: amberDark.amber6,
68 | yellow7: amberDark.amber7,
69 | yellow8: amberDark.amber8,
70 | yellow9: amberDark.amber9,
71 | yellow10: amberDark.amber10,
72 | yellow11: amberDark.amber11,
73 | yellow12: amberDark.amber12,
74 | gray1: slateDark.slate1,
75 | gray2: slateDark.slate2,
76 | gray3: slateDark.slate3,
77 | gray4: slateDark.slate4,
78 | gray5: slateDark.slate5,
79 | gray6: slateDark.slate6,
80 | gray7: slateDark.slate7,
81 | gray8: slateDark.slate8,
82 | gray9: slateDark.slate9,
83 | gray10: slateDark.slate10,
84 | gray11: slateDark.slate11,
85 | gray12: slateDark.slate12,
86 | },
87 | };
88 |
--------------------------------------------------------------------------------
/packages/styleguide-base/src/sizing.ts:
--------------------------------------------------------------------------------
1 | export const borderRadius = {
2 | none: 0,
3 | xs: 2,
4 | sm: 4,
5 | md: 6,
6 | lg: 10,
7 | xl: 16,
8 | '2xl': 20,
9 | '3xl': 24,
10 | full: 9999,
11 | };
12 |
13 | export const iconSize = {
14 | xs: 16,
15 | sm: 20,
16 | md: 24,
17 | lg: 28,
18 | xl: 32,
19 | };
20 |
--------------------------------------------------------------------------------
/packages/styleguide-base/src/spacing.ts:
--------------------------------------------------------------------------------
1 | const baseSize = 16;
2 |
3 | export const spacing = {
4 | 0: 1,
5 | '0.5': baseSize * 0.125,
6 | 1: baseSize * 0.25,
7 | '1.5': baseSize * 0.375,
8 | 2: baseSize * 0.5,
9 | '2.5': baseSize * 0.625,
10 | 3: baseSize * 0.75,
11 | '3.5': baseSize * 0.875,
12 | 4: baseSize * 1,
13 | 5: baseSize * 1.25,
14 | 6: baseSize * 1.5,
15 | 7: baseSize * 1.75,
16 | 8: baseSize * 2,
17 | 9: baseSize * 2.25,
18 | 10: baseSize * 2.5,
19 | 11: baseSize * 2.75,
20 | 12: baseSize * 3,
21 | 14: baseSize * 3.5,
22 | 16: baseSize * 4,
23 | 20: baseSize * 5,
24 | 24: baseSize * 6,
25 | 28: baseSize * 7,
26 | 32: baseSize * 8,
27 | 36: baseSize * 9,
28 | 40: baseSize * 10,
29 | 44: baseSize * 11,
30 | 48: baseSize * 12,
31 | 52: baseSize * 13,
32 | 56: baseSize * 14,
33 | 60: baseSize * 15,
34 | 64: baseSize * 16,
35 | 72: baseSize * 18,
36 | 80: baseSize * 20,
37 | 96: baseSize * 24,
38 | };
39 |
--------------------------------------------------------------------------------
/packages/styleguide-base/src/themes.ts:
--------------------------------------------------------------------------------
1 | import { palette } from './palette';
2 |
3 | export const lightTheme = {
4 | background: {
5 | default: palette.white,
6 | screen: palette.light.gray1,
7 | subtle: palette.light.gray2,
8 | element: palette.light.gray3,
9 | hover: palette.light.gray4,
10 | selected: palette.light.gray5,
11 | overlay: palette.white,
12 | success: palette.light.green3,
13 | warning: palette.light.yellow3,
14 | danger: palette.light.red3,
15 | info: palette.light.blue3,
16 | },
17 | icon: {
18 | default: palette.light.gray11,
19 | secondary: palette.light.gray10,
20 | tertiary: palette.light.gray9,
21 | quaternary: palette.light.gray8,
22 | success: palette.light.green10,
23 | warning: palette.light.yellow11,
24 | danger: palette.light.red10,
25 | info: palette.light.blue10,
26 | },
27 | text: {
28 | default: palette.light.gray12,
29 | secondary: palette.light.gray11,
30 | tertiary: palette.light.gray10,
31 | quaternary: palette.light.gray9,
32 | link: palette.light.blue11,
33 | success: palette.light.green11,
34 | warning: palette.light.yellow11,
35 | danger: palette.light.red11,
36 | info: palette.light.blue11,
37 | },
38 | border: {
39 | default: palette.light.gray7,
40 | secondary: palette.light.gray6,
41 | success: palette.light.green7,
42 | warning: palette.light.yellow7,
43 | danger: palette.light.red7,
44 | info: palette.light.blue7,
45 | },
46 | button: {
47 | primary: {
48 | background: palette.light.blue10,
49 | border: palette.light.blue10,
50 | hover: palette.light.blue11,
51 | icon: palette.light.blue3,
52 | text: palette.white,
53 | disabled: {
54 | background: palette.light.blue7,
55 | border: palette.light.blue7,
56 | text: palette.white,
57 | },
58 | destructive: {
59 | background: palette.light.red10,
60 | border: palette.light.red10,
61 | hover: palette.light.red11,
62 | icon: palette.light.red3,
63 | text: palette.white,
64 | disabled: {
65 | background: palette.light.red7,
66 | border: palette.light.red7,
67 | text: palette.white,
68 | },
69 | },
70 | },
71 | secondary: {
72 | background: palette.white,
73 | border: palette.light.gray8,
74 | hover: palette.light.gray3,
75 | icon: palette.light.gray11,
76 | text: palette.light.gray12,
77 | disabled: {
78 | background: palette.white,
79 | border: palette.light.gray6,
80 | text: palette.light.gray9,
81 | },
82 | destructive: {
83 | background: palette.white,
84 | border: palette.light.red7,
85 | hover: palette.light.red3,
86 | icon: palette.light.red9,
87 | text: palette.light.red11,
88 | disabled: {
89 | background: palette.white,
90 | border: palette.light.red5,
91 | text: palette.light.red8,
92 | },
93 | },
94 | },
95 | tertiary: {
96 | background: 'transparent',
97 | border: 'transparent',
98 | hover: palette.light.blue4,
99 | icon: palette.light.blue9,
100 | text: palette.light.blue10,
101 | disabled: {
102 | background: 'transparent',
103 | border: 'transparent',
104 | text: palette.light.blue8,
105 | },
106 | },
107 | quaternary: {
108 | background: 'transparent',
109 | border: 'transparent',
110 | hover: palette.light.gray4,
111 | icon: palette.light.gray11,
112 | text: palette.light.gray12,
113 | disabled: {
114 | background: 'transparent',
115 | border: 'transparent',
116 | text: palette.light.gray9,
117 | },
118 | },
119 | },
120 | };
121 |
122 | export const darkTheme = {
123 | background: {
124 | default: palette.dark.gray1,
125 | screen: '#0C0D0E',
126 | subtle: palette.dark.gray2,
127 | element: palette.dark.gray3,
128 | hover: palette.dark.gray4,
129 | selected: palette.dark.gray5,
130 | overlay: palette.dark.gray2,
131 | success: palette.dark.green3,
132 | warning: palette.dark.yellow3,
133 | danger: palette.dark.red3,
134 | info: palette.dark.blue3,
135 | },
136 | icon: {
137 | default: palette.dark.gray11,
138 | secondary: palette.dark.gray10,
139 | tertiary: palette.dark.gray9,
140 | quaternary: palette.dark.gray8,
141 | success: palette.dark.green10,
142 | warning: palette.dark.yellow11,
143 | danger: palette.dark.red10,
144 | info: palette.dark.blue10,
145 | },
146 | text: {
147 | default: palette.dark.gray12,
148 | secondary: palette.dark.gray11,
149 | tertiary: palette.dark.gray10,
150 | quaternary: palette.dark.gray9,
151 | link: palette.dark.blue11,
152 | success: palette.dark.green11,
153 | warning: palette.dark.yellow11,
154 | danger: palette.dark.red11,
155 | info: palette.dark.blue11,
156 | },
157 | border: {
158 | default: palette.dark.gray7,
159 | secondary: palette.dark.gray6,
160 | success: palette.dark.green7,
161 | warning: palette.dark.yellow7,
162 | danger: palette.dark.red7,
163 | info: palette.dark.blue7,
164 | },
165 | button: {
166 | primary: {
167 | background: palette.dark.blue8,
168 | border: palette.dark.blue8,
169 | hover: palette.dark.blue7,
170 | icon: palette.dark.blue12,
171 | text: palette.white,
172 | disabled: {
173 | background: palette.dark.blue7,
174 | border: palette.dark.blue7,
175 | text: palette.dark.gray11,
176 | },
177 | destructive: {
178 | background: palette.dark.red8,
179 | border: palette.dark.red8,
180 | hover: palette.dark.red7,
181 | icon: palette.dark.red12,
182 | text: palette.white,
183 | disabled: {
184 | background: palette.dark.red6,
185 | border: palette.dark.red6,
186 | text: palette.dark.red11,
187 | },
188 | },
189 | },
190 | secondary: {
191 | background: palette.dark.gray3,
192 | border: palette.dark.gray8,
193 | hover: palette.dark.gray4,
194 | icon: palette.dark.gray12,
195 | text: palette.white,
196 | disabled: {
197 | background: palette.dark.gray1,
198 | border: palette.dark.gray7,
199 | text: palette.dark.gray11,
200 | },
201 | destructive: {
202 | background: palette.dark.red3,
203 | border: palette.dark.red7,
204 | hover: palette.dark.red2,
205 | icon: palette.dark.red9,
206 | text: palette.white,
207 | disabled: {
208 | background: palette.dark.red2,
209 | border: palette.dark.red6,
210 | text: palette.dark.red10,
211 | },
212 | },
213 | },
214 | tertiary: {
215 | background: 'transparent',
216 | border: 'transparent',
217 | hover: palette.dark.blue4,
218 | icon: palette.dark.blue10,
219 | text: palette.dark.blue11,
220 | disabled: {
221 | background: 'transparent',
222 | border: 'transparent',
223 | text: palette.dark.blue8,
224 | },
225 | },
226 | quaternary: {
227 | background: 'transparent',
228 | border: 'transparent',
229 | hover: palette.dark.gray4,
230 | icon: palette.dark.gray10,
231 | text: palette.dark.gray11,
232 | disabled: {
233 | background: 'transparent',
234 | border: 'transparent',
235 | text: palette.dark.gray9,
236 | },
237 | },
238 | },
239 | };
240 |
--------------------------------------------------------------------------------
/packages/styleguide-base/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "noImplicitAny": true,
5 | "allowSyntheticDefaultImports": true,
6 | "declaration": true,
7 | "outDir": "dist"
8 | },
9 | "include": ["./src/**/*.ts", "./index.ts"],
10 | "exclude": ["node_modules"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/.env.example:
--------------------------------------------------------------------------------
1 | FIGMA_TOKEN=[your personal access token]
2 | FILE_ID=zHeGLN45wIgf39kqdvKpoj
--------------------------------------------------------------------------------
/packages/styleguide-icons/.gitignore:
--------------------------------------------------------------------------------
1 | src/custom
2 | src/duotone
3 | src/outline
4 | src/solid
5 |
6 | custom
7 | duotone
8 | outline
9 | solid
10 |
11 | /index.js
12 | /mergeClasses.js
13 |
14 | *.d.ts
15 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023 Expo
2 |
3 | Unauthorized use of this software via any medium is strictly prohibited. Any
4 | person obtaining a copy of this software and associated documentation files
5 | (the "Software"), are restricted from using the software in any way other
6 | than for the use in Expo websites, products, and apps. It is not permitted
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software without the authorization of Expo.
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/styleguide-icons/README.md:
--------------------------------------------------------------------------------
1 | # @expo/styleguide-icons
2 |
3 | Expo's icons for use on the web.
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies with `yarn`.
8 | 2. Set up a .env file. To do this, you'll need to:
9 | a. Duplicate the **.env.example** file, and name the copy: **.env**.
10 | b. Inside it, define a `FIGMA_TOKEN` with a personal access token from Figma. Click on your avatar in Figma in the top right > Settings > Account tab. The personal access token settings are near the bottom.
11 | 3. Build everything with `yarn build`.
12 |
13 | ### Icon generation
14 |
15 | We generate all icon files based on our Figma icons. The process is:
16 |
17 | 1. Make a call to Figma to get all the icons from a specific file.
18 | 2. Once we get every component from the icons pages specified in **figma.config.js**, we optimize them all with SVGO.
19 | 3. After that, we use SVGR to create React components of each icon. The outputter is defined in **figma.config.js**, and we use a custom template in **svgr-icon-template.js**. These components are stored in **tmp**.
20 | 4. Finally, we use `rollup` to build our final package. These files are saved in **dist**.
21 |
22 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/check-env.sh:
--------------------------------------------------------------------------------
1 | if [ -z "${FIGMA_TOKEN}" ] && [ ! -f ./.env ]; then
2 | echo "⚠️ You do not have packages/styleguide-icons/.env file or correct environment variables set.
3 |
4 | Please create it from packages/styleguide-icons/.env.example and fill it with your credentials.
5 |
6 | Instead, bundling a stub for @expo/styleguide-icons that is empty.
7 | Read the README.md in packages/styleguide-icons to build the icon set.
8 | "
9 | yarn build-stub && exit 0
10 | else
11 | yarn build-icons && exit 0
12 | fi
13 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/figma.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | const svgo = require('@figma-export/transform-svg-with-svgo');
4 | const figmaUtils = require('@figma-export/utils');
5 |
6 | const template = require('./svgr-icon-template');
7 | const pascalCase = figmaUtils.pascalCase;
8 | const fileId = process.env.FILE_ID;
9 |
10 | function getComponentName({ componentName, pageName }) {
11 | if (pageName === 'outline' || pageName === 'custom') {
12 | return pascalCase(componentName) + 'Icon';
13 | }
14 |
15 | return pascalCase(componentName) + pascalCase(pageName) + 'Icon';
16 | }
17 |
18 | const outputters = [
19 | require('@figma-export/output-components-as-svgr')({
20 | getFileExtension: () => '.tsx',
21 | getComponentName,
22 | getSvgrConfig: ({ componentName, pageName }) => ({
23 | typescript: true,
24 | svgProps: {
25 | className: '{_className}',
26 | role: 'img',
27 | },
28 | replaceAttrValues: {
29 | black: 'currentColor',
30 | [componentName]: `${componentName}-${pageName}-icon`,
31 | },
32 | template,
33 | }),
34 | // Exports the component as a named export without the '.tsx' extension inside the generated index.ts file. By default the '.tsx' extension is added, which makes TypeScript complain.
35 | getExportTemplate: ({ componentName, pageName }) =>
36 | `export { ${getComponentName({
37 | componentName,
38 | pageName,
39 | })} } from './${getComponentName({ componentName, pageName })}';`,
40 | output: './src',
41 | }),
42 | ];
43 |
44 | const commonSVGOConfig = [
45 | {
46 | name: 'removeHiddenElems',
47 | active: true,
48 | },
49 | {
50 | name: 'removeXMLNS',
51 | active: true,
52 | },
53 | {
54 | name: 'removeUselessStrokeAndFill',
55 | active: true,
56 | },
57 | {
58 | name: 'removeUselessDefs',
59 | active: true,
60 | },
61 | {
62 | name: 'collapseGroups',
63 | active: true,
64 | },
65 | {
66 | name: 'removeEmptyContainers',
67 | active: true,
68 | },
69 | {
70 | name: 'removeDimensions',
71 | active: true,
72 | },
73 | {
74 | name: 'sortAttrs',
75 | active: true,
76 | },
77 | ];
78 |
79 | /** @type {import('svgo').PluginConfig[]} */
80 | const solidSVGOConfig = [
81 | ...commonSVGOConfig,
82 | {
83 | name: 'removeAttrs',
84 | params: {
85 | attrs: 'fill',
86 | },
87 | },
88 | {
89 | name: 'addAttributesToSVGElement',
90 | params: {
91 | attribute: {
92 | fill: 'currentColor',
93 | },
94 | },
95 | },
96 | ];
97 |
98 | /** @type {import('svgo').PluginConfig[]} */
99 | const customSVGOConfig = [...commonSVGOConfig];
100 |
101 | /** @type {import('svgo').PluginConfig[]} */
102 | const outlineSVGOConfig = [
103 | ...commonSVGOConfig,
104 | {
105 | name: 'removeAttrs',
106 | params: {
107 | attrs: 'stroke',
108 | },
109 | },
110 | {
111 | name: 'addAttributesToSVGElement',
112 | params: {
113 | attribute: {
114 | stroke: 'currentColor',
115 | },
116 | },
117 | },
118 | ];
119 |
120 | /** @type {import('svgo').PluginConfig[]} */
121 | const duotoneSVGOConfig = [
122 | ...commonSVGOConfig,
123 | {
124 | name: 'removeAttrs',
125 | params: {
126 | attrs: 'stroke',
127 | },
128 | },
129 | {
130 | name: 'addAttributesToSVGElement',
131 | params: {
132 | attribute: {
133 | stroke: 'currentColor',
134 | },
135 | },
136 | },
137 | ];
138 |
139 | /** @type {import('@figma-export/types').FigmaExportRC} */
140 | module.exports = {
141 | commands: [
142 | [
143 | 'components',
144 | {
145 | fileId,
146 | onlyFromPages: ['outline'],
147 | transformers: [svgo({ multipass: true, plugins: outlineSVGOConfig })],
148 | outputters,
149 | },
150 | ],
151 | [
152 | 'components',
153 | {
154 | fileId,
155 | onlyFromPages: ['duotone'],
156 | transformers: [svgo({ multipass: true, plugins: duotoneSVGOConfig })],
157 | outputters,
158 | },
159 | ],
160 | [
161 | 'components',
162 | {
163 | fileId,
164 | onlyFromPages: ['solid'],
165 | transformers: [svgo({ multipass: true, plugins: solidSVGOConfig })],
166 | outputters,
167 | },
168 | ],
169 | [
170 | 'components',
171 | {
172 | fileId,
173 | onlyFromPages: ['custom'],
174 | transformers: [svgo({ multipass: true, plugins: customSVGOConfig })],
175 | outputters,
176 | },
177 | ],
178 | ],
179 | };
180 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@expo/styleguide-icons",
3 | "version": "2.2.2",
4 | "description": "Expo's icons for use on the web.",
5 | "main": "index",
6 | "types": "index.d.ts",
7 | "sideEffects": false,
8 | "files": [
9 | "custom",
10 | "duotone",
11 | "outline",
12 | "solid",
13 | "index.d.ts",
14 | "index.js",
15 | "mergeClasses.d.ts",
16 | "mergeClasses.js"
17 | ],
18 | "scripts": {
19 | "check-env": "sh ./check-env.sh",
20 | "clean": "rimraf custom duotone outline solid 'index.*' 'mergeClasses.*'",
21 | "export": "figma-export use-config figma.config.js",
22 | "bundle": "rollup --config && node ./postbundle.js",
23 | "build": "run-s check-env",
24 | "build-stub": "export STUB=true && run-s clean bundle",
25 | "build-icons": "run-s clean export bundle"
26 | },
27 | "author": "Expo",
28 | "license": "UNLICENSED",
29 | "homepage": "https://github.com/expo/styleguide",
30 | "repository": {
31 | "type": "git",
32 | "url": "https://github.com/expo/styleguide.git",
33 | "directory": "packages/styleguide"
34 | },
35 | "bugs": {
36 | "url": "https://github.com/expo/styleguide/issues"
37 | },
38 | "dependencies": {
39 | "tailwind-merge": "^2.5.4"
40 | },
41 | "devDependencies": {
42 | "@figma-export/cli": "^4.8.0",
43 | "@figma-export/output-components-as-svgr": "^4.8.0",
44 | "@figma-export/transform-svg-with-svgo": "^4.8.0",
45 | "dotenv": "^16.3.1",
46 | "npm-run-all": "*",
47 | "rimraf": "*",
48 | "rollup": "*",
49 | "tslib": "^2.8.0"
50 | },
51 | "peerDependencies": {
52 | "react": ">= 16"
53 | },
54 | "eslintConfig": {
55 | "extends": [
56 | "universe/web",
57 | "universe/node"
58 | ],
59 | "ignorePatterns": [
60 | "custom",
61 | "duotone",
62 | "outline",
63 | "solid"
64 | ]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/postbundle.js:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs');
2 | const path = require('node:path');
3 |
4 | // Flatten package structure.
5 | fs.cpSync(path.resolve(__dirname, 'dist'), path.resolve(__dirname), { recursive: true });
6 | fs.rmSync('./dist', { recursive: true });
7 |
8 | console.log('');
9 |
10 | // Replace all index files content.
11 | fs.readdirSync(path.resolve(__dirname), { withFileTypes: true })
12 | .reduce((acc, dirent) => {
13 | const directoryPath = path.resolve(__dirname, dirent.name);
14 | if (dirent.isDirectory()) {
15 | const files = fs.readdirSync(directoryPath).map((fileName) => path.join(directoryPath, fileName));
16 | return acc.concat(files);
17 | }
18 | return acc.concat(directoryPath);
19 | }, [])
20 | .filter((file) => ['index.js', 'index.d.ts'].includes(path.basename(file)))
21 | .forEach((indexFile) => {
22 | // Overwrite the index file with an empty file.
23 | fs.writeFileSync(indexFile, '');
24 | console.log(`🧹 Cleared: \x1b[36m${indexFile.replace(__dirname, '')}\x1b[0m`);
25 | });
26 |
27 | console.log('');
28 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 | import copy from 'rollup-plugin-copy';
4 |
5 | const baseConfig = {
6 | input: 'src/index.ts',
7 | output: {
8 | name: 'index',
9 | dir: 'dist',
10 | format: 'cjs',
11 | generatedCode: 'es2015',
12 | exports: 'named',
13 | preserveModules: true,
14 | preserveModulesRoot: 'src',
15 | },
16 | treeshake: 'smallest',
17 | plugins: [
18 | typescript(),
19 | terser(),
20 | ],
21 | external: ['react', 'tailwind-merge'],
22 | };
23 |
24 | function getConfig() {
25 | if (process.env.STUB) {
26 | return {
27 | ...baseConfig,
28 | input: 'src/index-stub.js',
29 | plugins: [
30 | copy({
31 | targets: [
32 | { src: './stub.d.ts', dest: './', rename: 'index.d.ts' }
33 | ],
34 | }),
35 | ],
36 | };
37 | }
38 | return baseConfig;
39 | }
40 |
41 | const config = getConfig();
42 |
43 | export default config;
44 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/src/index-stub.js:
--------------------------------------------------------------------------------
1 | // The purpose of this file is to provide a module that can be imported by the example website without breaking when there is no .env file present.
2 |
3 | const stub = (args) => null;
4 |
5 | export { stub };
6 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './solid';
2 | export * from './outline';
3 | export * from './duotone';
4 | export * from './custom';
5 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/src/mergeClasses.ts:
--------------------------------------------------------------------------------
1 | import { extendTailwindMerge } from 'tailwind-merge';
2 |
3 | type AdditionalClassGroupIds = 'icon';
4 |
5 | export const mergeClasses = extendTailwindMerge({
6 | extend: {
7 | classGroups: {
8 | icon: [{ icon: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] }],
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/stub.d.ts:
--------------------------------------------------------------------------------
1 | declare function stub(props: any): JSX.Element;
2 | export default stub;
3 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/svgr-icon-template.js:
--------------------------------------------------------------------------------
1 | const svgrTemplate = ({ imports, interfaces, componentName, props, jsx }, { tpl, options }) => {
2 | return tpl`${imports}
3 |
4 | import { mergeClasses } from "../mergeClasses";
5 |
6 | export function ${componentName}({ className, ...props }: React.SVGProps & React.HTMLAttributes) {
7 | const _className = mergeClasses("icon-md text-icon-default translate-z shrink-0", className);
8 | return ${jsx};
9 | }
10 |
11 | ${componentName}.displayName = "${componentName}";`;
12 | };
13 |
14 | module.exports = svgrTemplate;
15 |
--------------------------------------------------------------------------------
/packages/styleguide-icons/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "strict": true,
5 | "noImplicitAny": true,
6 | "allowSyntheticDefaultImports": true,
7 | "declaration": true,
8 | "outDir": "dist",
9 | "target": "es2018",
10 | "paths": {
11 | "@/*": [
12 | "./src/*"
13 | ]
14 | }
15 | },
16 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./src/*.ts"],
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/packages/styleguide-native/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Expo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/styleguide-native/README.md:
--------------------------------------------------------------------------------
1 | # @expo/styleguide-native
2 |
3 | Expo's React Native styleguide components.
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies with `yarn`.
8 | 2. Build everything with `yarn build`.
9 | 3. Develop with `yarn dev`.
10 |
--------------------------------------------------------------------------------
/packages/styleguide-native/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/logos';
2 | export type { IconProps } from './src/types';
3 | export { shadows } from './src/styles/shadows';
4 | export { typography } from './src/styles/typography';
5 |
--------------------------------------------------------------------------------
/packages/styleguide-native/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@expo/styleguide-native",
3 | "version": "8.0.0",
4 | "description": "Expo's React Native styleguide components.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "scripts": {
8 | "clean": "rimraf dist",
9 | "bundle": "rollup --config",
10 | "build": "run-s clean bundle",
11 | "dev": "rollup --config --watch"
12 | },
13 | "files": [
14 | "dist"
15 | ],
16 | "author": "Expo",
17 | "license": "MIT",
18 | "homepage": "https://github.com/expo/styleguide",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/expo/styleguide.git",
22 | "directory": "packages/styleguide-native"
23 | },
24 | "devDependencies": {
25 | "@babel/preset-env": "^7.26.9",
26 | "@babel/preset-react": "^7.26.3",
27 | "@babel/preset-typescript": "^7.26.0",
28 | "@rollup/plugin-babel": "^6.0.4",
29 | "@rollup/plugin-commonjs": "^28.0.3",
30 | "@rollup/plugin-node-resolve": "^16.0.1",
31 | "@svgr/cli": "^6.5.1",
32 | "npm-run-all": "*",
33 | "react-native-svg": "^15.8.0",
34 | "rimraf": "*",
35 | "rollup": "*"
36 | },
37 | "peerDependencies": {
38 | "react": "*",
39 | "react-native-svg": "*"
40 | },
41 | "eslintConfig": {
42 | "extends": "universe/native"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/styleguide-native/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import babel from '@rollup/plugin-babel';
2 | import resolve from '@rollup/plugin-node-resolve';
3 | import commonjs from '@rollup/plugin-commonjs';
4 | import terser from '@rollup/plugin-terser';
5 | import typescript from '@rollup/plugin-typescript';
6 |
7 | const config = [
8 | {
9 | input: 'index.ts',
10 | output: {
11 | file: 'dist/index.js',
12 | format: 'esm',
13 | sourcemap: true,
14 | },
15 | external: [
16 | 'react',
17 | 'react-native',
18 | 'react-native-svg',
19 | ],
20 | plugins: [
21 | typescript(),
22 | resolve({
23 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
24 | }),
25 | commonjs(),
26 | babel({
27 | exclude: 'node_modules/**',
28 | babelHelpers: 'runtime',
29 | presets: [
30 | '@babel/preset-env',
31 | '@babel/preset-react',
32 | '@babel/preset-typescript',
33 | ],
34 | plugins: ['@babel/plugin-transform-runtime'],
35 | }),
36 | terser(),
37 | ],
38 | },
39 | ];
40 |
41 | export default config;
42 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/DocsLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | import { IconProps } from '../types';
5 | export default function DocsLogo(props: IconProps) {
6 | const { color } = props;
7 | return (
8 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/ExpoGoLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | import { IconProps } from '../types';
5 | export default function ExpoGoLogo(props: IconProps) {
6 | const { color } = props;
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/Logo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | import { IconProps } from '../types';
5 | export default function Logo(props: IconProps) {
6 | const { color } = props;
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/SnackLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | import { IconProps } from '../types';
5 | export default function SnackLogo(props: IconProps) {
6 | const { color } = props;
7 | return (
8 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/WordMarkLogo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Svg, { Path } from 'react-native-svg';
3 |
4 | import { IconProps } from '../types';
5 | export default function WordMarkLogo(props: IconProps) {
6 | const { color } = props;
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/logos/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as DocsLogo } from './DocsLogo';
2 | export { default as ExpoGoLogo } from './ExpoGoLogo';
3 | export { default as Logo } from './Logo';
4 | export { default as SnackLogo } from './SnackLogo';
5 | export { default as WordMarkLogo } from './WordMarkLogo';
6 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/styles/shadows.ts:
--------------------------------------------------------------------------------
1 | export const shadows = {
2 | xs: {
3 | elevation: 1,
4 | shadowColor: '#000',
5 | shadowRadius: 1,
6 | shadowOffset: { height: 1, width: 0 },
7 | shadowOpacity: 0.075,
8 | },
9 | sm: {
10 | elevation: 4,
11 | shadowColor: '#000',
12 | shadowRadius: 3,
13 | shadowOffset: { height: 3, width: 0 },
14 | shadowOpacity: 0.15,
15 | },
16 | md: {
17 | elevation: 8,
18 | shadowColor: '#000',
19 | shadowRadius: 8,
20 | shadowOffset: { height: 6, width: 0 },
21 | shadowOpacity: 0.15,
22 | },
23 | lg: {
24 | elevation: 16,
25 | shadowColor: '#000',
26 | shadowRadius: 10,
27 | shadowOffset: { height: 10, width: 0 },
28 | shadowOpacity: 0.17,
29 | },
30 | xl: {
31 | elevation: 28,
32 | shadowColor: '#000',
33 | shadowRadius: 25,
34 | shadowOffset: { height: 16, width: 0 },
35 | shadowOpacity: 0.2,
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/packages/styleguide-native/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { SvgProps } from 'react-native-svg';
2 |
3 | export type IconProps = SvgProps & {
4 | size?: number;
5 | };
6 |
--------------------------------------------------------------------------------
/packages/styleguide-native/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "strict": true,
5 | "noImplicitAny": true,
6 | "allowSyntheticDefaultImports": true,
7 | "declaration": true,
8 | "outDir": "dist"
9 | },
10 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./index.ts"],
11 | "exclude": ["node_modules"]
12 | }
13 |
--------------------------------------------------------------------------------
/packages/styleguide/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Expo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/packages/styleguide/README.md:
--------------------------------------------------------------------------------
1 | # @expo/styleguide
2 |
3 | Expo's styleguide and components for use on the web.
4 |
5 | ## Usage
6 |
7 | 1. Install Expo Styleguide package:
8 | ```shell
9 | yarn add @expo/styleguide
10 | ```
11 | 2. Import global CSS files from the package in your JS(X)/TS(X) code:
12 | ```jsx
13 | import "@expo/styleguide/dist/expo-theme.css";
14 | ```
15 | or import it the main stylesheet file:
16 | ```css
17 | @import "@expo/styleguide/dist/expo-theme.css";
18 | ```
19 | 3. Add `'./node_modules/@expo/styleguide/dist/**/*.{js,ts,jsx,tsx}'` to the Tailwind `content` paths.
20 |
21 | ### Tailwind theme
22 |
23 | For the Styleguide we use our custom Tailwind theme, which is based on the default TW theme, with the following differences:
24 | * only valid media screen scopes are: `xs:`, `sm:`, `md:`, `lg:` and `xl:`
25 | * there is a custom `hocus:` scope which is a shorthand for hover and focus states
26 | * typography elements are predefined as a `heading-[size]` styles sets
27 | * `icon-[size]` are custom component classes defined for icons sizing
28 |
29 | The theme can be extended, if needed, and includes `@tailwindcss/typography` plugin by default, with a stripped down version of default config.
30 |
31 | ## Development
32 |
33 | ### Get started
34 |
35 | 1. Install dependencies with `yarn`.
36 | 2. Build everything with `yarn build`.
37 | 3. Develop with `yarn dev`.
38 |
39 | ### Changing Tailwind theme
40 |
41 | In order to see changes made to the exported **tailwind.js** config:
42 |
43 | - Change a value in **packages/styleguide/tailwind.js**
44 | - Run `yarn build` in **packages/styleguide**
45 | - Navigate to **example-web** and restart the dev server
--------------------------------------------------------------------------------
/packages/styleguide/index.ts:
--------------------------------------------------------------------------------
1 | export * from './src/components';
2 | export * from './src/logos';
3 | export { shadows } from './src/styles/shadows';
4 | export { appBackgroundColors } from './src/styles/colors';
5 | export { theme } from './src/styles/themes';
6 | export { typography } from './src/styles/typography';
7 | export { mergeClasses } from './src/helpers/mergeClasses';
8 |
--------------------------------------------------------------------------------
/packages/styleguide/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@expo/styleguide",
3 | "version": "9.1.2",
4 | "description": "Expo's styleguide components for use on the web.",
5 | "main": "dist/index.js",
6 | "types": "dist/index.d.ts",
7 | "files": [
8 | "dist",
9 | "tailwind.js"
10 | ],
11 | "scripts": {
12 | "clean": "rimraf dist",
13 | "bundle": "rollup --config",
14 | "build": "run-s clean bundle",
15 | "dev": "rollup --config --watch"
16 | },
17 | "homepage": "https://github.com/expo/styleguide",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/expo/styleguide.git",
21 | "directory": "packages/styleguide"
22 | },
23 | "keywords": [
24 | "expo"
25 | ],
26 | "author": "Expo",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/expo/styleguide/issues"
30 | },
31 | "dependencies": {
32 | "@expo/styleguide-base": "^2.0.3",
33 | "tailwind-merge": "^2.5.4"
34 | },
35 | "devDependencies": {
36 | "@tailwindcss/typography": "^0.5.15",
37 | "npm-run-all": "*",
38 | "rimraf": "*",
39 | "rollup": "*",
40 | "tailwindcss": "^3.4.17"
41 | },
42 | "peerDependencies": {
43 | "next": ">= 13",
44 | "react": ">= 16"
45 | },
46 | "eslintConfig": {
47 | "extends": "universe/web"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/packages/styleguide/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import terser from '@rollup/plugin-terser';
2 | import typescript from '@rollup/plugin-typescript';
3 | import copy from 'rollup-plugin-copy';
4 |
5 | const config = [
6 | {
7 | input: 'index.ts',
8 | output: {
9 | dir: 'dist',
10 | format: 'cjs',
11 | },
12 | plugins: [
13 | typescript(),
14 | terser({
15 | keep_fnames: /.+ColorMode/
16 | }),
17 | copy({
18 | targets: [
19 | { src: './src/styles/expo-theme.css', dest: 'dist' },
20 | ],
21 | }),
22 | ],
23 | external: [
24 | '@expo/styleguide-base',
25 | 'next/link',
26 | 'react',
27 | 'tailwind-merge'
28 | ],
29 | },
30 | ];
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Button/ButtonBase.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 | import type { ButtonHTMLAttributes } from 'react';
3 |
4 | import { mergeClasses } from '../../helpers/mergeClasses';
5 |
6 | export type ButtonBaseProps = ButtonHTMLAttributes & {
7 | testID?: string;
8 | };
9 |
10 | export const ButtonBase = forwardRef(
11 | ({ children, testID, className, onClick, type = 'button', disabled = false, ...rest }, ref) => {
12 | return (
13 |
23 | );
24 | }
25 | );
26 |
27 | ButtonBase.displayName = 'ButtonBase';
28 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Button/helpers.ts:
--------------------------------------------------------------------------------
1 | const STOPWORDS = 'a an and at but by for in nor of on or out so the to up with yet'.split(' ');
2 |
3 | type Options = {
4 | keepSpaces?: boolean | undefined;
5 | stopwords?: string[] | undefined;
6 | };
7 |
8 | export function titleCase(value: string, options: Options = {}) {
9 | if (!value) return '';
10 |
11 | const stop = options.stopwords ?? STOPWORDS;
12 | const keep = options.keepSpaces;
13 | const splitter = /(\s+|[-‑–—,:;!?()])/;
14 |
15 | return value
16 | .split(splitter)
17 | .map((word, index, all) => {
18 | if (index % 2) {
19 | if (/\s+/.test(word)) return keep ? word : ' ';
20 | return word;
21 | }
22 |
23 | const lower = word.toLocaleLowerCase();
24 |
25 | if (index !== 0 && index !== all.length - 1 && stop.includes(lower)) {
26 | return lower;
27 | }
28 |
29 | return capitalize(word);
30 | })
31 | .join('');
32 | }
33 |
34 | function capitalize(text: string) {
35 | return text.charAt(0).toUpperCase() + text.slice(1);
36 | }
37 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ButtonBase';
2 | export * from './Button';
3 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Link/Link.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from 'react';
2 |
3 | import { LinkBase, LinkBaseProps } from './LinkBase';
4 | import { mergeClasses } from '../../helpers/mergeClasses';
5 |
6 | export const Link = forwardRef(({ className, disabled, ...rest }, ref) => {
7 | return (
8 |
13 | );
14 | });
15 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Link/LinkBase.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import React, { forwardRef } from 'react';
3 | import type { AnchorHTMLAttributes } from 'react';
4 |
5 | export type LinkBaseProps = AnchorHTMLAttributes & {
6 | testID?: string;
7 | openInNewTab?: boolean;
8 | disabled?: boolean;
9 | skipNextLink?: boolean;
10 | };
11 |
12 | export const LinkBase = forwardRef(
13 | ({ children, testID, href, openInNewTab, onClick, target, disabled, skipNextLink, rel, ...rest }, ref) => {
14 | if (disabled) {
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
22 | if (!href || href.startsWith('#')) {
23 | return (
24 |
25 | {children}
26 |
27 | );
28 | }
29 |
30 | const Tag = skipNextLink ? 'a' : Link;
31 |
32 | return (
33 |
41 | {children}
42 |
43 | );
44 | }
45 | );
46 |
47 | LinkBase.displayName = 'LinkBase';
48 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Link/index.ts:
--------------------------------------------------------------------------------
1 | export * from './LinkBase';
2 | export * from './Link';
3 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Theme/BlockingSetInitialColorMode.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function isLocalStorageAvailable(): boolean {
4 | try {
5 | if (!window.localStorage || typeof window.localStorage === 'undefined') {
6 | return false;
7 | }
8 | window.localStorage.setItem('localStorage:test', 'value');
9 | if (window.localStorage.getItem('localStorage:test') !== 'value') {
10 | return false;
11 | }
12 | window.localStorage.removeItem('localStorage:test');
13 | return true;
14 | } catch {
15 | return false;
16 | }
17 | }
18 |
19 | export function getInitialColorMode(): string | null {
20 | if (isLocalStorageAvailable()) {
21 | const preference = window.localStorage.getItem('data-expo-theme');
22 | const hasPreference = typeof preference === 'string';
23 |
24 | if (hasPreference) {
25 | return preference;
26 | }
27 | }
28 |
29 | const mql = window.matchMedia('(prefers-color-scheme: dark)');
30 |
31 | const systemPreference = typeof mql.matches === 'boolean';
32 | if (systemPreference) {
33 | return mql.matches ? 'dark' : 'light';
34 | }
35 |
36 | return 'light';
37 | }
38 |
39 | function setInitialColorMode() {
40 | const colorMode = getInitialColorMode();
41 |
42 | // add HTML attribute if dark mode
43 | if (colorMode === 'dark') {
44 | document.documentElement.classList.add('dark-theme');
45 | } else {
46 | document.documentElement.classList.remove('dark-theme');
47 | }
48 | }
49 |
50 | // our function needs to be a string so that we can call it
51 | const blockingSetInitialColorMode = `(function() {
52 | ${isLocalStorageAvailable.toString()}
53 | ${getInitialColorMode.toString()}
54 | ${setInitialColorMode.toString()}
55 | setInitialColorMode();
56 | })()
57 | `;
58 |
59 | export function BlockingSetInitialColorMode() {
60 | return (
61 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Theme/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useEffect, useState, useContext, ReactNode } from 'react';
2 |
3 | import { getInitialColorMode, isLocalStorageAvailable } from './BlockingSetInitialColorMode';
4 |
5 | export enum Themes {
6 | AUTO = 'auto',
7 | DARK = 'dark',
8 | LIGHT = 'light',
9 | }
10 |
11 | const ThemeContext = createContext({
12 | setDarkMode: () => {},
13 | setLightMode: () => {},
14 | setAutoMode: () => {},
15 | themeName: Themes.AUTO,
16 | });
17 |
18 | type ThemeProviderProps = {
19 | children: ReactNode;
20 | disabled?: boolean;
21 | };
22 |
23 | export function ThemeProvider(props: ThemeProviderProps) {
24 | const { children, disabled = false } = props;
25 | const initialTheme = (process as any).browser ? (document.documentElement.dataset.expoTheme as Themes) : Themes.AUTO;
26 | const [themeName, setThemeName] = useState(disabled ? Themes.LIGHT : initialTheme);
27 |
28 | useEffect(function didMount() {
29 | if (disabled) return;
30 |
31 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
32 |
33 | try {
34 | mediaQuery.addEventListener('change', onThemeChange);
35 | } catch {
36 | // Fallback to old-style listening for changes, for Safari and IE
37 | mediaQuery.addListener(onThemeChange);
38 | }
39 |
40 | if (isLocalStorageAvailable()) {
41 | const themePreference = window.localStorage.getItem('data-expo-theme');
42 |
43 | if (themePreference === Themes.LIGHT || themePreference === Themes.DARK) {
44 | setThemeName(themePreference);
45 | } else {
46 | setThemeName(Themes.AUTO);
47 | }
48 | } else {
49 | setThemeName(Themes.AUTO);
50 | }
51 |
52 | return function unMount() {
53 | try {
54 | mediaQuery.removeEventListener('change', onThemeChange);
55 | } catch {
56 | // Fallback to old-style listening for changes, for Safari and IE
57 | mediaQuery.removeListener(onThemeChange);
58 | }
59 | };
60 | }, []);
61 |
62 | function setDocumentTheme(themeName: Themes) {
63 | if (disabled) return;
64 |
65 | if (themeName === Themes.DARK) {
66 | document.documentElement.classList.add('dark-theme');
67 | } else {
68 | document.documentElement.classList.remove('dark-theme');
69 | }
70 | }
71 |
72 | function onThemeChange(event: MediaQueryListEvent | MediaQueryList) {
73 | if (isLocalStorageAvailable()) {
74 | const themePreference = window.localStorage.getItem('data-expo-theme');
75 |
76 | if (!themePreference) {
77 | if (event.matches) {
78 | setDocumentTheme(Themes.DARK);
79 | } else {
80 | setDocumentTheme(Themes.LIGHT);
81 | }
82 | }
83 | } else {
84 | setDocumentTheme(Themes.AUTO);
85 | }
86 | }
87 |
88 | function setDarkMode() {
89 | if (isLocalStorageAvailable()) {
90 | window.localStorage.setItem('data-expo-theme', Themes.DARK);
91 | }
92 | setDocumentTheme(Themes.DARK);
93 | setThemeName(Themes.DARK);
94 | }
95 |
96 | function setLightMode() {
97 | if (isLocalStorageAvailable()) {
98 | window.localStorage.setItem('data-expo-theme', Themes.LIGHT);
99 | }
100 | setDocumentTheme(Themes.LIGHT);
101 | setThemeName(Themes.LIGHT);
102 | }
103 |
104 | function setAutoMode() {
105 | if (isLocalStorageAvailable()) {
106 | window.localStorage.removeItem('data-expo-theme');
107 | }
108 |
109 | const themeName = getInitialColorMode() as Themes;
110 | setDocumentTheme(themeName);
111 | setThemeName(Themes.AUTO);
112 | }
113 |
114 | return (
115 |
122 | {children}
123 |
124 | );
125 | }
126 |
127 | export function useTheme() {
128 | const context = useContext(ThemeContext);
129 |
130 | if (context === undefined) {
131 | throw new Error('useTheme must be used within a ThemeProvider');
132 | }
133 |
134 | return context;
135 | }
136 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/Theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from './BlockingSetInitialColorMode';
2 | export * from './ThemeProvider';
3 |
--------------------------------------------------------------------------------
/packages/styleguide/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Theme';
2 | export * from './Button';
3 | export * from './Link';
4 |
--------------------------------------------------------------------------------
/packages/styleguide/src/helpers/mergeClasses.ts:
--------------------------------------------------------------------------------
1 | import { extendTailwindMerge } from 'tailwind-merge';
2 |
3 | type AdditionalClassGroupIds = 'icon' | 'heading';
4 |
5 | export const mergeClasses = extendTailwindMerge({
6 | extend: {
7 | conflictingClassGroups: {
8 | heading: ['font-size', 'leading'],
9 | },
10 | classGroups: {
11 | icon: [{ icon: ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] }],
12 | heading: [{ heading: ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl'] }],
13 | },
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/DocsLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function DocsLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/ExpoGoLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function ExpoGoLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/Logo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function Logo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/OrbitLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function OrbitLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/RouterLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function RouterLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/SnackLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function SnackLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/WordMarkLogo.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLAttributes } from 'react';
2 |
3 | import { mergeClasses } from '../helpers/mergeClasses';
4 |
5 | export function WordMarkLogo({ className, ...rest }: HTMLAttributes) {
6 | const _className = mergeClasses('icon-md text-icon-default', className);
7 | return (
8 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/packages/styleguide/src/logos/index.tsx:
--------------------------------------------------------------------------------
1 | export { DocsLogo } from './DocsLogo';
2 | export { ExpoGoLogo } from './ExpoGoLogo';
3 | export { Logo } from './Logo';
4 | export { OrbitLogo } from './OrbitLogo';
5 | export { RouterLogo } from './RouterLogo';
6 | export { SnackLogo } from './SnackLogo';
7 | export { WordMarkLogo } from './WordMarkLogo';
8 |
--------------------------------------------------------------------------------
/packages/styleguide/src/styles/colors.ts:
--------------------------------------------------------------------------------
1 | export const appBackgroundColors = {
2 | cyan: '#07c0cb',
3 | lightBlue: '#1e92c4',
4 | darkBlue: '#0b67af',
5 | indigo: '#4b50b2',
6 | purple: '#8945a3',
7 | pink: '#c04891',
8 | orange: '#e96d3c',
9 | gold: '#f38f2f',
10 | yellow: '#eebc01',
11 | lime: '#aabd04',
12 | lightGreen: '#6aa72a',
13 | darkGreen: '#3a8e39',
14 | };
15 |
--------------------------------------------------------------------------------
/packages/styleguide/src/styles/shadows.ts:
--------------------------------------------------------------------------------
1 | export const shadows = {
2 | none: 'var(--expo-theme-shadows-none)',
3 | xs: 'var(--expo-theme-shadows-xs)',
4 | sm: 'var(--expo-theme-shadows-sm)',
5 | md: 'var(--expo-theme-shadows-md)',
6 | lg: 'var(--expo-theme-shadows-lg)',
7 | xl: 'var(--expo-theme-shadows-xl)',
8 | };
9 |
--------------------------------------------------------------------------------
/packages/styleguide/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "strict": true,
5 | "noImplicitAny": true,
6 | "allowSyntheticDefaultImports": true,
7 | "outDir": "dist",
8 | "moduleResolution": "node",
9 | "esModuleInterop": true,
10 | "declaration": true
11 | },
12 | "include": ["./src/**/*.tsx", "./src/**/*.ts", "./index.ts"],
13 | "exclude": ["node_modules"]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "commonjs",
5 | "declaration": true,
6 | "strict": true,
7 | "jsx": "react",
8 | "esModuleInterop": true
9 | },
10 | "exclude": ["./node_modules/*"]
11 | }
12 |
--------------------------------------------------------------------------------