Compare commits

...

19 Commits

Author SHA1 Message Date
Balázs Orbán
35583a513d fix: ts type, and transpilation (#2037)
* fix(ts): mark getUserByEmail param as nullable

* fix(build): transpile with optional-catch-binding
2021-05-20 20:40:45 +02:00
Nico Domino
665d91019f style: small tweaks to navbar (#2024) 2021-05-20 16:22:31 +02:00
Daniel Sabbagh
f2b816b7b9 docs: fix minor typo (#2022)
* fix minor typo

* fix typo again

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-19 20:59:00 +02:00
Nico Domino
2e770fb0bf docs: update github PR template comments (#2025) 2021-05-19 01:11:42 +02:00
Nico Domino
e83e7231fb docs(search): add new algolia docsearch (#2023)
* docs(search): add new algolia docsearch

* style(search): fix algolia docsearch mobile style
2021-05-18 21:49:43 +02:00
Marco Valsecchi
4593ec8b01 docs(provider): Fix Using a custom OAuth Provider index link (#2019) 2021-05-18 14:30:17 +02:00
Nico Domino
12517f629b docs(style): add github star counter to navbar (#2015)
* docs(style): add github star counter to navbar

* chore: cleanup kFormatter logic
2021-05-18 00:23:44 +02:00
Balázs Orbán
77012bc00c fix(deps): pin down legacy adapter versions (#2009)
* fix(deps): pin down legacy adapter versions

* chore: trigger github actions
2021-05-16 20:52:04 +02:00
Chalk
60fdf26a56 fix(provider): support multiple image formats for Twitter profile (#1995)
see supported formats: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-15 23:28:34 +02:00
Igor Danchenko
0fae0c7a8e feat(provider): forward request to authorize (#1979)
* feat/add-request-to-credentials-authorize

* Update src/server/routes/callback.js

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Update types/providers.d.ts

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Update www/docs/providers/credentials.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Update www/docs

* Update test app

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-15 03:00:39 +02:00
Nico Domino
eba79f4445 feat: upgrade docusaurus + style a bit (#1993) 2021-05-13 23:24:02 +02:00
Balázs Orbán
e3bb9881ea chore(dev): fix dev app imports (#1991) 2021-05-13 12:36:28 +02:00
Balázs Orbán
827049cb35 docs(www): Docusaurus webpack 5 (#1989)
This reverts commit bc9805d1ba.
2021-05-13 01:28:36 +02:00
Nico Domino
ad8100d402 docs: max cookie size information (#1949)
* fix: max cookie information

* fix: typo

* fix: wording regarding cookie size

* Update www/docs/faq.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:22:08 +02:00
dependabot[bot]
7b5defff16 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#1976)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:20:24 +02:00
Balázs Orbán
bc9805d1ba docs: revert "Docusaurus webpack 5" (#1982)
This reverts commit c823016b36.
2021-05-12 10:19:30 +02:00
Sébastien Lorber
c823016b36 docs(www): update Docusaurus to webpack 5 (#1826)
* upgrade

* upgrade

* fix lunr plugin bug

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-12 10:01:33 +02:00
dependabot[bot]
ca0f4c6fba chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /www (#1977)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 21:04:27 +02:00
Balázs Orbán
c0d2f2d852 fix(adapter): upgrade legacy adapters (#1952)
* refactor(adapter): upgrade typeorm-legacy-adapter

* fix(ts): correct exported typeorm types

* fix(adapter): correct adapter exports

* chore(deps): upgrade typeorm-legacy-adapter

* chore(deps): upgrade dependencies

* chore: match comment for legacy adapters

* fix(ts): correctly export Prisma legacy types

* chore(deps): upgrade prisma legacy adapter

* chore(deps): remove unused dependencies

* test(ts): only run TS tests on latest TS version

* chore(deps): remove unused dev dependencies

* chore(deps): upgrade prisma adapter
2021-05-11 00:15:01 +02:00
35 changed files with 19298 additions and 11771 deletions

View File

@@ -18,21 +18,23 @@ merge of your pull request!
## Reasoning 💡
What changes are being made? What feature/bug is being fixed here?
<!-- What changes are being made? What feature/bug is being fixed here? -->
## Checklist 🧢
Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
<!-- Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
To check an item, place an `x` in the box like so: `- [x] Documentation`.
To check an item, place an `x` in the box like so: `- [x] Documentation`. -->
- [ ] Documentation
- [ ] Tests
- [ ] Ready to be merged
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
## Affected issues 🎟
<!--
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
If you write `"Fixes"` or `"Closes"` before the issue link like so:
@@ -42,3 +44,5 @@ Fixes #359
```
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
-->

View File

@@ -1,5 +1,9 @@
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
@@ -28,15 +32,15 @@ export default NextAuth({
// }
// },
providers: [
Providers.Email({
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
from: process.env.EMAIL_FROM,
}),
Providers.GitHub({
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Auth0({
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
@@ -45,36 +49,36 @@ export default NextAuth({
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
protection: "pkce",
}),
Providers.Twitter({
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
clientSecret: process.env.TWITTER_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
CredentialsProvider({
name: "Credentials",
credentials: {
password: { label: 'Password', type: 'password' }
password: { label: "Password", type: "password" },
},
async authorize (credentials) {
if (credentials.password === 'password') {
async authorize(credentials, req) {
if (credentials.password === "password") {
return {
id: 1,
name: 'Fill Murray',
email: 'bill@fillmurray.com',
image: 'https://www.fillmurray.com/64/64'
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
}
}
return null
}
})
},
}),
],
jwt: {
encryption: true,
secret: process.env.SECRET
secret: process.env.SECRET,
},
debug: false,
theme: 'auto'
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL

View File

@@ -5,7 +5,7 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
plugins: [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
],
comments: false,

4652
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,7 @@
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test": "echo \"Write some tests...\"; npm run test:types",
"test:types": "dtslint types",
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
@@ -62,9 +62,8 @@
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "canary",
"@next-auth/typeorm-legacy-adapter": "canary",
"crypto-js": "^4.0.0",
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
@@ -73,9 +72,7 @@
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
"querystring": "^0.2.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
@@ -91,10 +88,9 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@babel/preset-env": "^7.14.2",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
@@ -115,19 +111,10 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"next": "^10.0.5",
"pg": "^8.2.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"prisma": "^2.16.1",
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3"

View File

@@ -1,8 +1,10 @@
import TypeORM from './typeorm'
import Prisma from './prisma'
import * as TypeORM from "./typeorm"
import * as Prisma from "./prisma"
export { TypeORM, Prisma }
export default {
Default: TypeORM.Adapter,
TypeORM,
Prisma
Prisma,
}

View File

@@ -1,8 +1,6 @@
/*
* Source code is now at:
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
*/
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
export default PrismaLegacyAdapter
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"

View File

@@ -1,8 +1,9 @@
/*
* Source code is now at:
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
*/
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
export default TypeORMLegacyAdapter
export {
TypeORMLegacyAdapter as Adapter,
Models,
} from "@next-auth/typeorm-legacy-adapter"

View File

@@ -15,7 +15,7 @@ export default function Twitter(options) {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, ".jpg"),
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
}
},
...options,

View File

@@ -336,7 +336,7 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials
credentials, {...req, options: {}, cookies: {}}
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res

33
types/adapters.d.ts vendored
View File

@@ -1,13 +1,36 @@
import { AppOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig } from "./providers"
import { ConnectionOptions } from "typeorm"
/** Legacy */
export {
TypeORMAccountModel,
TypeORMSessionModel,
TypeORMUserModel,
TypeORMVerificationRequestModel,
} from "@next-auth/typeorm-legacy-adapter"
import {
TypeORMAdapter,
TypeORMAdapterModels,
} from "@next-auth/typeorm-legacy-adapter"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
export const TypeORM: {
Models: TypeORMAdapterModels
Adapter: TypeORMAdapter
}
export const Prisma: {
Adapter: PrismaLegacyAdapter
}
declare const Adapters: {
Default: Adapter<ConnectionOptions>
TypeORM: { Adapter: Adapter<ConnectionOptions> }
Prisma: { Adapter: Adapter }
Default: TypeORMAdapter
TypeORM: typeof TypeORM
Prisma: typeof Prisma
}
export default Adapters
@@ -26,7 +49,7 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
displayName?: string
createUser(profile: P): Promise<U>
getUser(id: string): Promise<U | null>
getUserByEmail(email: string): Promise<U | null>
getUserByEmail(email: string | null): Promise<U | null>
getUserByProviderAccountId(
providerId: string,
providerAccountId: string

View File

@@ -1,5 +1,5 @@
import { Profile, TokenSet, User } from "."
import { Awaitable } from "./internals/utils"
import { Awaitable, NextApiRequest } from "./internals/utils"
export type ProviderType = "oauth" | "email" | "credentials"
@@ -115,7 +115,7 @@ interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(credentials: Record<keyof C, string>): Awaitable<User | null>
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
}
export type CredentialsProvider = (

View File

@@ -156,9 +156,9 @@ Check out the content of all the params in addition `token`, to see what info yo
:::
:::warning
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain is commonly imposed by browsers.
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
:::
## Session callback

View File

@@ -8,7 +8,7 @@ Authentication Providers in **NextAuth.js** are services that can be used to sig
There's four ways a user can be signed in:
- [Using a built-in OAuth Provider](#oauth-providers) (e.g Github, Twitter, Google, etc...)
- [Using a custom OAuth Provider](#-using-a-custom-provider)
- [Using a custom OAuth Provider](#using-a-custom-provider)
- [Using Email](#email-provider)
- [Using Credentials](#credentials-provider)
@@ -254,12 +254,14 @@ providers: [
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
const user = (credentials) => {
async authorize(credentials, req) {
const user = (credentials, req) => {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the request object to obtain additional parameters
// (i.e., the request IP address)
return null
}
if (user) {
@@ -282,10 +284,10 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
### Options
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials) => Promise<User>` | Yes |
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :-----------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials, req) => Promise<User>` | Yes |

View File

@@ -196,9 +196,9 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.

View File

@@ -3,7 +3,7 @@ id: typescript
title: TypeScript
---
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about how certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
https://github.com/nextauthjs/next-auth-typescript-example

View File

@@ -39,6 +39,8 @@ The Credentials provider is specified like other providers, except that you need
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
The Credentials provider's `authorize()` method also provides the request object as the second parameter (see example below).
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
@@ -53,7 +55,7 @@ providers: [
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
@@ -90,7 +92,7 @@ As with all providers, the order you specify them is the order they are displaye
Providers.Credentials({
id: 'domain-login',
name: "Domain Account",
async authorize(credentials) {
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},
@@ -103,7 +105,7 @@ As with all providers, the order you specify them is the order they are displaye
Providers.Credentials({
id: 'intranet-credentials',
name: "Two Factor Auth",
async authorize(credentials) {
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},

View File

@@ -22,7 +22,7 @@ export default NextAuth({
username: { label: "DN", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
async authorize(credentials, req) {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: process.env.LDAP_URI,

View File

@@ -1,52 +1,57 @@
module.exports = {
title: 'NextAuth.js',
tagline: 'Authentication for Next.js',
url: 'https://next-auth.js.org',
baseUrl: '/',
favicon: 'img/favicon.ico',
organizationName: 'nextauthjs',
projectName: 'next-auth',
title: "NextAuth.js",
tagline: "Authentication for Next.js",
url: "https://next-auth.js.org",
baseUrl: "/",
favicon: "img/favicon.ico",
organizationName: "nextauthjs",
projectName: "next-auth",
themeConfig: {
sidebarCollapsible: true,
prism: {
theme: require('prism-react-renderer/themes/vsDark')
theme: require("prism-react-renderer/themes/vsDark"),
},
algolia: {
apiKey: "b81e3ca39a920b7815e880aea49c00ec",
indexName: "next-auth",
searchParameters: {},
},
navbar: {
title: 'NextAuth.js',
title: "NextAuth.js",
logo: {
alt: 'NextAuth Logo',
src: 'img/logo/logo-xs.png'
alt: "NextAuth Logo",
src: "img/logo/logo-xs.png",
},
items: [
{
to: '/getting-started/introduction',
activeBasePath: 'docs',
label: 'Documentation',
position: 'left'
to: "/getting-started/introduction",
activeBasePath: "docs",
label: "Documentation",
position: "left",
},
{
to: '/tutorials',
activeBasePath: 'docs',
label: 'Tutorials',
position: 'left'
to: "/tutorials",
activeBasePath: "docs",
label: "Tutorials",
position: "left",
},
{
to: '/faq',
activeBasePath: 'docs',
label: 'FAQ',
position: 'left'
to: "/faq",
activeBasePath: "docs",
label: "FAQ",
position: "left",
},
{
href: 'https://www.npmjs.com/package/next-auth',
label: 'npm',
position: 'right'
href: "https://www.npmjs.com/package/next-auth",
label: "npm",
position: "right",
},
{
href: 'https://github.com/nextauthjs/next-auth',
label: 'GitHub',
position: 'right'
}
]
href: "https://github.com/nextauthjs/next-auth",
label: "GitHub",
position: "right",
},
],
},
// announcementBar: {
// id: 'release-candiate-announcement',
@@ -57,45 +62,45 @@ module.exports = {
footer: {
links: [
{
title: 'About NextAuth.js',
title: "About NextAuth.js",
items: [
{
label: 'Introduction',
to: '/getting-started/introduction'
label: "Introduction",
to: "/getting-started/introduction",
},
{
label: 'Contributors',
to: '/contributors'
label: "Contributors",
to: "/contributors",
},
{
label: 'Canary documentation',
to: 'https://next-auth-git-canary.nextauthjs.vercel.app/'
}
]
label: "Canary documentation",
to: "https://next-auth-git-canary.nextauthjs.vercel.app/",
},
],
},
{
title: 'Download',
title: "Download",
items: [
{
label: 'GitHub',
to: 'https://github.com/nextauthjs/next-auth'
label: "GitHub",
to: "https://github.com/nextauthjs/next-auth",
},
{
label: 'NPM',
to: 'https://www.npmjs.com/package/next-auth'
}
]
label: "NPM",
to: "https://www.npmjs.com/package/next-auth",
},
],
},
{
title: 'Acknowledgements',
title: "Acknowledgements",
items: [
{
label: 'Docusaurus',
to: 'https://v2.docusaurus.io/'
label: "Docusaurus",
to: "https://v2.docusaurus.io/",
},
{
label: 'Images by unDraw',
to: 'https://undraw.co/'
label: "Images by unDraw",
to: "https://undraw.co/",
},
{
html: `
@@ -106,28 +111,27 @@ module.exports = {
height="32"
src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg"
/>
</a>`
}
]
}
</a>`,
},
],
},
],
copyright: 'NextAuth.js &copy; Iain Collins 2021'
}
copyright: "NextAuth.js &copy; Iain Collins 2021",
},
},
presets: [
[
'@docusaurus/preset-classic',
"@docusaurus/preset-classic",
{
docs: {
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/nextauthjs/next-auth/edit/main/www'
routeBasePath: "/",
sidebarPath: require.resolve("./sidebars.js"),
editUrl: "https://github.com/nextauthjs/next-auth/edit/main/www",
},
theme: {
customCss: require.resolve('./src/css/index.css')
}
}
]
customCss: require.resolve("./src/css/index.css"),
},
},
],
],
plugins: ['docusaurus-lunr-search']
}

24109
www/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,27 @@
{
"name": "next-auth-docs",
"version": "0.1.1",
"version": "0.2.0",
"scripts": {
"start": "npm run generate-providers && docusaurus start",
"build": "npm run generate-providers && docusaurus build",
"docusaurus": "docusaurus",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"serve": "docusaurus serve",
"clear": "docusaurus clear",
"lint": "standard",
"lint:fix": "standard --fix",
"generate-providers": "node ./scripts/generate-providers.js"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-alpha.70",
"@docusaurus/preset-classic": "^2.0.0-alpha.70",
"classnames": "^2.2.6",
"docusaurus-lunr-search": "^2.1.10",
"jose": "^2.0.2",
"@docusaurus/core": "2.0.0-beta.0",
"@docusaurus/preset-classic": "2.0.0-beta.0",
"classnames": "^2.3.1",
"lodash.times": "^4.3.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-marquee-slider": "^1.1.2",
"styled-components": "^5.2.1"
"styled-components": "^5.2.3"
},
"browserslist": {
"production": [

View File

@@ -1,31 +1,53 @@
const providers = require('./providers.json')
module.exports = {
sidebar: {
'Getting Started': [
'getting-started/introduction',
'getting-started/example',
'getting-started/client',
'getting-started/rest-api',
'getting-started/typescript'
],
Configuration: [
'configuration/options',
'configuration/providers',
'configuration/databases',
'configuration/pages',
'configuration/callbacks',
'configuration/events'
],
'Models & Schemas': [
'schemas/models',
'schemas/mysql',
'schemas/postgres',
'schemas/mssql',
'schemas/mongodb',
'schemas/adapters'
],
'Authentication Providers': Object.entries(providers)
.sort(([, a], [, b]) => a.localeCompare(b))
.map(([provider]) => `providers/${provider}`)
}
docs: [
{
type: "category",
label: "Getting Started",
collapsed: false,
items: [
"getting-started/introduction",
"getting-started/example",
"getting-started/client",
"getting-started/rest-api",
"getting-started/typescript",
],
},
{
type: "category",
label: "Configuration",
collapsed: true,
items: [
"configuration/options",
"configuration/providers",
"configuration/databases",
"configuration/pages",
"configuration/callbacks",
"configuration/events",
],
},
{
type: "category",
label: "Models & Schemas",
collapsed: true,
items: [
"schemas/models",
"schemas/mysql",
"schemas/postgres",
"schemas/mssql",
"schemas/mongodb",
"schemas/adapters",
],
},
{
type: "category",
label: "Authentication Providers",
collapsed: true,
items: [
{
type: "autogenerated",
dirName: "providers",
},
],
},
],
}

View File

@@ -34,9 +34,9 @@
html[data-theme="dark"]:root {
--ifm-color-link: #289ef9;
--ifm-footer-background-color: #111;
--ifm-footer-background-color: #000;
--ifm-html-background-color: #242526;
--ifm-background-color: #000000;
--ifm-background-color: #090909;
--ifm-hero-background-color: #111111;
--ifm-navbar-background-color: rgba(0, 0, 0, 0.95);
}
@@ -47,6 +47,7 @@ html[data-theme="dark"]:root {
@import "table-of-contents.css";
@import "sidebar.css";
@import "providers.css";
@import "search.css";
@media screen and (max-width: 360px) {
html {
@@ -183,9 +184,26 @@ html[data-theme="dark"] hr {
border-color: #242526;
}
.github-counter {
position: absolute;
color: #000;
top: -10px;
right: 5px;
font-size: 9px;
background-color: #ccc;
padding: 2px 5px;
border-radius: 10px;
z-index: -1;
}
html[data-theme="dark"] .github-counter {
background-color: #222;
color: #fff;
}
.navbar__item.navbar__link[href*="github"],
.navbar__item.navbar__link[href*="npmjs"] {
padding: 0 1rem 0 0;
padding: 0 1.5rem 0 0;
display: flex;
font-size: 0;
}
@@ -198,6 +216,26 @@ html[data-theme="dark"] hr {
background-repeat: no-repeat;
}
.navbar__items .react-toggle {
margin-right: 5px;
}
.react-toggle--focus .react-toggle-thumb,
.react-toggle:hover .react-toggle-thumb {
box-shadow: none !important;
}
.navbar__search-input:focus {
outline: none;
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
rgba(19, 19, 19, 0.2) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
transition: box-shadow 350ms ease-in-out;
}
html[data-theme="dark"] .navbar__search-input:focus {
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
rgba(29, 29, 29, 0.5) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
}
html[data-theme="dark"] .navbar__item.navbar__link[href*="github"]:before {
background-image: url("/img/brand-github-inverted.svg");
}

28
www/src/css/search.css Normal file
View File

@@ -0,0 +1,28 @@
html[data-theme="light"]:root {
--docsearch-searchbox-shadow: inset 0 0 0 2px #a553b3;
}
html[data-theme="light"] .DocSearch-Modal .DocSearch-Search-Icon {
color: #a553b3;
}
html[data-theme="dark"] .DocSearch-Modal .DocSearch-Search-Icon {
color: #7c2f89;
}
html[data-theme="dark"]:root {
--docsearch-searchbox-background: #040404;
--docsearch-key-gradient: #000;
--docsearch-key-shadow: #ccc;
--docsearch-searchbox-shadow: inset 0 0 0 2px #7c2f89;
}
html[data-theme="dark"] .DocSearch-Button-Key {
--docsearch-muted-color: #333;
}
@media screen and (max-width: 740px) {
.DocSearch-Container {
margin-top: 60px;
}
}

View File

@@ -1,47 +1,50 @@
import React from 'react'
import classnames from 'classnames'
import Layout from '@theme/Layout'
import Link from '@docusaurus/Link'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import useBaseUrl from '@docusaurus/useBaseUrl'
import CodeBlock from '@theme/CodeBlock'
import ProviderMarquee from '../components/ProviderMarquee'
import Seo from './seo'
import styles from './index.module.css'
import React, { useEffect } from "react"
import classnames from "classnames"
import Layout from "@theme/Layout"
import Link from "@docusaurus/Link"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import useBaseUrl from "@docusaurus/useBaseUrl"
import CodeBlock from "@theme/CodeBlock"
import ProviderMarquee from "../components/ProviderMarquee"
import Seo from "./seo"
import styles from "./index.module.css"
const features = [
{
title: 'Easy',
imageUrl: 'img/undraw_social.svg',
title: "Easy",
imageUrl: "img/undraw_social.svg",
description: (
<ul>
<li>Built in support for popular services<br />
<li>
Built in support for popular services
<br />
<em>(Google, Facebook, Auth0, Apple)</em>
</li>
<li>Built in email / passwordless / magic link</li>
<li>Use with any username / password store</li>
<li>Use with OAuth 1.0 &amp; 2.0 services</li>
</ul>
)
),
},
{
title: 'Flexible',
imageUrl: 'img/undraw_authentication.svg',
title: "Flexible",
imageUrl: "img/undraw_authentication.svg",
description: (
<ul>
<li>Built for Serverless, runs anywhere</li>
<li>
Bring Your Own Database - or none!<br />
Bring Your Own Database - or none!
<br />
<em>(MySQL, Postgres, MSSQL, MongoDB)</em>
</li>
<li>Choose database sessions or JWT</li>
<li>Secure web pages and API routes</li>
</ul>
)
),
},
{
title: 'Secure',
imageUrl: 'img/undraw_secure.svg',
title: "Secure",
imageUrl: "img/undraw_secure.svg",
description: (
<ul>
<li>Signed, prefixed, server-only cookies</li>
@@ -50,84 +53,105 @@ const features = [
<li>Tab syncing, auto-revalidation, keepalives</li>
<li>Doesn't rely on client side JavaScript</li>
</ul>
)
}
),
},
]
function Feature ({ imageUrl, title, description }) {
const kFormatter = (num) => {
return Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k"
}
function Feature({ imageUrl, title, description }) {
const imgUrl = useBaseUrl(imageUrl)
return (
<div className={classnames('col col--4', styles.feature)}>
<div className={classnames("col col--4", styles.feature)}>
{imgUrl && (
<div className='text--center'>
<div className='feature-image-wrapper'>
<div className="text--center">
<div className="feature-image-wrapper">
<img className={styles.featureImage} src={imgUrl} alt={title} />
</div>
</div>
)}
<h3 className='text--center'>{title}</h3>
<h3 className="text--center">{title}</h3>
<p>{description}</p>
</div>
)
}
function Home () {
function Home() {
const context = useDocusaurusContext()
const { siteConfig = {} } = context
useEffect(() => {
fetch("https://api.github.com/repos/nextauthjs/next-auth")
.then((res) => res.json())
.then((data) => {
const navLinks = document.getElementsByClassName(
"navbar__item navbar__link"
)
const githubStat = document.createElement("span")
githubStat.innerHTML = kFormatter(data.stargazers_count)
githubStat.className = "github-counter"
navLinks[4].appendChild(githubStat)
})
}, [])
return (
<Layout description={siteConfig.tagline}>
<Seo />
<div className='home-wrapper'>
<header className={classnames('hero', styles.heroBanner)}>
<div className='container'>
<div className='hero-inner'>
<div className="home-wrapper">
<header className={classnames("hero", styles.heroBanner)}>
<div className="container">
<div className="hero-inner">
<img
src='/img/logo/logo-sm.png'
alt='Shield with key icon'
src="/img/logo/logo-sm.png"
alt="Shield with key icon"
className={styles.heroLogo}
/>
<div className={styles.heroText}>
<h1 className='hero__title'>{siteConfig.title}</h1>
<p className='hero__subtitle'>{siteConfig.tagline}</p>
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
</div>
<div className={styles.buttons}>
<a
className={classnames(
'button button--outline button--secondary button--lg rounded-pill',
"button button--outline button--secondary button--lg rounded-pill",
styles.button
)}
href='https://next-auth-example.now.sh'
>Live Demo
href="https://next-auth-example.now.sh"
>
Live Demo
</a>
<Link
className={classnames(
'button button--primary button--lg rounded-pill',
"button button--primary button--lg rounded-pill",
styles.button
)}
to={useBaseUrl('/getting-started/example')}
>Get Started
to={useBaseUrl("/getting-started/example")}
>
Get Started
</Link>
</div>
</div>
<div className='hero-marquee'>
<div className="hero-marquee">
<ProviderMarquee />
</div>
</div>
<div className='hero-wave'>
<div className='hero-wave-inner' />
<div className="hero-wave">
<div className="hero-wave-inner" />
</div>
</header>
<main className='home-main'>
<main className="home-main">
<section className={`section-features ${styles.features}`}>
<div className='container'>
<div className='row'>
<div className='col'>
<div className="container">
<div className="row">
<div className="col">
<h2 className={styles.featuresTitle}>
<span>Open Source.</span> <span>Full Stack.</span> <span>Own Your Data.</span>
<span>Open Source.</span> <span>Full Stack.</span>{" "}
<span>Own Your Data.</span>
</h2>
</div>
</div>
<div className='row'>
<div className="row">
{features.map((props, idx) => (
<Feature key={idx} {...props} />
))}
@@ -135,53 +159,63 @@ function Home () {
</div>
</section>
<section>
<div className='container'>
<div className='row'>
<div className='col'>
<p className='text--center'>
<div className="container">
<div className="row">
<div className="col">
<p className="text--center">
<a
href='https://www.npmjs.com/package/next-auth'
className='button button--primary button--outline rounded-pill button--lg'
>npm install next-auth
href="https://www.npmjs.com/package/next-auth"
className="button button--primary button--outline rounded-pill button--lg"
>
npm install next-auth
</a>
</p>
</div>
</div>
<div className='row'>
<div className='col'>
<h2 className='text--center' style={{ fontSize: '2.5rem' }}>
<div className="row">
<div className="col">
<h2 className="text--center" style={{ fontSize: "2.5rem" }}>
Add authentication in minutes!
</h2>
</div>
</div>
<div className='row'>
<div className='col col--6'>
<div className='code'>
<h4 className='code-heading'>Server <span>/pages/api/auth/[...nextauth].js</span></h4>
<CodeBlock className='javascript'>{serverlessFunctionCode}</CodeBlock>
<div className="row">
<div className="col col--6">
<div className="code">
<h4 className="code-heading">
Server <span>/pages/api/auth/[...nextauth].js</span>
</h4>
<CodeBlock className="javascript">
{serverlessFunctionCode}
</CodeBlock>
</div>
</div>
<div className='col col--6'>
<div className='code'>
<h4 className='code-heading'>Client <span>/pages/index.js</span></h4>
<CodeBlock className='javascript'>{reactComponentCode}</CodeBlock>
<div className="col col--6">
<div className="code">
<h4 className="code-heading">
Client <span>/pages/index.js</span>
</h4>
<CodeBlock className="javascript">
{reactComponentCode}
</CodeBlock>
</div>
</div>
</div>
<div className='row'>
<div className='col'>
<p className='text--center' style={{ marginTop: '2rem' }}>
<div className="row">
<div className="col">
<p className="text--center" style={{ marginTop: "2rem" }}>
<Link
to='/getting-started/example'
className='button button--primary button--lg rounded-pill'
>Example Code
to="/getting-started/example"
className="button button--primary button--lg rounded-pill"
>
Example Code
</Link>
</p>
</div>
</div>
</div>
</section>
<div className='home-subtitle'>
<div className="home-subtitle">
<p>NextAuth.js is an open source community project.</p>
</div>
</main>

View File

@@ -17,13 +17,13 @@
}
.heroLogo {
margin-bottom: .5rem;
margin-bottom: 0.5rem;
width: 8rem;
}
@media screen and (min-width: 689px) {
.heroLogo {
margin-bottom: -.5rem;
margin-bottom: -0.5rem;
}
}
@@ -87,8 +87,8 @@
}
.features ul li {
margin-top: .5rem;
margin-bottom: .5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-size: 1rem;
white-space: nowrap;
text-align: center;
@@ -102,4 +102,4 @@
.featureImage {
height: 220px;
width: 220px;
}
}

View File

@@ -1,28 +1,24 @@
import React from 'react'
import Head from '@docusaurus/Head'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import React from "react"
import Head from "@docusaurus/Head"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
const Seo = () => {
const context = useDocusaurusContext()
const { siteConfig = {} } = context
const {
title,
tagline,
url
} = siteConfig
const { title, tagline, url } = siteConfig
return (
<Head>
<meta charSet='utf-8' />
<link rel='canonical' href={url} />
<meta property='og:title' content={title} />
<meta property='og:description' content={tagline} />
<meta property='og:image' content={`${url}/img/social-media-card.png`} />
<meta property='og:url' content={url} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={tagline} />
<meta name='twitter:image' content={`${url}/img/social-media-card.png`} />
<meta charSet="utf-8" />
<link rel="canonical" href={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={tagline} />
<meta property="og:image" content={`${url}/img/social-media-card.png`} />
<meta property="og:url" content={url} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={tagline} />
<meta name="twitter:image" content={`${url}/img/social-media-card.png`} />
</Head>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,108 +0,0 @@
/* eslint-disable */
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useRef, useCallback } from "react";
import classnames from "classnames";
import { useHistory } from "@docusaurus/router";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
let loaded = false;
const Search = (props) => {
const initialized = useRef(false);
const searchBarRef = useRef(null);
const history = useHistory();
const { siteConfig = {} } = useDocusaurusContext();
const { baseUrl } = siteConfig;
const initAlgolia = () => {
if (!initialized.current) {
new window.DocSearch({
searchData: window.searchData,
inputSelector: "#search_input_react",
// Override algolia's default selection event, allowing us to do client-side
// navigation and avoiding a full page refresh.
handleSelected: (_input, _event, suggestion) => {
const url = baseUrl + suggestion.url;
// Use an anchor tag to parse the absolute url into a relative url
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
const a = document.createElement("a");
a.href = url;
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
history.push(url);
},
});
initialized.current = true;
}
};
const getSearchData = () =>
process.env.NODE_ENV === "production"
? fetch(`${baseUrl}search-doc.json`).then((content) => content.json())
: Promise.resolve([]);
const loadAlgolia = () => {
if (!loaded) {
Promise.all([
getSearchData(),
import("./lib/DocSearch"),
import("./algolia.css"),
]).then(([searchData, { default: DocSearch }]) => {
loaded = true;
window.searchData = searchData;
window.DocSearch = DocSearch;
initAlgolia();
});
} else {
initAlgolia();
}
};
const toggleSearchIconClick = useCallback(
(e) => {
if (!searchBarRef.current.contains(e.target)) {
searchBarRef.current.focus();
}
props.handleSearchBarToggle(!props.isSearchBarExpanded);
},
[props.isSearchBarExpanded]
);
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames("search-icon", {
"search-icon-hidden": props.isSearchBarExpanded,
})}
onClick={toggleSearchIconClick}
onKeyDown={toggleSearchIconClick}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
"navbar__search-input",
{ "search-bar-expanded": props.isSearchBarExpanded },
{ "search-bar": !props.isSearchBarExpanded }
)}
onClick={loadAlgolia}
onMouseOver={loadAlgolia}
onFocus={toggleSearchIconClick}
onBlur={toggleSearchIconClick}
ref={searchBarRef}
/>
</div>
);
};
export default Search;

View File

@@ -1,340 +0,0 @@
import Hogan from 'hogan.js'
import LunrSearchAdapter from './lunar-search'
import autocomplete from 'autocomplete.js'
import templates from './templates'
import utils from './utils'
import $ from './zepto'
/**
* Adds an autocomplete dropdown to an input field
* @function DocSearch
* @param {Object} options.searchData Read-only API key
* @param {string} options.inputSelector CSS selector that targets the input
* value.
* @param {Object} [options.autocompleteOptions] Options to pass to the underlying autocomplete instance
* @return {Object}
*/
const usage = `Usage:
documentationSearch({
searchData,
inputSelector,
[ appId ],
[ autocompleteOptions.{hint,debug} ]
})`
class DocSearch {
constructor ({
searchData,
inputSelector,
debug = false,
queryDataCallback = null,
autocompleteOptions = {
debug: false,
hint: false,
autoselect: true
},
transformData = false,
queryHook = false,
handleSelected = false,
enhancedSearchInput = false,
layout = 'collumns'
}) {
DocSearch.checkArguments({
searchData,
inputSelector
})
this.searchData = searchData
this.input = DocSearch.getInputFromSelector(inputSelector)
this.queryDataCallback = queryDataCallback || null
const autocompleteOptionsDebug =
autocompleteOptions && autocompleteOptions.debug
? autocompleteOptions.debug
: false
// eslint-disable-next-line no-param-reassign
autocompleteOptions.debug = debug || autocompleteOptionsDebug
this.autocompleteOptions = autocompleteOptions
this.autocompleteOptions.cssClasses =
this.autocompleteOptions.cssClasses || {}
this.autocompleteOptions.cssClasses.prefix =
this.autocompleteOptions.cssClasses.prefix || 'ds'
const inputAriaLabel =
this.input &&
typeof this.input.attr === 'function' &&
this.input.attr('aria-label')
this.autocompleteOptions.ariaLabel =
this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input'
this.isSimpleLayout = layout === 'simple'
this.client = new LunrSearchAdapter(this.searchData)
if (enhancedSearchInput) {
this.input = DocSearch.injectSearchBox(this.input)
}
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
{
source: this.getAutocompleteSource(transformData, queryHook),
templates: {
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
footer: templates.footer,
empty: DocSearch.getEmptyTemplate()
}
}
])
const customHandleSelected = handleSelected
this.handleSelected = customHandleSelected || this.handleSelected
// We prevent default link clicking if a custom handleSelected is defined
if (customHandleSelected) {
$('.algolia-autocomplete').on('click', '.ds-suggestions a', event => {
event.preventDefault()
})
}
this.autocomplete.on(
'autocomplete:selected',
this.handleSelected.bind(null, this.autocomplete.autocomplete)
)
this.autocomplete.on(
'autocomplete:shown',
this.handleShown.bind(null, this.input)
)
if (enhancedSearchInput) {
DocSearch.bindSearchBoxEvent()
}
}
/**
* Checks that the passed arguments are valid. Will throw errors otherwise
* @function checkArguments
* @param {object} args Arguments as an option object
* @returns {void}
*/
static checkArguments (args) {
if (!args.searchData) {
throw new Error(usage)
}
if (typeof args.inputSelector !== 'string') {
throw new Error(
`Error: inputSelector:${args.inputSelector} must be a string. Each selector must match only one element and separated by ','`
)
}
if (!DocSearch.getInputFromSelector(args.inputSelector)) {
throw new Error(
`Error: No input element in the page matches ${args.inputSelector}`
)
}
}
static injectSearchBox (input) {
input.before(templates.searchBox)
const newInput = input
.prev()
.prev()
.find('input')
input.remove()
return newInput
}
static bindSearchBoxEvent () {
$('.searchbox [type="reset"]').on('click', function () {
$('input#docsearch').focus()
$(this).addClass('hide')
autocomplete.autocomplete.setVal('')
})
$('input#docsearch').on('keyup', () => {
const searchbox = document.querySelector('input#docsearch')
const reset = document.querySelector('.searchbox [type="reset"]')
reset.className = 'searchbox__reset'
if (searchbox.value.length === 0) {
reset.className += ' hide'
}
})
}
/**
* Returns the matching input from a CSS selector, null if none matches
* @function getInputFromSelector
* @param {string} selector CSS selector that matches the search
* input of the page
* @returns {void}
*/
static getInputFromSelector (selector) {
const input = $(selector).filter('input')
return input.length ? $(input[0]) : null
}
/**
* Returns the `source` method to be passed to autocomplete.js. It will query
* the Algolia index and call the callbacks with the formatted hits.
* @function getAutocompleteSource
* @param {function} transformData An optional function to transform the hits
* @param {function} queryHook An optional function to transform the query
* @returns {function} Method to be passed as the `source` option of
* autocomplete
*/
getAutocompleteSource (transformData, queryHook) {
return (query, callback) => {
if (queryHook) {
// eslint-disable-next-line no-param-reassign
query = queryHook(query) || query
}
this.client.search(query).then(hits => {
if (
this.queryDataCallback &&
typeof this.queryDataCallback === 'function'
) {
this.queryDataCallback(hits)
}
if (transformData) {
hits = transformData(hits) || hits
}
callback(DocSearch.formatHits(hits))
})
}
}
// Given a list of hits returned by the API, will reformat them to be used in
// a Hogan template
static formatHits (receivedHits) {
const clonedHits = utils.deepClone(receivedHits)
const hits = clonedHits.map(hit => {
if (hit._highlightResult) {
// eslint-disable-next-line no-param-reassign
hit._highlightResult = utils.mergeKeyWithParent(
hit._highlightResult,
'hierarchy'
)
}
return utils.mergeKeyWithParent(hit, 'hierarchy')
})
// Group hits by category / subcategory
let groupedHits = utils.groupBy(hits, 'lvl0')
$.each(groupedHits, (level, collection) => {
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1')
const flattenedHits = utils.flattenAndFlagFirst(
groupedHitsByLvl1,
'isSubCategoryHeader'
)
groupedHits[level] = flattenedHits
})
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader')
// Translate hits into smaller objects to be send to the template
return groupedHits.map(hit => {
const url = DocSearch.formatURL(hit)
const category = utils.getHighlightedValue(hit, 'lvl0')
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category
const displayTitle = utils
.compact([
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
utils.getHighlightedValue(hit, 'lvl3'),
utils.getHighlightedValue(hit, 'lvl4'),
utils.getHighlightedValue(hit, 'lvl5'),
utils.getHighlightedValue(hit, 'lvl6')
])
.join(
'<span class="aa-suggestion-title-separator" aria-hidden="true"> </span>'
)
const text = utils.getSnippetedValue(hit, 'content')
const isTextOrSubcategoryNonEmpty =
(subcategory && subcategory !== '') ||
(displayTitle && displayTitle !== '')
const isLvl1EmptyOrDuplicate =
!subcategory || subcategory === '' || subcategory === category
const isLvl2 =
displayTitle && displayTitle !== '' && displayTitle !== subcategory
const isLvl1 =
!isLvl2 &&
(subcategory && subcategory !== '' && subcategory !== category)
const isLvl0 = !isLvl1 && !isLvl2
return {
isLvl0,
isLvl1,
isLvl2,
isLvl1EmptyOrDuplicate,
isCategoryHeader: hit.isCategoryHeader,
isSubCategoryHeader: hit.isSubCategoryHeader,
isTextOrSubcategoryNonEmpty,
category,
subcategory,
title: displayTitle,
text,
url
}
})
}
static formatURL (hit) {
const { url, anchor } = hit
if (url) {
const containsAnchor = url.indexOf('#') !== -1
if (containsAnchor) return url
else if (anchor) return `${hit.url}#${hit.anchor}`
return url
} else if (anchor) return `#${hit.anchor}`
/* eslint-disable */
console.warn("no anchor nor url for : ", JSON.stringify(hit));
/* eslint-enable */
return null
}
static getEmptyTemplate () {
return args => Hogan.compile(templates.empty).render(args)
}
static getSuggestionTemplate (isSimpleLayout) {
const stringTemplate = isSimpleLayout
? templates.suggestionSimple
: templates.suggestion
const template = Hogan.compile(stringTemplate)
return suggestion => template.render(suggestion)
}
handleSelected (input, event, suggestion, datasetNumber, context = {}) {
// Do nothing if click on the suggestion, as it's already a <a href>, the
// browser will take care of it. This allow Ctrl-Clicking on results and not
// having the main window being redirected as well
if (context.selectionMethod === 'click') {
return
}
input.setVal('')
window.location.assign(suggestion.url)
}
handleShown (input) {
const middleOfInput = input.offset().left + input.width() / 2
let middleOfWindow = $(document).width() / 2
if (isNaN(middleOfWindow)) {
middleOfWindow = 900
}
const alignClass =
middleOfInput - middleOfWindow >= 0
? 'algolia-autocomplete-right'
: 'algolia-autocomplete-left'
const otherAlignClass =
middleOfInput - middleOfWindow < 0
? 'algolia-autocomplete-right'
: 'algolia-autocomplete-left'
const autocompleteWrapper = $('.algolia-autocomplete')
if (!autocompleteWrapper.hasClass(alignClass)) {
autocompleteWrapper.addClass(alignClass)
}
if (autocompleteWrapper.hasClass(otherAlignClass)) {
autocompleteWrapper.removeClass(otherAlignClass)
}
}
}
export default DocSearch

View File

@@ -1,169 +0,0 @@
/* eslint-disable */
import lunr from 'lunr'
lunr.tokenizer.separator = /[\s\-/]+/
class LunrSearchAdapter {
constructor (searchData) {
this.searchData = searchData
this.init()
this.titleHitsRes = []
}
init () {
const { searchData } = this
this.lunrIndex = lunr(function () {
this.ref('id')
this.field('title', { boost: 200 })
this.field('content', { boost: 2 })
this.field('keywords', { boost: 100 })
this.metadataWhitelist = ['position']
searchData.forEach((d, i) => {
const doc = {
id: i,
title: d.title,
content: d.content,
keywords: d.keywords
}
this.add(doc)
})
})
}
getLunrResult (input) {
return this.lunrIndex.query(function (query) {
const tokens = lunr.tokenizer(input)
query.term(tokens, {
boost: 10
})
query.term(tokens, {
wildcard: lunr.Query.wildcard.TRAILING
})
})
}
getHit (doc, formattedTitle, formattedContent) {
return {
hierarchy: {
lvl0: doc.pageTitle || doc.title,
lvl1: doc.type === 0 ? null : doc.title
},
url: doc.url,
_snippetResult: formattedContent ? {
content: {
value: formattedContent,
matchLevel: 'full'
}
} : null,
_highlightResult: {
hierarchy: {
lvl0: {
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle
},
lvl1:
doc.type === 0
? null
: {
value: formattedTitle || doc.title
}
}
}
}
}
getTitleHit (doc, position, length) {
const start = position[0]
const end = position[0] + length
const formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length)
return this.getHit(doc, formattedTitle)
}
getKeywordHit (doc, position, length) {
const start = position[0]
const end = position[0] + length
const formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
return this.getHit(doc, formattedTitle)
}
getContentHit (doc, position) {
const start = position[0]
const end = position[0] + position[1]
let previewStart = start
let previewEnd = end
let ellipsesBefore = true
let ellipsesAfter = true
for (let k = 0; k < 3; k++) {
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2)
const nextDot = doc.content.lastIndexOf('.', previewStart - 2)
if ((nextDot > 0) && (nextDot > nextSpace)) {
previewStart = nextDot + 1
ellipsesBefore = false
break
}
if (nextSpace < 0) {
previewStart = 0
ellipsesBefore = false
break
}
previewStart = nextSpace + 1
}
for (let k = 0; k < 10; k++) {
const nextSpace = doc.content.indexOf(' ', previewEnd + 1)
const nextDot = doc.content.indexOf('.', previewEnd + 1)
if ((nextDot > 0) && (nextDot < nextSpace)) {
previewEnd = nextDot
ellipsesAfter = false
break
}
if (nextSpace < 0) {
previewEnd = doc.content.length
ellipsesAfter = false
break
}
previewEnd = nextSpace
}
let preview = doc.content.substring(previewStart, start)
if (ellipsesBefore) {
preview = '... ' + preview
}
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>'
preview += doc.content.substring(end, previewEnd)
if (ellipsesAfter) {
preview += ' ...'
}
return this.getHit(doc, null, preview)
}
search (input) {
return new Promise((resolve, rej) => {
const results = this.getLunrResult(input)
const hits = []
results.length > 5 && (results.length = 5)
this.titleHitsRes = []
this.contentHitsRes = []
results.forEach(result => {
const doc = this.searchData[result.ref]
const { metadata } = result.matchData
for (const i in metadata) {
if (metadata[i].title) {
if (!this.titleHitsRes.includes(result.ref)) {
const position = metadata[i].title.position[0]
hits.push(this.getTitleHit(doc, position, input.length))
this.titleHitsRes.push(result.ref)
}
} else if (metadata[i].content) {
const position = metadata[i].content.position[0]
hits.push(this.getContentHit(doc, position))
} else if (metadata[i].keywords) {
const position = metadata[i].keywords.position[0]
hits.push(this.getKeywordHit(doc, position, input.length))
this.titleHitsRes.push(result.ref)
}
}
})
hits.length > 5 && (hits.length = 5)
resolve(hits)
})
}
}
export default LunrSearchAdapter

View File

@@ -1,114 +0,0 @@
const prefix = 'algolia-docsearch'
const suggestionPrefix = `${prefix}-suggestion`
const footerPrefix = `${prefix}-footer`
/* eslint-disable max-len */
const templates = {
suggestion: `
<a class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
"
aria-label="Link to the result"
href="{{{url}}}"
>
<div class="${suggestionPrefix}--category-header">
<span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
</div>
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--subcategory-column">
<span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
</div>
{{#isTextOrSubcategoryNonEmpty}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
<div class="${suggestionPrefix}--title">{{{title}}}</div>
{{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
</div>
{{/isTextOrSubcategoryNonEmpty}}
</div>
</a>
`,
suggestionSimple: `
<div class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
suggestion-layout-simple
">
<div class="${suggestionPrefix}--category-header">
{{^isLvl0}}
<span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
{{^isLvl1}}
{{^isLvl1EmptyOrDuplicate}}
<span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
{{{subcategory}}}
</span>
{{/isLvl1EmptyOrDuplicate}}
{{/isLvl1}}
{{/isLvl0}}
<div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
{{#isLvl2}}
{{{title}}}
{{/isLvl2}}
{{#isLvl1}}
{{{subcategory}}}
{{/isLvl1}}
{{#isLvl0}}
{{{category}}}
{{/isLvl0}}
</div>
</div>
<div class="${suggestionPrefix}--wrapper">
{{#text}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--text">{{{text}}}</div>
</div>
{{/text}}
</div>
</div>
`,
footer: `
<div class="${footerPrefix}">
</div>
`,
empty: `
<div class="${suggestionPrefix}">
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
<div class="${suggestionPrefix}--title">
<div class="${suggestionPrefix}--text">
No results found for query <b>"{{query}}"</b>
</div>
</div>
</div>
</div>
</div>
`,
searchBox: `
<form novalidate="novalidate" onsubmit="return false;" class="searchbox">
<div role="search" class="searchbox__wrapper">
<input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
<button type="submit" title="Submit your search query." class="searchbox__submit" >
<svg width=12 height=12 role="img" aria-label="Search">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
</svg>
</button>
<button type="reset" title="Clear the search query." class="searchbox__reset hide">
<svg width=12 height=12 role="img" aria-label="Reset">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
</svg>
</button>
</div>
</form>
<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
<symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
</svg>
</div>
`
}
export default templates

View File

@@ -1,270 +0,0 @@
import $ from './zepto'
const utils = {
/*
* Move the content of an object key one level higher.
* eg.
* {
* name: 'My name',
* hierarchy: {
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* }
* Will be converted to
* {
* name: 'My name',
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* @param {Object} object Main object
* @param {String} property Main object key to move up
* @return {Object}
* @throws Error when key is not an attribute of Object or is not an object itself
*/
mergeKeyWithParent (object, property) {
if (object[property] === undefined) {
return object
}
if (typeof object[property] !== 'object') {
return object
}
const newObject = $.extend({}, object, object[property])
delete newObject[property]
return newObject
},
/*
* Group all objects of a collection by the value of the specified attribute
* If the attribute is a string, use the lowercase form.
*
* eg.
* groupBy([
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexS', category: 'dev'},
* {name: 'AlexK', category: 'sales'}
* ], 'category');
* =>
* {
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* }
* @param {array} collection Array of objects to group
* @param {String} property The attribute on which apply the grouping
* @return {array}
* @throws Error when one of the element does not have the specified property
*/
groupBy (collection, property) {
const newCollection = {}
$.each(collection, (index, item) => {
if (item[property] === undefined) {
throw new Error(`[groupBy]: Object has no key ${property}`)
}
let key = item[property]
if (typeof key === 'string') {
key = key.toLowerCase()
}
// fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
// such as the constructor, so we need to do this check.
if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
newCollection[key] = []
}
newCollection[key].push(item)
})
return newCollection
},
/*
* Return an array of all the values of the specified object
* eg.
* values({
* foo: 42,
* bar: true,
* baz: 'yep'
* })
* =>
* [42, true, yep]
* @param {object} object Object to extract values from
* @return {array}
*/
values (object) {
return Object.keys(object).map(key => object[key])
},
/*
* Flattens an array
* eg.
* flatten([1, 2, [3, 4], [5, 6]])
* =>
* [1, 2, 3, 4, 5, 6]
* @param {array} array Array to flatten
* @return {array}
*/
flatten (array) {
const results = []
array.forEach(value => {
if (!Array.isArray(value)) {
results.push(value)
return
}
value.forEach(subvalue => {
results.push(subvalue)
})
})
return results
},
/*
* Flatten all values of an object into an array, marking each first element of
* each group with a specific flag
* eg.
* flattenAndFlagFirst({
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* , 'isTop');
* =>
* [
* {name: 'Tim', category: 'dev', isTop: true},
* {name: 'Vincent', category: 'dev', isTop: false},
* {name: 'AlexS', category: 'dev', isTop: false},
* {name: 'Ben', category: 'sales', isTop: true},
* {name: 'Jeremy', category: 'sales', isTop: false},
* {name: 'AlexK', category: 'sales', isTop: false}
* ]
* @param {object} object Object to flatten
* @param {string} flag Flag to set to true on first element of each group
* @return {array}
*/
flattenAndFlagFirst (object, flag) {
const values = this.values(object).map(collection =>
collection.map((item, index) => {
// eslint-disable-next-line no-param-reassign
item[flag] = index === 0
return item
})
)
return this.flatten(values)
},
/*
* Removes all empty strings, null, false and undefined elements array
* eg.
* compact([42, false, null, undefined, '', [], 'foo']);
* =>
* [42, [], 'foo']
* @param {array} array Array to compact
* @return {array}
*/
compact (array) {
const results = []
array.forEach(value => {
if (!value) {
return
}
results.push(value)
})
return results
},
/*
* Returns the highlighted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly
* eg.
* getHighlightedValue({
* _highlightResult: {
* text: {
* value: '<mark>foo</mark>'
* }
* },
* text: 'foo'
* }, 'text');
* =>
* '<mark>foo</mark>'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getHighlightedValue (object, property) {
if (
object._highlightResult &&
object._highlightResult.hierarchy_camel &&
object._highlightResult.hierarchy_camel[property] &&
object._highlightResult.hierarchy_camel[property].matchLevel &&
object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
object._highlightResult.hierarchy_camel[property].value
) {
return object._highlightResult.hierarchy_camel[property].value
}
if (
object._highlightResult &&
object._highlightResult &&
object._highlightResult[property] &&
object._highlightResult[property].value
) {
return object._highlightResult[property].value
}
return object[property]
},
/*
* Returns the snippeted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly.
* Will add starting and ending ellipsis (…) if we detect that a sentence is
* incomplete
* eg.
* getSnippetedValue({
* _snippetResult: {
* text: {
* value: '<mark>This is an unfinished sentence</mark>'
* }
* },
* text: 'This is an unfinished sentence'
* }, 'text');
* =>
* '<mark>This is an unfinished sentence</mark>…'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getSnippetedValue (object, property) {
if (
!object._snippetResult ||
!object._snippetResult[property] ||
!object._snippetResult[property].value
) {
return object[property]
}
let snippet = object._snippetResult[property].value
if (snippet[0] !== snippet[0].toUpperCase()) {
snippet = `${snippet}`
}
if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
snippet = `${snippet}`
}
return snippet
},
/*
* Deep clone an object.
* Note: This will not clone functions and dates
* @param {object} object Object to clone
* @return {object}
*/
deepClone (object) {
return JSON.parse(JSON.stringify(object))
}
}
export default utils

View File

@@ -1,2 +0,0 @@
import zepto from 'autocomplete.js/zepto'
export default zepto

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.search-icon {
background-image: var(--ifm-navbar-search-input-icon);
height: auto;
width: 24px;
cursor: pointer;
padding: 8px;
line-height: 32px;
background-repeat: no-repeat;
background-position: center;
display: none;
}
.search-icon-hidden {
visibility: hidden;
}
@media (max-width: 360px) {
.search-bar {
width: 0 !important;
background: none !important;
padding: 0 !important;
transition: none !important;
}
.search-bar-expanded {
width: 9rem !important;
}
.search-icon {
display: inline;
vertical-align: sub;
}
}