diff --git a/README.md b/README.md index fb1b2b5..1e22287 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,74 @@ Display Github permalinks as codeblocks. Display Github issue links. +Display browser compatibility tables from caniuse.com. + +![screenshot of the tool in action - dark mode ](./screenshot-permalink-dark.png) +![screenshot of the tool in action - light mode ](./screenshot-permalink-light.png) +![screenshot of the tool in action - dark mode ](./screenshot-issuelink-dark.png) +![screenshot of the tool in action - light mode ](./screenshot-issuelink-light.png) +![screenshot of the tool in action - dark mode - inline ](./screenshot-issuelink-inline-dark.png) +![screenshot of the tool in action - light mode - inline ](./screenshot-issuelink-inline-light.png) + +## CaniuseLink Component + +The CaniuseLink component displays live browser compatibility tables for web features using data from caniuse.com. It shows support across different browsers and versions with color-coded indicators. + +### Basic Usage + +```jsx +import { CaniuseLink } from 'react-github-permalink'; +import "react-github-permalink/dist/github-permalink.css"; + +export function MyApp() { + return +} +``` + +### Inline Usage + +```jsx +

This layout uses for positioning.

+``` + +### With Custom Data + +```jsx +import { CaniuseLinkBase } from 'react-github-permalink'; + +const customData = { + title: "CSS Flexible Box Layout Module", + description: "Method of positioning elements...", + stats: { + chrome: { "89": "y", "90": "y" }, + firefox: { "84": "y", "85": "y" }, + safari: { "14.0": "y", "14.1": "y" }, + edge: { "89": "y", "90": "y" }, + ie: { "8": "n", "9": "n", "10": "a x", "11": "a" } + }, + status: "ok" +}; + +export function MyApp() { + return +} +``` + +### RSC Usage + +```jsx +import { CaniuseLinkRsc } from 'react-github-permalink/dist/rsc'; + +export function MyApp() { + return +} +``` + +The component fetches live data from the caniuse GitHub repository, ensuring up-to-date compatibility information. Support levels are color-coded: +- **Green**: Fully supported +- **Yellow**: Partial support +- **Red**: Not supported + ![screenshot of the tool in action - dark mode ](./screenshot-permalink-dark.png) ![screenshot of the tool in action - light mode ](./screenshot-permalink-light.png) ![screenshot of the tool in action - dark mode ](./screenshot-issuelink-dark.png) @@ -126,6 +194,7 @@ The global configuration object has this signature type BaseConfiguration = { getDataFn: (permalink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise; getIssueFn: (issueLink: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise; + getCaniuseFn: (feature: string, githubToken?: string | undefined, onError?: ((err: unknown) => void) | undefined) => Promise; githubToken: string | undefined; onError: ((e: unknown) => void) | undefined; } @@ -136,21 +205,24 @@ type BaseConfiguration = { Client components are configured via context provider: ```tsx -import { GithubPermalink, GithubIssueLink GithubPermalinkProvider, } from 'react-github-permalink'; +import { GithubPermalink, GithubIssueLink, CaniuseLink, GithubPermalinkProvider } from 'react-github-permalink'; import "react-github-permalink/dist/github-permalink.css"; export function MyApp() { return { + getDataFn={(permalink: string) => { // Your implementation to retrieve permalinks here }} getIssueFn={(issueLink: string) => { // Your implementation to retrieve issue links here }} + getCaniuseFn={(feature: string) => { + // Your implementation to retrieve caniuse data here + }} - // Don't put a put a github token into the context provider in production! It will visible for all the world to see! + // Don't put a github token into the context provider in production! It will be visible for all the world to see! // Instead you will need to expose a data fetching function on the backend to do it for you - githubToken={process.env.NODE_ENV='development' && process.env.MY_GITHUB_TOKEN} + githubToken={process.env.NODE_ENV === 'development' && process.env.MY_GITHUB_TOKEN} onError={(err) => { Sentry.captureException(err); @@ -158,6 +230,7 @@ export function MyApp() { > + } ``` diff --git a/src/app/page.tsx b/src/app/page.tsx index 77ec7d0..d5388d8 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,49 @@ +import { CaniuseLink } from "../library/CaniuseLink/CaniuseLink"; +import { CaniuseLinkBase } from "../library/CaniuseLink/CaniuseLinkBase"; +import { GithubPermalinkProvider } from "../library/config/GithubPermalinkContext"; +import "../library/GithubPermalink/github-permalink.css"; + +// Mock data for demonstration +const mockFlexboxData = { + title: "CSS Flexible Box Layout Module", + description: "Method of positioning elements in horizontal or vertical stacks. Support includes all properties prefixed with `flex`, as well as `display: flex`, `display: inline-flex`, `align-content`, `align-items`, `align-self`, `justify-content` and `order`.", + stats: { + chrome: { "89": "y", "90": "y", "91": "y", "92": "y", "93": "y" }, + firefox: { "84": "y", "85": "y", "86": "y", "87": "y", "88": "y" }, + safari: { "14.0": "y", "14.1": "y", "15.0": "y", "15.1": "y", "15.2": "y" }, + edge: { "89": "y", "90": "y", "91": "y", "92": "y", "93": "y" }, + ie: { "8": "n", "9": "n", "10": "a x", "11": "a" } + }, + status: "ok" as const +}; + export default function Home() { return (
-
+

React Github Permalink

- - - See the Storybook +

See the Storybook

+ +

CaniuseLink Demo

+ +
+

Live API Example

+ +
+ +
+

Mock Data Example

+ +
+ +
+

Inline Usage

+

+ This layout uses for positioning elements. +

+
+
); diff --git a/src/library/CaniuseLink/CaniuseLink.stories.tsx b/src/library/CaniuseLink/CaniuseLink.stories.tsx new file mode 100644 index 0000000..498f141 --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLink.stories.tsx @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CaniuseLink } from "./CaniuseLink"; +import { + GithubPermalinkProvider, +} from "../config/GithubPermalinkContext"; +import "../GithubPermalink/github-permalink.css"; + +const meta: Meta = { + component: CaniuseLink, +}; + +export default meta; + +type Story = StoryObj; + +export const BlockSuccess: Story = { + render: () => ( + + + + ), +}; + +export const InlineSuccess: Story = { + render: () => ( + +

+ This example uses{" "} + + {" "}for layout. +

+
+ ), +}; + +export const BlockError: Story = { + render: () => ( + + + + ), +}; \ No newline at end of file diff --git a/src/library/CaniuseLink/CaniuseLink.test.ts b/src/library/CaniuseLink/CaniuseLink.test.ts new file mode 100644 index 0000000..a7d6058 --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLink.test.ts @@ -0,0 +1,71 @@ +import { expect, test, describe, vi } from 'vitest' +import { defaultGetCaniuseFn } from '../config/defaultFunctions' + +// Mock fetch for testing +global.fetch = vi.fn() + +describe('defaultGetCaniuseFn', () => { + test('should handle successful response', async () => { + const mockResponse = { + title: 'CSS Flexible Box Layout Module', + description: 'Method of positioning elements in horizontal or vertical stacks.', + stats: { + chrome: { + '89': 'y', + '90': 'y' + }, + firefox: { + '84': 'y', + '85': 'y' + } + } + } + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + } as Response) + + const result = await defaultGetCaniuseFn('flexbox') + + expect(result).toEqual({ + title: 'CSS Flexible Box Layout Module', + description: 'Method of positioning elements in horizontal or vertical stacks.', + stats: { + chrome: { + '89': 'y', + '90': 'y' + }, + firefox: { + '84': 'y', + '85': 'y' + } + }, + status: 'ok' + }) + }) + + test('should handle 404 response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 404, + headers: new Headers(), + } as Response) + + const result = await defaultGetCaniuseFn('non-existent-feature') + + expect(result).toEqual({ + status: '404' + }) + }) + + test('should handle network error', async () => { + vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error')) + + const result = await defaultGetCaniuseFn('flexbox') + + expect(result).toEqual({ + status: 'other-error' + }) + }) +}) \ No newline at end of file diff --git a/src/library/CaniuseLink/CaniuseLink.tsx b/src/library/CaniuseLink/CaniuseLink.tsx new file mode 100644 index 0000000..027e9cf --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLink.tsx @@ -0,0 +1,30 @@ +"use client" + +import { useContext, useEffect, useState } from "react"; +import { GithubPermalinkContext, CaniuseLinkDataResponse } from "../config/GithubPermalinkContext"; +import { CaniuseLinkBase, CaniuseLinkBaseProps } from "./CaniuseLinkBase"; + +type CaniuseLinkProps = Omit; + +export function CaniuseLink(props: CaniuseLinkProps) { + const { feature } = props; + const [data, setData] = useState(null as null | CaniuseLinkDataResponse); + const { getCaniuseFn, githubToken, onError } = useContext(GithubPermalinkContext); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + getCaniuseFn(feature, githubToken, onError).then((v) => { + setIsLoading(false); + setData(v); + }); + }, [getCaniuseFn, githubToken, feature, onError]); + + if (isLoading) { + return null; + } + if (!data) { + throw new Error("Loading is complete, but no data was returned."); + } + + return ; +} \ No newline at end of file diff --git a/src/library/CaniuseLink/CaniuseLinkBase.stories.tsx b/src/library/CaniuseLink/CaniuseLinkBase.stories.tsx new file mode 100644 index 0000000..ba8ab95 --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLinkBase.stories.tsx @@ -0,0 +1,103 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { CaniuseLinkBase } from "./CaniuseLinkBase"; +import "../GithubPermalink/github-permalink.css"; + +const meta: Meta = { + component: CaniuseLinkBase, +}; + +export default meta; + +type Story = StoryObj; + +const mockFlexboxData = { + title: "CSS Flexible Box Layout Module", + description: "Method of positioning elements in horizontal or vertical stacks. Support includes all properties prefixed with `flex`, as well as `display: flex`, `display: inline-flex`, `align-content`, `align-items`, `align-self`, `justify-content` and `order`.", + stats: { + chrome: { + "89": "y", + "90": "y", + "91": "y", + "92": "y", + "93": "y" + }, + firefox: { + "84": "y", + "85": "y", + "86": "y", + "87": "y", + "88": "y" + }, + safari: { + "14.0": "y", + "14.1": "y", + "15.0": "y", + "15.1": "y", + "15.2": "y" + }, + edge: { + "89": "y", + "90": "y", + "91": "y", + "92": "y", + "93": "y" + }, + ie: { + "8": "n", + "9": "n", + "10": "a x", + "11": "a" + } + }, + status: "ok" as const +}; + +export const BlockSuccess: Story = { + render: () => ( + + ), +}; + +export const BlockError: Story = { + render: () => ( + + ), +}; + +export const InlineSuccess: Story = { + render: () => ( +

+ This example uses {" "} + + {" "} for layout. +

+ ), +}; + +export const InlineError: Story = { + render: () => ( +

+ This example uses {" "} + + {" "} for layout. +

+ ), +}; \ No newline at end of file diff --git a/src/library/CaniuseLink/CaniuseLinkBase.tsx b/src/library/CaniuseLink/CaniuseLinkBase.tsx new file mode 100644 index 0000000..cd9383a --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLinkBase.tsx @@ -0,0 +1,130 @@ +import { PropsWithChildren } from "react"; +import { CaniuseLinkDataResponse } from "../config/GithubPermalinkContext"; +import { ErrorMessages } from "../ErrorMessages/ErrorMessages"; + +export type CaniuseLinkBaseProps = { + className?: string; + feature: string; + data: CaniuseLinkDataResponse; + variant?: "inline" | "block"; +} + +export function CaniuseLinkBase(props: CaniuseLinkBaseProps) { + const { data, variant = "block", feature } = props; + + if (variant === "inline") { + if (data.status === "ok") { + return ( + + + {data.title} + + + ); + } else { + return ( + + + {feature} + + + ); + } + } + + if (data.status === "ok") { + return ( + +
+

{data.title}

+ + View on caniuse.com + +
+ {data.description && ( +

{data.description}

+ )} + + }> +
+
+ {Object.entries(data.stats).map(([browser, versions]) => ( +
+
+ {getBrowserName(browser)} +
+
+ {Object.entries(versions).slice(-5).map(([version, support]) => ( +
+ {version} +
+ ))} +
+
+ ))} +
+
+
+ ); + } + + return ( + + + + ); +} + +function CaniuseLinkInner(props: PropsWithChildren<{ + header?: React.ReactNode +} & { + feature: string; + className?: string; +}>) { + const { feature, className = '' } = props; + const caniuseUrl = `https://caniuse.com/${feature}`; + + return ( +
+
+ {props.header ?? {feature}} +
+ {props.children} +
+ ); +} + +function getBrowserName(browser: string): string { + const browserNames: Record = { + chrome: "Chrome", + firefox: "Firefox", + safari: "Safari", + edge: "Edge", + ie: "IE", + opera: "Opera", + ios_saf: "iOS Safari", + and_chr: "Android Chrome", + and_ff: "Android Firefox", + samsung: "Samsung Internet" + }; + return browserNames[browser] || browser; +} + +function getSupportClass(support: string): string { + if (support === "y") return "supported"; + if (support === "n") return "not-supported"; + if (support.startsWith("a")) return "partial"; + return "unknown"; +} + +function getSupportTitle(support: string): string { + if (support === "y") return "Supported"; + if (support === "n") return "Not supported"; + if (support.startsWith("a")) return "Partial support"; + return "Unknown support"; +} \ No newline at end of file diff --git a/src/library/CaniuseLink/CaniuseLinkRsc.tsx b/src/library/CaniuseLink/CaniuseLinkRsc.tsx new file mode 100644 index 0000000..1a85b4f --- /dev/null +++ b/src/library/CaniuseLink/CaniuseLinkRsc.tsx @@ -0,0 +1,13 @@ +import { githubPermalinkRscConfig } from "../config/GithubPermalinkRscConfig"; +import { CaniuseLinkBase, CaniuseLinkBaseProps } from "./CaniuseLinkBase"; + +export type CaniuseLinkRscProps = Omit; + +export async function CaniuseLinkRsc(props: CaniuseLinkRscProps) { + const dataFn = githubPermalinkRscConfig.getCaniuseFn(); + const token = githubPermalinkRscConfig.getGithubToken(); + const onError = githubPermalinkRscConfig.getOnError(); + + const data = await dataFn(props.feature, token, onError); + return ; +} \ No newline at end of file diff --git a/src/library/GithubPermalink/github-permalink.css b/src/library/GithubPermalink/github-permalink.css index 5229982..9b25107 100644 --- a/src/library/GithubPermalink/github-permalink.css +++ b/src/library/GithubPermalink/github-permalink.css @@ -19,6 +19,11 @@ --rgp-color-status-error: red; + --rgp-color-caniuse-supported: #13a10e; + --rgp-color-caniuse-partial: #ffb700; + --rgp-color-caniuse-not-supported: #d93025; + --rgp-color-caniuse-unknown: #666; + --rgp-color-reaction-foreground: #0969da; --rgp-color-reaction-background: #ddf4ff; @@ -62,6 +67,11 @@ --rgp-color-status-closed: rgb(130, 80, 223); --rgp-color-status-error: red; + --rgp-color-caniuse-supported: #13a10e; + --rgp-color-caniuse-partial: #ffb700; + --rgp-color-caniuse-not-supported: #d93025; + --rgp-color-caniuse-unknown: #666; + --rgp-color-reaction-foreground: #4493f8; --rgp-color-reaction-background: #388bfd33; @@ -400,3 +410,125 @@ svg.github-logo{ margin: 0 2px; } } + +/* CaniuseLink styles */ +.react-caniuse-link { + border: solid 1px var(--rgp-color-border); + margin: 0.5em; + font-size: 12px; + border-radius: 4px; + text-align: left; + background-color: var(--rgp-color-bg-stark); +} + +.react-caniuse-header { + background-color: var(--rgp-color-bg-frame); + padding: 8px; + border-bottom: solid 1px var(--rgp-color-border); +} + +.react-caniuse-title { + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + align-items: center; + gap: 1em; +} + +.react-caniuse-title h3 { + margin: 0; + font-size: 16px; + color: var(--rgp-color-text-stark); +} + +.react-caniuse-link a { + color: var(--rgp-color-file-link); + text-decoration: none; + font-size: 12px; +} + +.react-caniuse-link a:hover { + text-decoration: underline; +} + +.react-caniuse-description { + margin: 0.5em 0 0 0; + color: var(--rgp-color-text-frame); + font-size: 12px; + line-height: 1.4; +} + +.react-caniuse-table { + padding: 12px; +} + +.react-caniuse-browsers { + display: flex; + flex-direction: column; + gap: 8px; +} + +.react-caniuse-browser { + display: flex; + flex-flow: row nowrap; + align-items: center; + gap: 8px; +} + +.react-caniuse-browser-name { + min-width: 120px; + font-weight: bold; + color: var(--rgp-color-text-stark); + font-size: 12px; +} + +.react-caniuse-versions { + display: flex; + flex-flow: row nowrap; + gap: 4px; +} + +.react-caniuse-version { + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: bold; + color: white; + min-width: 40px; + text-align: center; +} + +.react-caniuse-version.supported { + background-color: var(--rgp-color-caniuse-supported); +} + +.react-caniuse-version.partial { + background-color: var(--rgp-color-caniuse-partial); +} + +.react-caniuse-version.not-supported { + background-color: var(--rgp-color-caniuse-not-supported); +} + +.react-caniuse-version.unknown { + background-color: var(--rgp-color-caniuse-unknown); +} + +.react-caniuse-inline { + display: inline-block; + margin: 0 0.25em; +} + +.react-caniuse-inline a { + color: var(--rgp-color-file-link); + text-decoration: none; + font-family: monospace; + background-color: var(--rgp-color-bg-frame); + padding: 0.125em 0.25em; + border-radius: 2px; + border: solid 1px var(--rgp-color-border); +} + +.react-caniuse-inline a:hover { + text-decoration: underline; +} diff --git a/src/library/config/BaseConfiguration.ts b/src/library/config/BaseConfiguration.ts index f75651f..f8d9d8d 100644 --- a/src/library/config/BaseConfiguration.ts +++ b/src/library/config/BaseConfiguration.ts @@ -1,5 +1,6 @@ import { defaultGetPermalinkFn } from "./defaultFunctions"; import { defaultGetIssueFn } from "./defaultFunctions"; +import { defaultGetCaniuseFn } from "./defaultFunctions"; export type BaseConfiguration = { @@ -11,6 +12,9 @@ export type BaseConfiguration = { /** Function to provide issue data payload */ getIssueFn: typeof defaultGetIssueFn; + /** Function to provide caniuse data payload */ + getCaniuseFn: typeof defaultGetCaniuseFn; + /** * A github personal access token - will be passed to the data fetching functions */ diff --git a/src/library/config/GithubPermalinkContext.tsx b/src/library/config/GithubPermalinkContext.tsx index c9b7f57..6822a32 100644 --- a/src/library/config/GithubPermalinkContext.tsx +++ b/src/library/config/GithubPermalinkContext.tsx @@ -3,6 +3,7 @@ import { PropsWithChildren, createContext } from "react"; import { BaseConfiguration } from "./BaseConfiguration"; import { defaultGetIssueFn } from "./defaultFunctions"; import { defaultGetPermalinkFn } from "./defaultFunctions"; +import { defaultGetCaniuseFn } from "./defaultFunctions"; // Thanks ChatGPT export type GithubPermalinkUrlInfo = { @@ -56,6 +57,17 @@ export type GithubIssueLinkDataResponse = { } } | ErrorResponses; +export type CaniuseLinkDataResponse = { + title: string; + description?: string; + stats: { + [browser: string]: { + [version: string]: string; + }; + }; + status: "ok"; +} | ErrorResponses; + @@ -63,12 +75,14 @@ export type GithubIssueLinkDataResponse = { export const GithubPermalinkContext = createContext({ getDataFn: defaultGetPermalinkFn, getIssueFn: defaultGetIssueFn, + getCaniuseFn: defaultGetCaniuseFn, }); export function GithubPermalinkProvider(props: PropsWithChildren>) { return diff --git a/src/library/config/GithubPermalinkRscConfig.ts b/src/library/config/GithubPermalinkRscConfig.ts index 0e66ded..e5c0f73 100644 --- a/src/library/config/GithubPermalinkRscConfig.ts +++ b/src/library/config/GithubPermalinkRscConfig.ts @@ -1,9 +1,10 @@ import { BaseConfiguration } from "./BaseConfiguration"; -import { defaultGetIssueFn, defaultGetPermalinkFn } from "./defaultFunctions"; +import { defaultGetIssueFn, defaultGetPermalinkFn, defaultGetCaniuseFn } from "./defaultFunctions"; const defaultConfiguration = { getDataFn: defaultGetPermalinkFn, getIssueFn: defaultGetIssueFn, + getCaniuseFn: defaultGetCaniuseFn, }; class GithubPermalinkRscConfig { @@ -23,6 +24,10 @@ class GithubPermalinkRscConfig { return this.baseConfiguration.getIssueFn; } + public getCaniuseFn() { + return this.baseConfiguration.getCaniuseFn; + } + public getGithubToken() { return this.baseConfiguration.githubToken; } diff --git a/src/library/config/defaultFunctions.ts b/src/library/config/defaultFunctions.ts index 63f54ad..f8bd459 100644 --- a/src/library/config/defaultFunctions.ts +++ b/src/library/config/defaultFunctions.ts @@ -2,6 +2,7 @@ import { GithubIssueLinkDataResponse } from "./GithubPermalinkContext"; import { parseGithubIssueLink, parseGithubPermalinkUrl } from "../utils/urlParsers"; import { GithubPermalinkDataResponse } from "./GithubPermalinkContext"; import { ErrorResponses } from "./GithubPermalinkContext"; +import { CaniuseLinkDataResponse } from "./GithubPermalinkContext"; export async function defaultGetIssueFn(issueLink: string, githubToken?: string, onError?: (err: unknown) => void): Promise { @@ -97,3 +98,28 @@ export function handleResponse(response: Response): ErrorResponses { }; } +export async function defaultGetCaniuseFn(feature: string, _githubToken?: string, onError?: (err: unknown) => void): Promise { + try { + const response = await fetch(`https://raw.githubusercontent.com/Fyrd/caniuse/main/features-json/${feature}.json`); + + if (!response.ok) { + onError?.(response); + return handleResponse(response); + } + + const data = await response.json(); + + return { + title: data.title, + description: data.description, + stats: data.stats, + status: "ok" + }; + } catch (error) { + onError?.(error); + return { + status: "other-error" + }; + } +} + diff --git a/src/library/export.ts b/src/library/export.ts index 31db22b..b4ee39c 100644 --- a/src/library/export.ts +++ b/src/library/export.ts @@ -3,4 +3,6 @@ export * from "./GithubPermalink/GithubPermalink"; export * from "./GithubPermalink/GithubPermalinkBase"; export * from "./GithubIssueLink/GithubIssueLink"; export * from "./GithubIssueLink/GithubIssueLinkBase" +export * from "./CaniuseLink/CaniuseLink"; +export * from "./CaniuseLink/CaniuseLinkBase"; export * from "./config/GithubPermalinkContext"; diff --git a/src/library/rsc.ts b/src/library/rsc.ts index ce35b3d..c6cc6d7 100644 --- a/src/library/rsc.ts +++ b/src/library/rsc.ts @@ -1,3 +1,4 @@ export * from "./GithubPermalink/GithubPermalinkRsc"; export * from "./config/GithubPermalinkRscConfig"; -export * from "./GithubIssueLink/GithubIssueLinkRsc"; \ No newline at end of file +export * from "./GithubIssueLink/GithubIssueLinkRsc"; +export * from "./CaniuseLink/CaniuseLinkRsc"; \ No newline at end of file