@hanzo/ui 2.0.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/lux-site-icons/android-chrome-192x192.png +0 -0
- package/assets/lux-site-icons/android-chrome-512x512.png +0 -0
- package/assets/lux-site-icons/apple-touch-icon.png +0 -0
- package/assets/lux-site-icons/favicon-16x16.png +0 -0
- package/assets/lux-site-icons/favicon-32x32.png +0 -0
- package/assets/lux-site-icons/favicon.ico +0 -0
- package/assets/standard-docs/LUX-NFT-Terms-and-Conditions.pdf +0 -0
- package/assets/standard-docs/LUX-Privacy-Policy.pdf +0 -0
- package/blocks/components/card-block.tsx +1 -1
- package/blocks/components/screenful-block/index.tsx +11 -1
- package/common/chat-widget.tsx +75 -0
- package/common/contact-dialog/contact-form.tsx +111 -0
- package/common/contact-dialog/disclaimer.tsx +13 -0
- package/common/contact-dialog/index.tsx +48 -0
- package/common/copyright.tsx +21 -0
- package/common/drawer-menu.tsx +54 -0
- package/common/footer.tsx +77 -0
- package/common/head-metadata/from-next/metadata-types.ts +158 -0
- package/common/head-metadata/from-next/opengraph-types.ts +267 -0
- package/common/head-metadata/from-next/twitter-types.ts +92 -0
- package/common/head-metadata/index.tsx +208 -0
- package/common/header/index.tsx +66 -0
- package/common/header/mobile-nav.tsx +72 -0
- package/common/header/theme-toggle.tsx +26 -0
- package/common/icons/index.tsx +34 -0
- package/common/icons/lux-logo.tsx +10 -0
- package/common/icons/secure-delivery.tsx +13 -0
- package/common/icons/social-icon.tsx +35 -0
- package/common/index.ts +14 -0
- package/common/logo.tsx +71 -0
- package/common/mini-chart/index.tsx +8 -0
- package/common/mini-chart/mini-chart-props.ts +44 -0
- package/common/mini-chart/mini-chart.tsx +76 -0
- package/common/mini-chart/wrapper.tsx +23 -0
- package/context-providers/index.ts +1 -0
- package/{style → context-providers}/theme-provider.tsx +2 -2
- package/next/README.md +11 -0
- package/next/analytics/fpixel.ts +18 -0
- package/next/analytics/pixel-analytics.tsx +55 -0
- package/next/determine-device-middleware.ts +16 -0
- package/next/fonts/DrukTextWide-Bold-Trial.otf +0 -0
- package/next/fonts/DrukTextWide-Heavy-Trial.otf +0 -0
- package/next/fonts/DrukTextWide-Medium-Trial.otf +0 -0
- package/next/fonts/DrukWide-Bold-Trial.otf +0 -0
- package/next/fonts/DrukWide-Heavy-Trial.otf +0 -0
- package/next/fonts/DrukWide-Medium-Trial.otf +0 -0
- package/next/get-app-router-font-classes.ts +12 -0
- package/next/load-and-return-lux-next-fonts-on-import.ts +94 -0
- package/next/next-font-desc.ts +28 -0
- package/next/not-found-content.mdx +5 -0
- package/next/not-found.tsx +23 -0
- package/next/pages-router-font-vars.tsx +18 -0
- package/next/root-layout.tsx +62 -0
- package/package.json +12 -10
- package/primitives/carousel.tsx +263 -0
- package/primitives/index.ts +8 -1
- package/primitives/toggle-group.tsx +1 -1
- package/primitives/youtube-embed.tsx +1 -1
- package/siteDef/footer/community.tsx +67 -0
- package/siteDef/footer/company.ts +37 -0
- package/siteDef/footer/ecosystem.ts +37 -0
- package/siteDef/footer/index.tsx +26 -0
- package/siteDef/footer/legal.ts +28 -0
- package/siteDef/footer/network.ts +37 -0
- package/siteDef/footer/svg/warpcast-logo.svg +12 -0
- package/siteDef/main-nav.ts +35 -0
- package/style/hanzo-default-colors.css +2 -2
- package/style/social-svg.css +3 -0
- package/tailwind/{fontSize.tailwind.ts → fonts.tailwind.ts} +19 -1
- package/tailwind/index.ts +15 -4
- package/tailwind/lux-tw-fonts.ts +37 -0
- package/tailwind/{tailwind.config.hanzo-preset.js → tailwind.config.base.js} +2 -4
- package/tailwind/typo-plugin/get-plugin-styles.js +42 -42
- package/tailwind/typography-test.mdx +1 -1
- package/types/index.ts +15 -5
- package/types/site-def.ts +36 -0
- package/primitives/icons/index.ts +0 -18
- package/tailwind/fontFamily.tailwind.ts +0 -7
- package/tailwind/tailwind.config.hanzo-preset.d.ts +0 -5
- /package/{primitives → common}/icons/github.tsx +0 -0
- /package/{primitives → common}/icons/youtube-logo.tsx +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -36,6 +36,8 @@ const ScreenfulComponent: React.FC<{
|
|
|
36
36
|
|
|
37
37
|
const specified = (s: string) => (containsToken(b.specifiers, s))
|
|
38
38
|
const narrowGutters = specified('narrow-gutters') // eg, for a table object that is large
|
|
39
|
+
const noGutters = specified('no-gutters')
|
|
40
|
+
const vertCenter = specified('vert-center')
|
|
39
41
|
|
|
40
42
|
// content wrapper clx:
|
|
41
43
|
// [
|
|
@@ -49,6 +51,8 @@ const ScreenfulComponent: React.FC<{
|
|
|
49
51
|
// mobile header: 44px / pt-11
|
|
50
52
|
narrowGutters ?
|
|
51
53
|
'px-6 lg:px-8 2xl:px-2 pb-6 ' + (snapTile ? 'pt-15 md:pt-26 lg:pt-28 ' : '') // otherwise assume there is a Main
|
|
54
|
+
: noGutters ?
|
|
55
|
+
'px-0 pb-0 ' + (snapTile ? 'pt-11 lg:pt-20 ' : '') // otherwise assume there is a Main
|
|
52
56
|
:
|
|
53
57
|
'px-[8vw] xl:px-[1vw] pb-[8vh] pt-[calc(44px+4vh)] md:pt-[calc(80px+6vh)] ',
|
|
54
58
|
|
|
@@ -68,7 +72,13 @@ const ScreenfulComponent: React.FC<{
|
|
|
68
72
|
initialInView={initialInView}
|
|
69
73
|
/>
|
|
70
74
|
)}
|
|
71
|
-
<div className={cn(
|
|
75
|
+
<div className={cn(
|
|
76
|
+
...cwclx,
|
|
77
|
+
snapTile ? 'absolute left-0 right-0 top-0 bottom-0' : 'flex min-h-screen w-full',
|
|
78
|
+
contentClx,
|
|
79
|
+
vertCenter ? '!py-0 self-center' : ''
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
72
82
|
<Content block={b} agent={agent} className='w-full'/>
|
|
73
83
|
{b.footer}
|
|
74
84
|
</div>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { X } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import LuxLogo from './icons/lux-logo'
|
|
7
|
+
import { Button, Card } from '../primitives'
|
|
8
|
+
|
|
9
|
+
const ChatWidget: React.FC<{
|
|
10
|
+
title: string,
|
|
11
|
+
chatbotUrl: string,
|
|
12
|
+
subtitle?: string,
|
|
13
|
+
question?: string,
|
|
14
|
+
/*
|
|
15
|
+
Currently supports these icons from remix icons (https://remixicon.com/):
|
|
16
|
+
GlobalLineIcon,
|
|
17
|
+
ShieldFlashLineIcon,
|
|
18
|
+
BankCardLineIcon,
|
|
19
|
+
GroupLineIcon,
|
|
20
|
+
QuestionnaireLineIcon
|
|
21
|
+
*/
|
|
22
|
+
suggestedQuestions?: { heading: string, message: string, icon?: string }[]
|
|
23
|
+
}> = ({
|
|
24
|
+
title,
|
|
25
|
+
chatbotUrl,
|
|
26
|
+
subtitle,
|
|
27
|
+
question,
|
|
28
|
+
suggestedQuestions
|
|
29
|
+
}) => {
|
|
30
|
+
|
|
31
|
+
const [showChatbot, setShowChatbot] = React.useState<boolean>(false)
|
|
32
|
+
|
|
33
|
+
const onClick = () => { setShowChatbot(!showChatbot) }
|
|
34
|
+
|
|
35
|
+
const searchParams = new URLSearchParams()
|
|
36
|
+
if (question) {
|
|
37
|
+
searchParams.append('question', question)
|
|
38
|
+
}
|
|
39
|
+
if (suggestedQuestions) {
|
|
40
|
+
searchParams.append('sQuestions', suggestedQuestions.map(({ message }) => message).join(','))
|
|
41
|
+
searchParams.append('sHeadings', suggestedQuestions.map(({ heading }) => heading).join(','))
|
|
42
|
+
searchParams.append('sIcons', suggestedQuestions.map(({ icon }) => icon).join(','))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const iframeSrc = `${chatbotUrl}?${searchParams.toString()}`
|
|
46
|
+
|
|
47
|
+
return (<>
|
|
48
|
+
<div className={
|
|
49
|
+
'fixed bottom-0 sm:bottom-20 right-0 w-full h-full ' +
|
|
50
|
+
'sm:max-w-[400px] sm:max-h-[550px] sm:px-4 z-[1002] ' +
|
|
51
|
+
(showChatbot ? 'flex' : 'hidden')
|
|
52
|
+
}>
|
|
53
|
+
<Card className='flex flex-col h-full w-full'>
|
|
54
|
+
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
|
55
|
+
<h3 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h3>
|
|
56
|
+
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
|
57
|
+
<X />
|
|
58
|
+
</Button>
|
|
59
|
+
</div>
|
|
60
|
+
<iframe src={iframeSrc} className='h-full' />
|
|
61
|
+
</Card>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<Button
|
|
65
|
+
variant='outline'
|
|
66
|
+
size='link'
|
|
67
|
+
onClick={onClick}
|
|
68
|
+
className='fixed bottom-4 right-0 h-12 w-12 mx-4 rounded-full z-[1001]'
|
|
69
|
+
>
|
|
70
|
+
{showChatbot ? <X /> : <LuxLogo width={24} height={24} className='mt-2' />}
|
|
71
|
+
</Button>
|
|
72
|
+
</>)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default ChatWidget
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React, { useTransition } from 'react'
|
|
4
|
+
|
|
5
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
6
|
+
import { useForm, type SubmitHandler, type ControllerRenderProps } from 'react-hook-form'
|
|
7
|
+
import * as z from 'zod'
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
import validator from 'validator'
|
|
10
|
+
|
|
11
|
+
import { Loader2 } from 'lucide-react'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Button,
|
|
15
|
+
Input,
|
|
16
|
+
Form,
|
|
17
|
+
FormControl,
|
|
18
|
+
FormField,
|
|
19
|
+
FormItem,
|
|
20
|
+
FormMessage,
|
|
21
|
+
} from '../../primitives'
|
|
22
|
+
|
|
23
|
+
import type { ContactInfo, SubmitServerAction } from '../../types'
|
|
24
|
+
|
|
25
|
+
const ValidationSchema = z.object({
|
|
26
|
+
email: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1, { message: "Email must be provided." })
|
|
29
|
+
.email("Invalid email."),
|
|
30
|
+
phone: z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1, { message: "Telephone must be provided." })
|
|
33
|
+
.refine(validator.isMobilePhone, { message: "Invalid format." })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const ContactForm: React.FC<{
|
|
37
|
+
onSubmit: SubmitServerAction
|
|
38
|
+
enclosure: any
|
|
39
|
+
}> = ({
|
|
40
|
+
onSubmit,
|
|
41
|
+
enclosure
|
|
42
|
+
}) => {
|
|
43
|
+
|
|
44
|
+
const form = useForm<ContactInfo>({
|
|
45
|
+
// @ts-ignore (pnpm linking / tsc bug )
|
|
46
|
+
resolver: zodResolver(ValidationSchema),
|
|
47
|
+
defaultValues: {
|
|
48
|
+
email: '',
|
|
49
|
+
phone: '',
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const [isPending, startTransition] = useTransition()
|
|
54
|
+
|
|
55
|
+
const onFormSubmit: SubmitHandler<ContactInfo> = (data) => {
|
|
56
|
+
// https://github.com/orgs/react-hook-form/discussions/10757#discussioncomment-6672403
|
|
57
|
+
// @ts-ignore
|
|
58
|
+
startTransition(async () => {
|
|
59
|
+
await onSubmit(data, enclosure)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MyFormItem: React.FC<{
|
|
64
|
+
field: ControllerRenderProps<ContactInfo, 'email'> | ControllerRenderProps<ContactInfo, 'phone'>
|
|
65
|
+
placeholder: string
|
|
66
|
+
}> = ({
|
|
67
|
+
field,
|
|
68
|
+
placeholder
|
|
69
|
+
}) => (
|
|
70
|
+
<FormItem className="space-y-0" >
|
|
71
|
+
<FormControl>
|
|
72
|
+
<Input placeholder={placeholder} {...field} className="mt-0 text-foreground"/>
|
|
73
|
+
</FormControl>
|
|
74
|
+
<div className="flex flex-row justify-start items-stretch gap-2">
|
|
75
|
+
<FormMessage />
|
|
76
|
+
</div>
|
|
77
|
+
</FormItem>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Form {...form}>
|
|
82
|
+
<form onSubmit={form.handleSubmit(onFormSubmit)} className="w-3/4">
|
|
83
|
+
<div className='flex flex-col justify-start items-stretch mt-4'>
|
|
84
|
+
<FormField
|
|
85
|
+
control={form.control}
|
|
86
|
+
name='email'
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
render={({ field }) => ( <MyFormItem field={field} placeholder='email'/> )}
|
|
89
|
+
/>
|
|
90
|
+
<FormField
|
|
91
|
+
control={form.control}
|
|
92
|
+
name='phone'
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
render={({ field }) => ( <MyFormItem field={field} placeholder='phone'/> )}
|
|
95
|
+
/>
|
|
96
|
+
<Button disabled={isPending} type='submit' className='bg-primary text-primary-fg hover:bg-primary-hover'>
|
|
97
|
+
{isPending ? (<>
|
|
98
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
99
|
+
Please wait
|
|
100
|
+
</>
|
|
101
|
+
) : (
|
|
102
|
+
<>Submit</>
|
|
103
|
+
)}
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
</form>
|
|
107
|
+
</Form>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default ContactForm
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const Disclaimer: React.FC = () => (
|
|
4
|
+
<div>
|
|
5
|
+
By entering your mobile number and submitting, you consent to receive text messages from Lux at the number provided.
|
|
6
|
+
Message and data rates may apply. Message frequency varies.
|
|
7
|
+
You can unsubscribe at any time by replying STOP.
|
|
8
|
+
View our <a href='/privacy'>Privacy Policy</a> and <a href='/terms'>Terms & conditions</a>.
|
|
9
|
+
</div>
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export default Disclaimer
|
|
13
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import type { ButtonModalProps} from '../../types'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
DialogTrigger,
|
|
14
|
+
} from '../../primitives'
|
|
15
|
+
|
|
16
|
+
import ContactForm from './contact-form'
|
|
17
|
+
import Disclaimer from './disclaimer'
|
|
18
|
+
|
|
19
|
+
const ContactDialog: React.FC<ButtonModalProps> = ({
|
|
20
|
+
open,
|
|
21
|
+
onOpenChange,
|
|
22
|
+
buttonText,
|
|
23
|
+
buttonProps,
|
|
24
|
+
title,
|
|
25
|
+
byline,
|
|
26
|
+
action,
|
|
27
|
+
actionEnclosure
|
|
28
|
+
}) => (
|
|
29
|
+
<Dialog open={open} onOpenChange={onOpenChange} >
|
|
30
|
+
<DialogTrigger asChild>
|
|
31
|
+
<Button {...buttonProps} >{buttonText}</Button>
|
|
32
|
+
</DialogTrigger>
|
|
33
|
+
<DialogContent className="sm:max-w-[500px] p-0 gap-0 bg-background border">
|
|
34
|
+
<DialogHeader className='py-6 text-foreground'>
|
|
35
|
+
<DialogTitle className='text-4xl font-heading text-center text-inherit'>{title}</DialogTitle>
|
|
36
|
+
{byline && (<DialogDescription className='text-inherit text-xl text-center'>{byline} </DialogDescription>)}
|
|
37
|
+
</DialogHeader>
|
|
38
|
+
<div className='p-8 rounded-e-lg flex flex-col justify-start items-center'>
|
|
39
|
+
<ContactForm onSubmit={action} enclosure={actionEnclosure}/>
|
|
40
|
+
<div className='text-muted-1 text-xs mt-4' >
|
|
41
|
+
<Disclaimer />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</DialogContent>
|
|
45
|
+
</Dialog>
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
export default ContactDialog
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const FIRST = 2020
|
|
4
|
+
|
|
5
|
+
const Copyright: React.FC<{
|
|
6
|
+
className?: string
|
|
7
|
+
}> = ({
|
|
8
|
+
className=''
|
|
9
|
+
}) => {
|
|
10
|
+
|
|
11
|
+
const year = new Date().getFullYear()
|
|
12
|
+
const yearString = (year > FIRST) ? `${FIRST} - ${year}` : FIRST.toString()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={className}>
|
|
16
|
+
{`Copyright © ${yearString}`} <br className='sm:hidden'/>Lux Partners Ltd. <br className='md:hidden'/> All rights reserved.
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default Copyright
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
import React, { type PropsWithChildren, type ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import { X as LucideX} from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import Logo from './logo'
|
|
7
|
+
import { Sheet, SheetContent, SheetTrigger } from '../primitives/sheet'
|
|
8
|
+
|
|
9
|
+
const DrawerMenu: React.FC<PropsWithChildren & {
|
|
10
|
+
trigger: ReactNode
|
|
11
|
+
className?: string
|
|
12
|
+
showLogo?: boolean,
|
|
13
|
+
propogate?: boolean
|
|
14
|
+
asChild?: boolean
|
|
15
|
+
}> = ({
|
|
16
|
+
trigger,
|
|
17
|
+
children,
|
|
18
|
+
className='',
|
|
19
|
+
showLogo=true,
|
|
20
|
+
propogate=true,
|
|
21
|
+
asChild=false
|
|
22
|
+
}) => {
|
|
23
|
+
|
|
24
|
+
const [open, setOpen] = React.useState(false)
|
|
25
|
+
|
|
26
|
+
const onAction = () => { setOpen(false) }
|
|
27
|
+
|
|
28
|
+
// https://stackoverflow.com/a/49052730/11645689
|
|
29
|
+
const updatedChildren = React.Children.map(
|
|
30
|
+
children,
|
|
31
|
+
(child) => (React.cloneElement(
|
|
32
|
+
child as any, { onAction }
|
|
33
|
+
))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Sheet open={open} onOpenChange={setOpen} >
|
|
38
|
+
<SheetTrigger className={className} asChild={asChild}>
|
|
39
|
+
{trigger}
|
|
40
|
+
</SheetTrigger>
|
|
41
|
+
<SheetContent
|
|
42
|
+
side="right"
|
|
43
|
+
closeButtonClass='text-inherit opacity-90'
|
|
44
|
+
onClick={propogate ? onAction : () => {}}
|
|
45
|
+
closeElement={<LucideX className='h-6 w-6 text-inherit'/>}
|
|
46
|
+
centerElement={showLogo ? <Logo size='xs' className='' /> : null}
|
|
47
|
+
>
|
|
48
|
+
{updatedChildren}
|
|
49
|
+
</SheetContent>
|
|
50
|
+
</Sheet>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default DrawerMenu
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import type { LinkDef, SiteDef } from '../types'
|
|
4
|
+
import { Copyright } from '.'
|
|
5
|
+
import { NavItems } from '../primitives'
|
|
6
|
+
import { legal } from '../siteDef/footer/legal'
|
|
7
|
+
|
|
8
|
+
import Logo from './logo'
|
|
9
|
+
import { cn } from '../util'
|
|
10
|
+
|
|
11
|
+
const Footer: React.FC<{
|
|
12
|
+
siteDef: SiteDef,
|
|
13
|
+
className?: string,
|
|
14
|
+
noHorizPadding?: boolean
|
|
15
|
+
}> = ({
|
|
16
|
+
siteDef,
|
|
17
|
+
className='',
|
|
18
|
+
noHorizPadding=false
|
|
19
|
+
}) => {
|
|
20
|
+
|
|
21
|
+
const { footer, aboveCopyright } = siteDef
|
|
22
|
+
const smGridCols = Math.floor(footer.length/2)
|
|
23
|
+
const smGridColsClx = `sm:grid-cols-${smGridCols} `
|
|
24
|
+
const _aboveCopyright = (typeof aboveCopyright === 'undefined') ? legal : aboveCopyright
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<footer className={cn('grow flex flex-col justify-end gap-6 pb-[2vh]', className)}>
|
|
28
|
+
<div className={
|
|
29
|
+
(noHorizPadding ? '' : 'px-5 md:px-8 ') +
|
|
30
|
+
'grid grid-cols-2 gap-4 gap-y-6 md:gap-x-6 lg:gap-8 ' + smGridColsClx +
|
|
31
|
+
'md:w-full sm:justify-items-center md:mx-0 lg:w-full max-w-screen-2xl ' +
|
|
32
|
+
'md:flex md:flex-row md:justify-between '
|
|
33
|
+
}>
|
|
34
|
+
<div className='hidden lg:flex flex-col' key={0}>
|
|
35
|
+
<Logo size='md' />
|
|
36
|
+
</div>
|
|
37
|
+
{footer.map((defs: LinkDef[], index) => {
|
|
38
|
+
|
|
39
|
+
const xsColSpanClx = ((index === footer.length - 1) && (footer.length % 2 === 1)) ?
|
|
40
|
+
'xs:col-span-2 xs:mx-auto md:col-span-1 md:mx-0 ' : ''
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<NavItems
|
|
44
|
+
items={defs}
|
|
45
|
+
currentAs={siteDef.currentAs}
|
|
46
|
+
as='nav'
|
|
47
|
+
className={cn('sm:min-w-[150px] md:min-w-0 flex flex-col justify-start items-start ' +
|
|
48
|
+
'gap-[11px] sm:gap-[12px] md:gap-[15px] ',
|
|
49
|
+
xsColSpanClx
|
|
50
|
+
)}
|
|
51
|
+
key={index + 1}
|
|
52
|
+
itemClx={(def: LinkDef) => ((def.variant === 'linkFG') ?
|
|
53
|
+
'font-nav text-[15px]/[1.3] font-medium text-foreground tracking-normal'
|
|
54
|
+
:
|
|
55
|
+
'text-[15px]/[1.1] font-normal tracking-[0.2px] text-muted-1'
|
|
56
|
+
)}
|
|
57
|
+
/>
|
|
58
|
+
)
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
<div className='md:mt-[2vh]'>
|
|
62
|
+
{_aboveCopyright.length > 0 && (
|
|
63
|
+
<NavItems
|
|
64
|
+
items={_aboveCopyright}
|
|
65
|
+
as='div'
|
|
66
|
+
className={'flex flex-row justify-center gap-4 mb-2'}
|
|
67
|
+
itemClx={'text-sm text-center text-muted-2 underline hover:text-foreground'}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<Copyright className='text-sm text-center text-muted-3'/>
|
|
71
|
+
</div>
|
|
72
|
+
</footer>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default Footer
|
|
77
|
+
// flex flex-col justify-between gap-6
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Metadata types
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface DeprecatedMetadataFields {
|
|
8
|
+
/**
|
|
9
|
+
* Deprecated options that have a preferred method
|
|
10
|
+
* @deprecated Use appWebApp to configure apple-mobile-web-app-capable which provides
|
|
11
|
+
* @see https://www.appsloveworld.com/coding/iphone/11/difference-between-apple-mobile-web-app-capable-and-apple-touch-fullscreen-ipho
|
|
12
|
+
*/
|
|
13
|
+
'apple-touch-fullscreen'?: never
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Obsolete since iOS 7.
|
|
17
|
+
* @see https://web.dev/apple-touch-icon/
|
|
18
|
+
* @deprecated use icons.apple or instead
|
|
19
|
+
*/
|
|
20
|
+
'apple-touch-icon-precomposed'?: never
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TemplateString =
|
|
24
|
+
| DefaultTemplateString
|
|
25
|
+
| AbsoluteTemplateString
|
|
26
|
+
| AbsoluteString
|
|
27
|
+
export type DefaultTemplateString = {
|
|
28
|
+
default: string
|
|
29
|
+
template: string
|
|
30
|
+
}
|
|
31
|
+
export type AbsoluteTemplateString = {
|
|
32
|
+
absolute: string
|
|
33
|
+
template: string | null
|
|
34
|
+
}
|
|
35
|
+
export type AbsoluteString = {
|
|
36
|
+
absolute: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Author = {
|
|
40
|
+
// renders as <link rel="author"...
|
|
41
|
+
url?: string | URL
|
|
42
|
+
// renders as <meta name="author"...
|
|
43
|
+
name?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// does not include "unsafe-URL". to use this users should
|
|
47
|
+
// use '"unsafe-URL" as ReferrerEnum'
|
|
48
|
+
export type ReferrerEnum =
|
|
49
|
+
| 'no-referrer'
|
|
50
|
+
| 'origin'
|
|
51
|
+
| 'no-referrer-when-downgrade'
|
|
52
|
+
| 'origin-when-cross-origin'
|
|
53
|
+
| 'same-origin'
|
|
54
|
+
| 'strict-origin'
|
|
55
|
+
| 'strict-origin-when-cross-origin'
|
|
56
|
+
|
|
57
|
+
export type ColorSchemeEnum =
|
|
58
|
+
| 'normal'
|
|
59
|
+
| 'light'
|
|
60
|
+
| 'dark'
|
|
61
|
+
| 'light dark'
|
|
62
|
+
| 'dark light'
|
|
63
|
+
| 'only light'
|
|
64
|
+
|
|
65
|
+
type RobotsInfo = {
|
|
66
|
+
// all and none will be inferred from index/follow boolean options
|
|
67
|
+
index?: boolean
|
|
68
|
+
follow?: boolean
|
|
69
|
+
|
|
70
|
+
/** @deprecated set index to false instead */
|
|
71
|
+
noindex?: never
|
|
72
|
+
/** @deprecated set follow to false instead */
|
|
73
|
+
nofollow?: never
|
|
74
|
+
|
|
75
|
+
noarchive?: boolean
|
|
76
|
+
nosnippet?: boolean
|
|
77
|
+
noimageindex?: boolean
|
|
78
|
+
nocache?: boolean
|
|
79
|
+
notranslate?: boolean
|
|
80
|
+
indexifembedded?: boolean
|
|
81
|
+
nositelinkssearchbox?: boolean
|
|
82
|
+
unavailable_after?: string
|
|
83
|
+
'max-video-preview'?: number | string
|
|
84
|
+
'max-image-preview'?: 'none' | 'standard' | 'large'
|
|
85
|
+
'max-snippet'?: number
|
|
86
|
+
}
|
|
87
|
+
export type Robots = RobotsInfo & {
|
|
88
|
+
// if you want to specify an alternate robots just for google
|
|
89
|
+
googleBot?: string | RobotsInfo
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ResolvedRobots = {
|
|
93
|
+
basic: string | null
|
|
94
|
+
googleBot: string | null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type IconURL = string | URL
|
|
98
|
+
export type Icon = IconURL | IconDescriptor
|
|
99
|
+
export type IconDescriptor = {
|
|
100
|
+
url: string | URL
|
|
101
|
+
type?: string
|
|
102
|
+
sizes?: string
|
|
103
|
+
color?: string
|
|
104
|
+
/** defaults to rel="icon" unless superseded by Icons map */
|
|
105
|
+
rel?: string
|
|
106
|
+
media?: string
|
|
107
|
+
/**
|
|
108
|
+
* @see https://developer.mozilla.org/docs/Web/API/HTMLImageElement/fetchPriority
|
|
109
|
+
*/
|
|
110
|
+
fetchPriority?: 'high' | 'low' | 'auto'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type Icons = {
|
|
114
|
+
/** rel="icon" */
|
|
115
|
+
icon?: Icon | Icon[]
|
|
116
|
+
/** rel="shortcut icon" */
|
|
117
|
+
shortcut?: Icon | Icon[]
|
|
118
|
+
/**
|
|
119
|
+
* @see https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
|
|
120
|
+
* rel="apple-touch-icon"
|
|
121
|
+
*/
|
|
122
|
+
apple?: Icon | Icon[]
|
|
123
|
+
/** rel inferred from descriptor, defaults to "icon" */
|
|
124
|
+
other?: IconDescriptor | IconDescriptor[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type Verification = {
|
|
128
|
+
google?: null | string | number | (string | number)[]
|
|
129
|
+
yahoo?: null | string | number | (string | number)[]
|
|
130
|
+
yandex?: null | string | number | (string | number)[]
|
|
131
|
+
me?: null | string | number | (string | number)[]
|
|
132
|
+
// if you ad-hoc additional verification
|
|
133
|
+
other?: {
|
|
134
|
+
[name: string]: string | number | (string | number)[]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ResolvedVerification = {
|
|
139
|
+
google?: null | (string | number)[]
|
|
140
|
+
yahoo?: null | (string | number)[]
|
|
141
|
+
yandex?: null | (string | number)[]
|
|
142
|
+
me?: null | (string | number)[]
|
|
143
|
+
other?: {
|
|
144
|
+
[name: string]: (string | number)[]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type ResolvedIcons = {
|
|
149
|
+
icon: IconDescriptor[]
|
|
150
|
+
apple: IconDescriptor[]
|
|
151
|
+
shortcut?: IconDescriptor[]
|
|
152
|
+
other?: IconDescriptor[]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type ThemeColorDescriptor = {
|
|
156
|
+
color: string
|
|
157
|
+
media?: string
|
|
158
|
+
}
|