Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0425bbbf | ||
|
|
e28cd85934 | ||
|
|
9f5a9d31af | ||
|
|
cf8f54fe13 | ||
|
|
21c84cda35 | ||
|
|
018e9cd911 | ||
|
|
1714bf76ea | ||
|
|
1f1d484eee | ||
|
|
0737ddf80a | ||
|
|
5b96b360c8 | ||
|
|
a5eea26642 | ||
|
|
eb2f482023 | ||
|
|
04e419ba08 | ||
|
|
54a7de8ab2 | ||
|
|
754c73c98a | ||
|
|
c72a1c09b7 | ||
|
|
254cfc9791 | ||
|
|
db24824250 | ||
|
|
8c5a9f32be | ||
|
|
42dc6f2d6f | ||
|
|
09a94343ce | ||
|
|
cd481922d9 | ||
|
|
77767dfbea | ||
|
|
f388383190 |
@@ -1,8 +0,0 @@
|
||||
# Changesets
|
||||
|
||||
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||
|
||||
We have a quick list of common questions to get you started engaging with this project in
|
||||
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "luxdefi/ui" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["www", "**-template"]
|
||||
}
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
comment:
|
||||
if: |
|
||||
github.repository_owner == 'luxdefi-ui' &&
|
||||
github.repository_owner == 'hanzoai-ui' &&
|
||||
${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
name: Write comment to the PR
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
for (const artifact of allArtifacts.data.artifacts) {
|
||||
// Extract the PR number and package version from the artifact name
|
||||
const match = /^npm-package-luxdefi-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
const match = /^npm-package-hanzoai-ui@(.*?)-pr-(\d+)/.exec(artifact.name);
|
||||
|
||||
if (match) {
|
||||
require("fs").appendFileSync(
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
A new prerelease is available for testing:
|
||||
|
||||
```sh
|
||||
npx luxdefi-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
npx hanzoai-ui@${{ env.BETA_PACKAGE_VERSION }}
|
||||
```
|
||||
|
||||
- name: "Remove the autorelease label once published"
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
prerelease:
|
||||
if: |
|
||||
github.repository_owner == 'luxdefi' &&
|
||||
github.repository_owner == 'hanzoai' &&
|
||||
contains(github.event.pull_request.labels.*.name, '🚀 autorelease')
|
||||
name: Build & Publish a beta release to NPM
|
||||
runs-on: ubuntu-latest
|
||||
@@ -56,5 +56,5 @@ jobs:
|
||||
- name: Upload packaged artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: npm-package-luxdefi-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
name: npm-package-hanzoai-ui@${{ steps.package-version.outputs.current-version }}-pr-${{ github.event.number }} # encode the PR number into the artifact name
|
||||
path: packages/cli/dist/index.js
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'luxdefi-ui' }}
|
||||
if: ${{ github.repository_owner == 'hanzoai-ui' }}
|
||||
name: Create a PR for release workflow
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
MIT License
|
||||
BSD License
|
||||
|
||||
Copyright (c) 2023 Lux Partners Limited
|
||||
Copyright (c) 2015-present, Hanzo, Inc.
|
||||
All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name Hanzo nor the names of its contributors may be used to
|
||||
endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,12 +1,12 @@
|
||||
# @luxdefi/ui
|
||||
# @hanzo/ui
|
||||
|
||||
Accessible and customizable components that you can copy and paste into your DeFi apps.
|
||||
Accessible and customizable components that you can use to build your AI apps.
|
||||
|
||||

|
||||
|
||||
## Documentation
|
||||
|
||||
Visit http://ui.lux.finance to view the documentation.
|
||||
Visit http://ui.hanzo.ai to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/luxdefi/ui/blob/main/LICENSE.md).
|
||||
Licensed under the [BSD 3 Clause](./LICENSE.md) License.
|
||||
|
||||
@@ -1 +1 @@
|
||||
> Files inside this directory is autogenerated by `./scripts/build-registry.ts`. **Do not edit them manually.** - luxdefi
|
||||
> Files inside this directory is autogenerated by `./scripts/build-registry.ts`. **Do not edit them manually.** - hanzoai
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function generateMetadata({
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@luxdefi",
|
||||
creator: "@hanzoai",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function DemoGithub() {
|
||||
<Card>
|
||||
<CardHeader className="grid grid-cols-[1fr_110px] items-start gap-4 space-y-0">
|
||||
<div className="space-y-1">
|
||||
<CardTitle>luxdefi/ui</CardTitle>
|
||||
<CardTitle>hanzoai/ui</CardTitle>
|
||||
<CardDescription>
|
||||
Beautifully designed components that you can copy and paste into
|
||||
your apps. Accessible. Customizable. Open Source.
|
||||
|
||||
@@ -21,7 +21,7 @@ export function UserNav() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src="/avatars/01.png" alt="@luxdefi" />
|
||||
<AvatarImage src="/avatars/01.png" alt="@hanzoai" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
@@ -29,7 +29,7 @@ export function UserNav() {
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">luxdefi</p>
|
||||
<p className="text-sm font-medium leading-none">hanzoai</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
m@example.com
|
||||
</p>
|
||||
|
||||
@@ -57,8 +57,8 @@ type ProfileFormValues = z.infer<typeof profileFormSchema>
|
||||
const defaultValues: Partial<ProfileFormValues> = {
|
||||
bio: "I own a computer.",
|
||||
urls: [
|
||||
{ value: "https://luxdefi.com" },
|
||||
{ value: "http://twitter.com/luxdefi" },
|
||||
{ value: "https://hanzo.ai" },
|
||||
{ value: "http://twitter.com/hanzoai" },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function ProfileForm() {
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="luxdefi" {...field} />
|
||||
<Input placeholder="hanzoai" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name. It can be your real name or a
|
||||
|
||||
@@ -21,7 +21,7 @@ export function UserNav() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
|
||||
<Avatar className="h-9 w-9">
|
||||
<AvatarImage src="/avatars/03.png" alt="@luxdefi" />
|
||||
<AvatarImage src="/avatars/03.png" alt="@hanzoai" />
|
||||
<AvatarFallback>SC</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
@@ -29,7 +29,7 @@ export function UserNav() {
|
||||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm font-medium leading-none">luxdefi</p>
|
||||
<p className="text-sm font-medium leading-none">hanzoai</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
m@example.com
|
||||
</p>
|
||||
|
||||
@@ -30,11 +30,11 @@ export const metadata: Metadata = {
|
||||
],
|
||||
authors: [
|
||||
{
|
||||
name: "luxdefi",
|
||||
url: "https://ui.lux.network",
|
||||
name: "hanzoai",
|
||||
url: "https://ui.hanzo.ai",
|
||||
},
|
||||
],
|
||||
creator: "luxdefi",
|
||||
creator: "hanzoai",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
@@ -56,7 +56,7 @@ export const metadata: Metadata = {
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
images: [siteConfig.ogImage],
|
||||
creator: "@luxdefi",
|
||||
creator: "@hanzoai",
|
||||
},
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
|
||||
@@ -16,37 +16,37 @@ const examples = [
|
||||
{
|
||||
name: "Dashboard",
|
||||
href: "/examples/dashboard",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/dashboard",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/dashboard",
|
||||
},
|
||||
{
|
||||
name: "Cards",
|
||||
href: "/examples/cards",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/cards",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/cards",
|
||||
},
|
||||
{
|
||||
name: "Tasks",
|
||||
href: "/examples/tasks",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/tasks",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/tasks",
|
||||
},
|
||||
{
|
||||
name: "Playground",
|
||||
href: "/examples/playground",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/playground",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/playground",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/examples/forms",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/forms",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/forms",
|
||||
},
|
||||
{
|
||||
name: "Music",
|
||||
href: "/examples/music",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/music",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/music",
|
||||
},
|
||||
{
|
||||
name: "Authentication",
|
||||
href: "/examples/authentication",
|
||||
code: "https://github.com/luxdefi/ui/tree/main/apps/www/app/examples/authentication",
|
||||
code: "https://github.com/hanzoai/ui/tree/main/apps/www/app/examples/authentication",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const siteConfig = {
|
||||
name: "luxdefi/ui",
|
||||
url: "https://ui.luxdefi.com",
|
||||
ogImage: "https://ui.luxdefi.com/og.jpg",
|
||||
url: "https://ui.hanzo.ai",
|
||||
ogImage: "https://ui.hanzo.ai/og.jpg",
|
||||
description:
|
||||
"Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.",
|
||||
links: {
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Powered by amazing open source projects.
|
||||
|
||||
## About
|
||||
|
||||
[ui.luxdefi.com](https://ui.luxdefi.com) is a project by [luxdefi](https://luxdefi.com).
|
||||
[ui.hanzo.ai](https://ui.hanzo.ai) is a project by [luxdefi](https://hanzo.ai).
|
||||
|
||||
## Credits
|
||||
|
||||
@@ -17,4 +17,4 @@ description: Powered by amazing open source projects.
|
||||
|
||||
## License
|
||||
|
||||
MIT © [luxdefi](https://luxdefi.com)
|
||||
MIT © [luxdefi](https://hanzo.ai)
|
||||
|
||||
@@ -23,11 +23,11 @@ See the <Link href="/docs/cli">CLI section</Link> for more information.
|
||||
|
||||
## $schema
|
||||
|
||||
You can see the JSON Schema for `components.json` [here](https://ui.luxdefi.com/schema.json).
|
||||
You can see the JSON Schema for `components.json` [here](https://ui.hanzo.ai/schema.json).
|
||||
|
||||
```json title="components.json"
|
||||
{
|
||||
"$schema": "https://ui.luxdefi.com/schema.json"
|
||||
"$schema": "https://ui.hanzo.ai/schema.json"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ Which frameworks are supported?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
|
||||
You can use any framework that supports React. [Next.js](https://ui.luxdefi.com/docs/installation/next), [Astro](https://ui.luxdefi.com/docs/installation/astro), [Remix](https://ui.luxdefi.com/docs/installation/remix), [Gatsby](https://ui.luxdefi.com/docs/installation/gatsby) etc.
|
||||
You can use any framework that supports React. [Next.js](https://ui.hanzo.ai/docs/installation/next), [Astro](https://ui.hanzo.ai/docs/installation/astro), [Remix](https://ui.hanzo.ai/docs/installation/remix), [Gatsby](https://ui.hanzo.ai/docs/installation/gatsby) etc.
|
||||
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@luxdefi/ui-demo-www",
|
||||
"name": "@hanzoai/ui-demo-www",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"_dependencies": {
|
||||
"@babel/core": "^7.22.1",
|
||||
"@changesets/changelog-github": "^0.4.8",
|
||||
"@changesets/cli": "^2.26.1",
|
||||
"@commitlint/cli": "^17.6.3",
|
||||
"@commitlint/config-conventional": "^17.6.3",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",
|
||||
"@manypkg/cli": "^0.20.0",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/react": "^18.2.7",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@typescript-eslint/parser": "^5.59.7",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"concurrently": "^8.0.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-next": "13.3.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-config-turbo": "^1.9.9",
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-tailwindcss": "^3.12.0",
|
||||
"postcss": "^8.4.24",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"turbo": "^1.9.9",
|
||||
"typescript": "^4.9.5",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.31.1"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@luxdefi/ui-mono",
|
||||
"name": "@hanzo/ui-mono",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"license": "BSD-3-Clause",
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "@lx/ui",
|
||||
"url": "https://twitter.com/luxdefi"
|
||||
"name": "hanzoai dev dudes",
|
||||
"url": "https://twitter.com/hanzoai"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
@@ -31,5 +31,10 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "^18.2.48"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
|
Before Width: | Height: | Size: 831 B After Width: | Height: | Size: 831 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
|
||||
import { containsToken, cn } from '../../util'
|
||||
import type { Block, BulletCardsBlock } from '../def'
|
||||
import InlineIcon from '../../primitives/inline-icon'
|
||||
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
import GridBlockComponent from './grid-block'
|
||||
|
||||
const BulletCardsBlockComponent: React.FC<BlockComponentProps> = ({
|
||||
block,
|
||||
className='',
|
||||
agent
|
||||
}) => {
|
||||
|
||||
if (block.blockType !== 'bullet-cards') {
|
||||
return <>bullet cards block required</>
|
||||
}
|
||||
const b = block as BulletCardsBlock
|
||||
const specified = (s: string) => (containsToken(b.specifiers, s))
|
||||
|
||||
const borderclx = specified('border-muted-3') ?
|
||||
'md:border-muted-3'
|
||||
:
|
||||
(specified('border-muted-1') ? 'md:border-muted-1' : 'md:border-muted-2')
|
||||
|
||||
|
||||
return (
|
||||
<GridBlockComponent block={{blockType: 'grid', grid: b.grid} as Block} className={className} agent={agent}>
|
||||
{b.cards.map((card, index) => (
|
||||
<div key={index} className={cn('px-0 sm:px-4 py-1 md:py-4 md:border rounded ' +
|
||||
'flex flex-row justify-start items-center not-typography text-foreground',
|
||||
borderclx
|
||||
)}>
|
||||
<InlineIcon icon={card.icon} size={b.iconSize ?? 28} agent={agent} className='shrink-0 mr-2 w-6 md:mr-4 md:w-7 '/>
|
||||
<p className='m-0 text-sm sm:text-base'>{card.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</GridBlockComponent>
|
||||
)
|
||||
}
|
||||
|
||||
export default BulletCardsBlockComponent
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from '../../../primitives'
|
||||
|
||||
import { cn, containsToken } from '../../../util'
|
||||
|
||||
import {
|
||||
getSpecifierData,
|
||||
getPrimaryStartingWith,
|
||||
getDim,
|
||||
} from '../../../util/specifier'
|
||||
|
||||
import type CarteBlancheBlock from '../../def/carte-blanche-block'
|
||||
|
||||
import CTABlockComponent from '../cta-block'
|
||||
import Content from '../content'
|
||||
import type BlockComponentProps from '../block-component-props'
|
||||
import { EnhHeadingBlockComponent } from '..'
|
||||
|
||||
type CardSection = 'entire' | 'header' | 'content' | 'footer'
|
||||
|
||||
const _getClx = (specifier: string, section: CardSection): string => {
|
||||
let result = ''
|
||||
if (specifier === 'big-padding') {
|
||||
switch (section) {
|
||||
// defaults: p-4 lg:p-6 xl:p-8
|
||||
case 'header': {
|
||||
result = 'md:p-8 lg:p-12 xl:p-16'
|
||||
} break
|
||||
}
|
||||
}
|
||||
else if (specifier === 'no-inner-borders') {
|
||||
switch (section) {
|
||||
case 'header': {
|
||||
result = 'border-none'
|
||||
} break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const CarteBlancheBlockComponent: React.FC<
|
||||
BlockComponentProps
|
||||
> = ({
|
||||
block,
|
||||
className='',
|
||||
agent,
|
||||
}) => {
|
||||
|
||||
if (block.blockType !== 'carte-blanche') {
|
||||
return <>carte blanche block required</>
|
||||
}
|
||||
|
||||
const b = block as CarteBlancheBlock
|
||||
|
||||
const specified = (s: string): boolean => (containsToken(b.specifiers, s))
|
||||
const getClx = (specifier: string, section: CardSection): string => (
|
||||
(specified(specifier)) ? _getClx(specifier, section) : ''
|
||||
)
|
||||
|
||||
//const bigPadding = specified('big-padding')
|
||||
|
||||
const headingclx = [
|
||||
getClx('big-padding', 'header'),
|
||||
getClx('no-inner-borders', 'header'),
|
||||
].join(' ')
|
||||
|
||||
return (
|
||||
<Card className={cn('flex flex-col ', className)} >
|
||||
{b.heading && (
|
||||
<CardHeader className={cn('typography-img:m-0', headingclx)} >
|
||||
{b.topContent && (
|
||||
<Content blocks={b.topContent} agent={agent} className=''/>
|
||||
)}
|
||||
<EnhHeadingBlockComponent block={b.heading} className='text-accent' agent={agent}/>
|
||||
</CardHeader>
|
||||
)}
|
||||
{b.content && (
|
||||
<CardContent className={cn('typography flex flex-col justify-center', className)}>
|
||||
<Content blocks={b.content} agent={agent}/>
|
||||
</CardContent>
|
||||
)}
|
||||
{b.cta && (
|
||||
<CardFooter className={'grid grid-cols-1 gap-2 md:flex md:flex-row md:justify-center ' /*+ paddingclx*/} >
|
||||
<CTABlockComponent block={b.cta} agent={agent}/>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default CarteBlancheBlockComponent
|
||||
@@ -2,32 +2,40 @@ import React, { type ComponentType, type ReactNode} from 'react'
|
||||
|
||||
import type * as B from '../def'
|
||||
|
||||
import CardBlockComponent from './card-block'
|
||||
import HeadingBlockComponent from './heading-block'
|
||||
import CTABlockComponent from './cta-block'
|
||||
import SpaceBlockComponent from './space-block'
|
||||
import ImageBlockComponent from './image-block'
|
||||
import VideoBlockComponent from './video-block'
|
||||
import AccordianBlockComponent from './accordian-block'
|
||||
import BulletCardsBlockComponent from './bullet-cards-block'
|
||||
import CTABlockComponent from './cta-block'
|
||||
import CardBlockComponent from './card-block'
|
||||
import CarteBlancheBlockComponent from './carte-blanche-block'
|
||||
import EnhHeadingBlockComponent from './enh-heading-block'
|
||||
import HeadingBlockComponent from './heading-block'
|
||||
import GroupBlockComponent from './group-block'
|
||||
import GridBlockComponent from './grid-block'
|
||||
import ImageBlockComponent from './image-block'
|
||||
import SpaceBlockComponent from './space-block'
|
||||
import VideoBlockComponent from './video-block'
|
||||
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
|
||||
const map = new Map<string, ComponentType<BlockComponentProps>>()
|
||||
map.set('accordian', AccordianBlockComponent)
|
||||
map.set('bullet-cards', BulletCardsBlockComponent)
|
||||
map.set('card', CardBlockComponent)
|
||||
map.set('heading', HeadingBlockComponent)
|
||||
map.set('carte-blanche', CarteBlancheBlockComponent)
|
||||
map.set('cta', CTABlockComponent)
|
||||
map.set('heading', HeadingBlockComponent)
|
||||
map.set('enh-heading', EnhHeadingBlockComponent as ComponentType<BlockComponentProps>)
|
||||
map.set('space', SpaceBlockComponent)
|
||||
map.set('image', ImageBlockComponent)
|
||||
map.set('video', VideoBlockComponent)
|
||||
map.set('accordian', AccordianBlockComponent)
|
||||
map.set('group', GroupBlockComponent)
|
||||
map.set('grid', GridBlockComponent)
|
||||
|
||||
const registerBlockType = (key: string, type: ComponentType<BlockComponentProps>): void => {
|
||||
map.set(key, type)
|
||||
}
|
||||
|
||||
const renderBlock = (block: B.Block, className: string, agent: string, keyStr?: string): ReactNode => {
|
||||
const renderBlock = (block: B.Block, className: string, agent?: string, keyStr?: string): ReactNode => {
|
||||
if (block.blockType === 'element') {
|
||||
return (block as B.ElementBlock).element
|
||||
}
|
||||
@@ -43,7 +51,7 @@ const ContentComponent: React.FC<{
|
||||
}> = ({
|
||||
blocks,
|
||||
className='',
|
||||
agent=''
|
||||
agent
|
||||
}) => {
|
||||
if (!blocks) return null
|
||||
if (Array.isArray(blocks)) {
|
||||
@@ -31,34 +31,43 @@ const CtaBlockComponent: React.FC<BlockComponentProps & {
|
||||
let itemclx = ''
|
||||
if (containsToken(specifiers, 'fill')) {
|
||||
wrapperClasses += 'w-full '
|
||||
itemclx += 'grow '
|
||||
itemclx += 'grow shrink !min-w-0'
|
||||
}
|
||||
else if (containsToken(specifiers, 'left')) {
|
||||
wrapperClasses += 'sm:justify-start '
|
||||
wrapperClasses += 'md:justify-start '
|
||||
}
|
||||
else if (containsToken(specifiers, 'right')) {
|
||||
wrapperClasses += 'sm:justify-end '
|
||||
wrapperClasses += 'md:justify-end '
|
||||
}
|
||||
else {
|
||||
wrapperClasses += 'md:justify-center '
|
||||
}
|
||||
|
||||
const containerclx = (
|
||||
agent === 'phone'
|
||||
&&
|
||||
containsToken(specifiers, 'mobile-2-columns')
|
||||
&&
|
||||
elements.length > 1
|
||||
)
|
||||
?
|
||||
'grid grid-cols-2 gap-2 self-stretch'
|
||||
const mobile2Columns = containsToken(specifiers, 'mobile-2-columns')
|
||||
const mobileCenterFirstIfOdd = containsToken(specifiers, 'mobile-center-first-if-odd')
|
||||
const mobileOddFullWidth = containsToken(specifiers, 'mobile-odd-full-width')
|
||||
|
||||
const layoutclx = (mobile2Columns && elements.length > 1) ?
|
||||
'grid grid-cols-2 gap-2 self-stretch md:flex md:flex-sm md:justify-center '
|
||||
:
|
||||
'flex flex-col items-stretch gap-2 self-stretch sm:flex-row sm:justify-center '
|
||||
'flex flex-col items-stretch gap-2 self-stretch md:flex-row sm:justify-center '
|
||||
|
||||
const getMobileColSpanClx = (index: number, total: number) => {
|
||||
const indexToCenter = (total % 2 === 0) ? -1 : (mobileCenterFirstIfOdd) ? 0 : total - 1
|
||||
const widthclx = mobileOddFullWidth ? 'w-full ' : 'w-3/5 mx-auto '
|
||||
return (
|
||||
(agent === 'phone' && mobile2Columns && (index === indexToCenter)) ? ('col-span-2 ' + widthclx) : ''
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
containerclx,
|
||||
layoutclx,
|
||||
wrapperClasses,
|
||||
className
|
||||
)}>
|
||||
{elements.map((element, index) => {
|
||||
const twoColClx = getMobileColSpanClx(index, elements.length)
|
||||
if ((element as any).title) {
|
||||
const def = element as LinkDef
|
||||
return renderLink ? renderLink(def, index) : (
|
||||
@@ -66,7 +75,7 @@ const CtaBlockComponent: React.FC<BlockComponentProps & {
|
||||
def={def}
|
||||
key={index}
|
||||
size={itemSize}
|
||||
className={cn(itemclx, itemClasses)}
|
||||
className={cn(itemclx, itemClasses, twoColClx)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -77,7 +86,7 @@ const CtaBlockComponent: React.FC<BlockComponentProps & {
|
||||
def={def}
|
||||
key={index}
|
||||
size={itemSize}
|
||||
className={cn(itemclx, itemClasses)}
|
||||
className={cn(itemclx, itemClasses, twoColClx)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, {type ElementType} from 'react'
|
||||
|
||||
import type { Icon } from '../../types'
|
||||
import type { EnhHeadingBlock } from '../def'
|
||||
import { ApplyTypography } from '../../primitives'
|
||||
import { cn, containsToken } from '../../util'
|
||||
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
import InlineIcon from '../../primitives/inline-icon'
|
||||
|
||||
const DEFAULTS = {
|
||||
preheading: {
|
||||
tag: 'h4' as ElementType,
|
||||
mb: 2
|
||||
},
|
||||
heading: {
|
||||
tag: 'h1' as ElementType,
|
||||
mb: 2
|
||||
},
|
||||
byline: {
|
||||
tag: 'h6' as ElementType,
|
||||
}
|
||||
}
|
||||
|
||||
const tagFromLevel = (level: number): ElementType => {
|
||||
switch (level) {
|
||||
case 1: {
|
||||
return 'h1'
|
||||
}
|
||||
case 2: {
|
||||
return 'h2'
|
||||
}
|
||||
case 3: {
|
||||
return 'h3'
|
||||
}
|
||||
case 4: {
|
||||
return 'h4'
|
||||
}
|
||||
case 5: {
|
||||
return 'h5'
|
||||
}
|
||||
case 6: {
|
||||
return 'h6'
|
||||
}
|
||||
}
|
||||
return 'p'
|
||||
}
|
||||
|
||||
// TODO Impl icon support
|
||||
const Element: React.FC<{
|
||||
asTag: ElementType
|
||||
text: string
|
||||
icon?: Icon
|
||||
iconLeft?: boolean
|
||||
className?: string
|
||||
}> = ({
|
||||
asTag: Tag,
|
||||
text,
|
||||
icon,
|
||||
iconLeft=true,
|
||||
className : elClassName=''
|
||||
}) => (
|
||||
<Tag className={elClassName}>{text}</Tag>
|
||||
)
|
||||
|
||||
const getPositionClx = (
|
||||
specified: (s: string) => boolean,
|
||||
agent: string | undefined
|
||||
): {
|
||||
preheading: string
|
||||
heading: string
|
||||
byline: string
|
||||
} => {
|
||||
|
||||
const mobileHeadingCentered = specified('mobile-heading-centered')
|
||||
const headingCentered = specified('center')
|
||||
const headingRight = specified('right')
|
||||
const bylineCentered = specified('byline-center')
|
||||
const bylineRight = specified('byline-right')
|
||||
|
||||
let headerclx = ''
|
||||
if (agent === 'phone') {
|
||||
headerclx = (mobileHeadingCentered || headingCentered) ?
|
||||
'self-center text-center' : (headingRight ? 'self-end text-right' : 'self-start text-left')
|
||||
}
|
||||
else {
|
||||
const largerclx = (headingCentered) ?
|
||||
'self-center text-center' : (headingRight ? 'self-end text-right' : 'self-start text-left')
|
||||
|
||||
if (mobileHeadingCentered) {
|
||||
headerclx = 'self-center text-center md:' + largerclx.split(' ').join(' md:')
|
||||
}
|
||||
else {
|
||||
headerclx = largerclx
|
||||
}
|
||||
}
|
||||
|
||||
const bylineclx = (bylineCentered) ?
|
||||
'self-center' : (bylineRight ? 'self-end' : 'self-start')
|
||||
|
||||
return {
|
||||
preheading: headerclx,
|
||||
heading: headerclx,
|
||||
byline: bylineclx
|
||||
}
|
||||
}
|
||||
|
||||
const EnhHeadingBlockComponent: React.FC<
|
||||
BlockComponentProps & {
|
||||
applyTypography?: boolean
|
||||
extraSpecifiers?: string
|
||||
}> = ({
|
||||
block,
|
||||
className='',
|
||||
agent,
|
||||
applyTypography=true,
|
||||
extraSpecifiers=''
|
||||
}) => {
|
||||
|
||||
if (block.blockType !== 'enh-heading') {
|
||||
return <>enhance heading block required</>
|
||||
}
|
||||
const b = block as EnhHeadingBlock
|
||||
const specified = (s: string) => (containsToken(b.specifiers + extraSpecifiers, s))
|
||||
const preheadingHeadingFont = specified('preheading-heading-font')
|
||||
const phFontClx = preheadingHeadingFont ? 'font-heading' : ''
|
||||
|
||||
const positionclx = getPositionClx(specified, agent)
|
||||
|
||||
const Inner: React.FC = () => {
|
||||
const toRender = [
|
||||
{
|
||||
tag: (b.preheading) ?
|
||||
(b.preheading.level !== undefined ? tagFromLevel(b.preheading.level) : DEFAULTS.preheading.tag)
|
||||
:
|
||||
undefined,
|
||||
clx: (b.preheading) ?
|
||||
(b.preheading.mb !== undefined ?
|
||||
`mb-${b.preheading.mb}` : `mb-${DEFAULTS.preheading.mb}`) + ' ' + positionclx.preheading + ' ' + phFontClx
|
||||
:
|
||||
positionclx.preheading + ' ' + phFontClx,
|
||||
text: (b.preheading) ? (b.preheading.text ) : undefined,
|
||||
},
|
||||
{
|
||||
tag: (b.heading.level !== undefined ? tagFromLevel(b.heading.level) : DEFAULTS.heading.tag),
|
||||
clx: (b.heading.mb !== undefined ? `mb-${b.heading.mb}` : `mb-${DEFAULTS.heading.mb}`) + ' ' + positionclx.heading,
|
||||
text: b.heading.text,
|
||||
},
|
||||
{
|
||||
tag: (b.byline) ?
|
||||
(b.byline.level !== undefined ? tagFromLevel(b.byline.level) : DEFAULTS.byline.tag)
|
||||
:
|
||||
undefined,
|
||||
clx: positionclx.byline,
|
||||
text: (b.byline) ? (b.byline.text ) : undefined,
|
||||
},
|
||||
] as {
|
||||
tag: ElementType
|
||||
clx: string
|
||||
text: string
|
||||
}[]
|
||||
|
||||
let iconRendered = false
|
||||
return <>
|
||||
{toRender.map(({tag, clx, text}, index) => {
|
||||
if (!tag) return null
|
||||
if (b.icon && !iconRendered) {
|
||||
iconRendered = true
|
||||
return (
|
||||
<div className={cn('flex flex-row items-center', clx)} key={`div-${index}`}>
|
||||
<InlineIcon icon={b.icon} size={b.iconSize ?? 32} agent={agent}/>
|
||||
<Element asTag={tag} text={text} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
(<Element asTag={tag} text={text} className={clx} key={`el-${index}`}/>)
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
return applyTypography ? (
|
||||
<ApplyTypography className={cn('flex flex-col w-full !gap-0', className)}>
|
||||
<Inner />
|
||||
</ApplyTypography>
|
||||
) : (
|
||||
<div className={cn('flex flex-col w-full gap-0', className)}>
|
||||
<Inner />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EnhHeadingBlockComponent
|
||||
@@ -0,0 +1,12 @@
|
||||
interface GridBlockMutator {
|
||||
gapClx?: string
|
||||
getCellClx?: (
|
||||
cellIndex: number,
|
||||
cellCount: number,
|
||||
colCount: number
|
||||
) => string
|
||||
}
|
||||
|
||||
export {
|
||||
type GridBlockMutator as default
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
|
||||
import { cn, containsToken } from '../../../util'
|
||||
import type { GridBlock } from '../../def'
|
||||
import type { GridColumnSpec } from '../../../types'
|
||||
|
||||
import Content from '../content'
|
||||
import type BlockComponentProps from '../block-component-props'
|
||||
|
||||
import getMutator from './mutator-registry'
|
||||
|
||||
// These must be / are safelisted in tailwind.config
|
||||
const gridClx = (d: GridColumnSpec, prefix?: string ): string => (
|
||||
(typeof d === 'number') ?
|
||||
`${(prefix ?? '')}grid-cols-${d} `
|
||||
:
|
||||
`${(prefix ?? '')}grid-cols-${d.columns} ${(prefix ?? '')}gap-${d.gap} `
|
||||
)
|
||||
|
||||
/**
|
||||
* A component that lays out the Blocks in 'cells' on a grid.
|
||||
* If invoked directly, and children are supplied,
|
||||
* block.cells is ignore and the children are rendered.
|
||||
*/
|
||||
const GridBlockComponent: React.FC<
|
||||
BlockComponentProps & PropsWithChildren
|
||||
> = ({
|
||||
block,
|
||||
className='',
|
||||
agent,
|
||||
children
|
||||
}) => {
|
||||
|
||||
if (block.blockType !== 'grid') {
|
||||
return <>grid block required</>
|
||||
}
|
||||
|
||||
const { cells, grid, specifiers } = block as GridBlock
|
||||
const specified = (s: string) => (containsToken(specifiers, s))
|
||||
const mutator = specified('style-table-borders') ? getMutator('style-table-borders') : undefined
|
||||
|
||||
// https://tailwindcss.com/docs/content-configuration#dynamic-class-names
|
||||
// All variants in use MUST be in style/safelist.tailwind.js
|
||||
let clx = 'grid '
|
||||
if (agent === 'phone') {
|
||||
const d = (grid.mobile) ? grid.mobile : (grid.at.xs ? grid.at.xs : (grid.at.sm ?? 1))
|
||||
clx += gridClx(d)
|
||||
}
|
||||
else {
|
||||
let defaultSet = false
|
||||
for (const [key, value] of Object.entries(grid.at as typeof grid.at)) {
|
||||
if (!defaultSet) {
|
||||
clx += gridClx(value)
|
||||
defaultSet = true
|
||||
}
|
||||
else {
|
||||
clx += gridClx(value, `${key}:`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let getMutatorCellClx = (ignore: number): string => ('')
|
||||
if (mutator?.getCellClx) {
|
||||
const colCount = (typeof grid.at.md! === 'number') ? grid.at.md! as number : grid.at.md!.columns
|
||||
const cellCount = cells?.length
|
||||
if (!colCount || !cellCount) {
|
||||
throw new Error('GridBlockComponent: using mutator, but colCount and / or cellCount is invalid!')
|
||||
}
|
||||
getMutatorCellClx = (cellIndex: number) => ( mutator!.getCellClx!(cellIndex, cellCount, colCount) )
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-2 md:gap-4 xl:gap-6', clx, (mutator?.gapClx ?? ''), className)}>
|
||||
{React.Children.count(children) > 0 ? children : cells?.map((cell, index) => (
|
||||
<Content blocks={cell} agent={agent} key={`cell-${index}`} className={getMutatorCellClx(index)}/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GridBlockComponent
|
||||
@@ -0,0 +1,10 @@
|
||||
import type GridBlockMutator from './grid-block-mutator'
|
||||
|
||||
import tableBorders from './table-borders.mutator'
|
||||
|
||||
const map = new Map<string, GridBlockMutator>()
|
||||
map.set('style-table-borders', tableBorders)
|
||||
|
||||
// get
|
||||
export default (key: string): GridBlockMutator | undefined => (map.get(key))
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type GridBlockMutator from './grid-block-mutator'
|
||||
|
||||
const getCellClx = (
|
||||
cellIndex: number,
|
||||
cellCount: number,
|
||||
colCount: number
|
||||
): string => {
|
||||
|
||||
const isFirstRow = (): boolean => (cellIndex < colCount)
|
||||
const isFirstColumn = (): boolean => (cellIndex % colCount === 0)
|
||||
const isTLCorner = (): boolean => (cellIndex === 0)
|
||||
const isTRCorner = (): boolean => (cellIndex === colCount -1 )
|
||||
const isBLCorner = (): boolean => (cellIndex === cellCount - colCount)
|
||||
const isBRCorner = (): boolean => (cellIndex === cellCount - 1)
|
||||
|
||||
// all get a right and bottom border and padding,
|
||||
// other borders are a special base
|
||||
let clx = 'border-b md:border-r p-4 md:p-8 lg:p-12 '
|
||||
if (isFirstRow()) {
|
||||
clx += 'border-t '
|
||||
}
|
||||
if (isFirstColumn()) {
|
||||
clx += 'md:border-l '
|
||||
}
|
||||
// on mobile this will be the first (top) cell
|
||||
if (isTLCorner()) {
|
||||
clx += 'border-t md:rounded-tl-lg '
|
||||
}
|
||||
else if (isTRCorner()) {
|
||||
clx += 'md:rounded-tr-lg '
|
||||
}
|
||||
else if (isBLCorner()) {
|
||||
clx += 'md:rounded-bl-lg '
|
||||
}
|
||||
else if (isBRCorner()) {
|
||||
clx += 'md:rounded-br-lg '
|
||||
}
|
||||
|
||||
return clx
|
||||
}
|
||||
|
||||
const gapClx = 'gap-0 md:gap-0 xl:gap-9'
|
||||
|
||||
export default {
|
||||
getCellClx,
|
||||
gapClx
|
||||
} as GridBlockMutator
|
||||
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import type { ImageBlock } from '../def'
|
||||
import { getConstaintsClx, containsToken, cn } from '../../util'
|
||||
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
|
||||
const ImageBlockComponent: React.FC<BlockComponentProps> = ({
|
||||
block,
|
||||
className='',
|
||||
agent
|
||||
}) => {
|
||||
|
||||
if (block.blockType !== 'image') {
|
||||
return <>image block required</>
|
||||
}
|
||||
|
||||
const {
|
||||
src,
|
||||
alt,
|
||||
dim,
|
||||
constraints,
|
||||
props,
|
||||
fullWidthOnMobile,
|
||||
svgFillClass,
|
||||
specifiers
|
||||
} = block as ImageBlock
|
||||
|
||||
const specified = (s: string): boolean => (containsToken(specifiers, s))
|
||||
|
||||
const toSpread: any = {}
|
||||
if (props?.fill === undefined) {
|
||||
toSpread.width = dim.w
|
||||
toSpread.height = dim.h
|
||||
}
|
||||
|
||||
let _alt: string
|
||||
if (alt) {
|
||||
_alt = alt
|
||||
}
|
||||
else {
|
||||
const tokens = src.split('/')
|
||||
// Something remotely meaningful
|
||||
_alt = (tokens.length > 0) ? tokens[tokens.length] : src
|
||||
}
|
||||
|
||||
const _svgFillClass = svgFillClass ?? ''
|
||||
|
||||
let constraintsClx = ''
|
||||
|
||||
// TODO: use two elements with 'md:hidden' for 3/4 size
|
||||
// https://nextjs.org/docs/app/building-your-application/optimizing/images#responsive
|
||||
if (agent === 'phone' ) {
|
||||
if (specified('mobile-full-width') || fullWidthOnMobile) {
|
||||
const toSpread: any = {
|
||||
style: {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
},
|
||||
sizes: '100vw',
|
||||
}
|
||||
// only for aspect ratio and to satisfy parser
|
||||
toSpread.width = dim.w
|
||||
toSpread.height = dim.h
|
||||
|
||||
return (
|
||||
<div className='flex flex-col items-center w-full'>
|
||||
<Image src={src} alt={_alt} {...toSpread} className={cn(_svgFillClass, className)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else if (constraints && ('xs' in constraints || 'w' in constraints)) {
|
||||
constraintsClx = getConstaintsClx(constraints, 'xs')
|
||||
}
|
||||
// last stop-gap
|
||||
else if (!specified('mobile-no-scale')) {
|
||||
if (props?.style?.width === 'auto' && typeof props.style.height === 'number' ) {
|
||||
props.style.height = props.style.height *.75
|
||||
}
|
||||
else if (props?.style?.height === 'auto' && typeof props?.style?.width === 'number' ) {
|
||||
props.style.width = props.style.width *.75
|
||||
}
|
||||
else if (props?.style && !props?.style.width) {
|
||||
if (dim) {
|
||||
toSpread.width = +dim.w * .75
|
||||
toSpread.height = +dim.h * .75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
constraintsClx = getConstaintsClx(constraints)
|
||||
}
|
||||
|
||||
const stopGapClx = constraintsClx ? 'mx-auto ' : ''
|
||||
|
||||
const right = containsToken(specifiers, 'right')
|
||||
const center = containsToken(specifiers, 'center')
|
||||
|
||||
const alignSelfClx = right ? 'self-end' : (center ? 'self-center' : 'self-start')
|
||||
|
||||
return (props?.fill) ? (
|
||||
<div className='relative w-full h-full'>
|
||||
<Image
|
||||
src={src}
|
||||
alt={_alt}
|
||||
{...toSpread}
|
||||
{...props}
|
||||
className={cn(_svgFillClass, stopGapClx, constraintsClx, className)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
src={src}
|
||||
alt={_alt}
|
||||
{...toSpread}
|
||||
{...props}
|
||||
className={cn(alignSelfClx, _svgFillClass, stopGapClx, constraintsClx, className)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageBlockComponent
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import AccordianBlock from './accordian-block'
|
||||
import { default as ContentComponent, registerBlockType} from './content'
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
import Blocks from './content'
|
||||
import CardBlock from './card-block'
|
||||
import { default as ContentComponent, registerBlockType} from './content'
|
||||
import CTABlock from './cta-block'
|
||||
import EnhHeadingBlock from './enh-heading-block'
|
||||
import GroupBlock from './group-block'
|
||||
import HeadingBlock from './heading-block'
|
||||
import ImageBlock from './image-block'
|
||||
import VideoBlock from './video-block'
|
||||
import SpaceBlock from './space-block'
|
||||
import ScreenfulBlock from './screenful-block'
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
import VideoBlock from './video-block'
|
||||
|
||||
export {
|
||||
AccordianBlock as AccordianBlockComponent,
|
||||
ContentComponent,
|
||||
registerBlockType,
|
||||
type BlockComponentProps,
|
||||
Blocks as BlocksComponent,
|
||||
CardBlock as CardBlockComponent,
|
||||
ContentComponent,
|
||||
CTABlock as CTABlockComponent,
|
||||
EnhHeadingBlock as EnhHeadingBlockComponent,
|
||||
GroupBlock as GroupBlockComponent,
|
||||
HeadingBlock as HeadingBlockComponent,
|
||||
ImageBlock as ImageBlockComponent,
|
||||
VideoBlock as VideoBlockComponent,
|
||||
registerBlockType,
|
||||
SpaceBlock as SpaceBlockComponent,
|
||||
ScreenfulBlock as ScreenfulBlockComponent,
|
||||
type BlockComponentProps
|
||||
VideoBlock as VideoBlockComponent,
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { type PropsWithChildren } from 'react'
|
||||
|
||||
import type { Block, ScreenfulBlock} from '../../def'
|
||||
import { containsToken, cn } from '../../../util'
|
||||
@@ -13,7 +13,7 @@ const ContentColumn: React.FC<{
|
||||
blocks,
|
||||
specifiers,
|
||||
agent,
|
||||
className=''
|
||||
className='',
|
||||
}) => {
|
||||
|
||||
const specified = (s: string) => (containsToken(specifiers, s))
|
||||
@@ -41,6 +41,9 @@ const ContentColumn: React.FC<{
|
||||
if (agent !== 'phone') {
|
||||
if (specified('bottom')) {
|
||||
modifiers += 'justify-end '
|
||||
}
|
||||
else if (specified('vert-center')) {
|
||||
modifiers += 'justify-center '
|
||||
}
|
||||
// default to top
|
||||
else {
|
||||
@@ -76,47 +79,26 @@ const Content: React.FC<{
|
||||
}> = ({
|
||||
block: b,
|
||||
agent,
|
||||
className=''
|
||||
className='',
|
||||
}) => {
|
||||
|
||||
const specified = (s: string) => (containsToken(b.specifiers, s))
|
||||
const narrowGutters = specified('narrow-gutters') // eg, for a table object that is large
|
||||
|
||||
let outerClasses = (narrowGutters ?
|
||||
// 44 + 24 = 68, 80 + 32 = 104
|
||||
'px-6 lg:px-8 2xl:px-2 pb-6 pt-[68px] md:pt-[104px] lg:pt-[112px] '
|
||||
:
|
||||
'px-[8vw] xl:px-[1vw] pb-[8vh] pt-[calc(44px+8vh)] md:pt-[calc(80px+8vh)] ')
|
||||
// + 'border border-accent ' // debug
|
||||
+ ' overflow-y-hidden' // safety valve
|
||||
|
||||
let moreModifiers = ''
|
||||
// 40px + 32px
|
||||
if (agent && agent !== 'desktop') {
|
||||
moreModifiers += 'pt-[72px] pb-0 px-4 '
|
||||
}
|
||||
|
||||
const multiColumnLayoutClx = (agent === 'phone') ?
|
||||
'flex flex-col gap-6'
|
||||
:
|
||||
`grid grid-cols-${b.contentColumns.length} gap-8`
|
||||
const layoutClx = 'flex flex-col gap-6 ' + ((agent !== 'phone') ? ('md:grid md:gap-8 ' + `md:grid-cols-${b.contentColumns.length} `) : '')
|
||||
|
||||
const orderclx = (columnIndex: number): string => {
|
||||
if ((agent !== 'phone') || !b.mobileOrder || !b.mobileOrder.includes(columnIndex)) return ''
|
||||
const orderIndex = b.mobileOrder.indexOf(columnIndex)
|
||||
return (orderIndex >= 0) ? `order-${orderIndex}` : '' // one-based in flexbox slec
|
||||
const orderIndex = b.mobileOrder?.indexOf(columnIndex)
|
||||
return (orderIndex && orderIndex >= 0) ? `order-${orderIndex} md:order-none` : '' // one-based in flexbox slec
|
||||
}
|
||||
|
||||
|
||||
return b.contentColumns.length == 1 ? (
|
||||
<ContentColumn
|
||||
blocks={b.contentColumns[0]}
|
||||
specifiers={b.columnSpecifiers?.[0]}
|
||||
agent={agent}
|
||||
className={cn(outerClasses, moreModifiers, className)}
|
||||
className={cn(className)}
|
||||
/>
|
||||
) : (
|
||||
<div className={cn(multiColumnLayoutClx, outerClasses, moreModifiers, className)}>
|
||||
<div className={cn(layoutClx, className)}>
|
||||
{b.contentColumns.map((column, index) => (
|
||||
<ContentColumn
|
||||
blocks={column}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
import type { Block, ScreenfulBlock, VideoBlock } from '../../def'
|
||||
import { cn } from '../../../util'
|
||||
import { containsToken, cn } from '../../../util'
|
||||
import { ApplyTypography } from '../../../primitives'
|
||||
|
||||
import Poster from './poster-background'
|
||||
@@ -30,9 +30,29 @@ const ScreenfulComponent: React.FC<{
|
||||
|
||||
const hasBannerVideo = (): boolean => (!!b.banner && (typeof b.banner === 'object'))
|
||||
|
||||
const contentclx = 'z-10 absolute left-0 right-0 top-0 bottom-0 xl:mx-auto max-w-screen-xl'
|
||||
const tileHeight = (agent === 'desktop') ? 'h-full ' : 'h-[100svh] '
|
||||
|
||||
const specified = (s: string) => (containsToken(b.specifiers, s))
|
||||
const narrowGutters = specified('narrow-gutters') // eg, for a table object that is large
|
||||
|
||||
// content wrapper clx:
|
||||
// [
|
||||
// positioning,
|
||||
// p&m,
|
||||
// p&m-modifiers
|
||||
// ]
|
||||
const cwclx = [
|
||||
'z-10 absolute left-0 right-0 top-0 bottom-0 xl:mx-auto max-w-screen-xl overflow-y-hidden ',
|
||||
// desktop header: 80px / pt-20
|
||||
// mobile header: 44px / pt-11
|
||||
narrowGutters ?
|
||||
'px-6 lg:px-8 2xl:px-2 pb-6 pt-15 md:pt-26 lg:pt-28 '
|
||||
:
|
||||
'px-[8vw] xl:px-[1vw] pb-[8vh] pt-[calc(44px+3vh)] md:pt-[calc(80px+6vh)] ',
|
||||
|
||||
(agent && agent !== 'desktop') ? 'pt-15 sm:pt-17 pb-0 px-3 sm:px-8' : ''
|
||||
]
|
||||
|
||||
return (
|
||||
<section className={cn('h-[100vh]', (snapTile ? 'snap-start snap-always' : ''), className)}>
|
||||
<ApplyTypography className={tileHeight + 'w-full flex flex-row justify-center self-stretch'} >
|
||||
@@ -44,7 +64,10 @@ const ScreenfulComponent: React.FC<{
|
||||
initialInView={initialInView}
|
||||
/>
|
||||
)}
|
||||
<Content block={b} agent={agent} className={contentclx} />
|
||||
<div className={cn(...cwclx)} >
|
||||
<Content block={b} agent={agent} className='w-full'/>
|
||||
{b.footer}
|
||||
</div>
|
||||
</Poster>
|
||||
</ApplyTypography>
|
||||
</section>
|
||||
@@ -25,7 +25,7 @@ const Poster: React.FC<{
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn('bg-transparent h-full v-full relative', className)}>
|
||||
<div className={cn('bg-transparent h-full w-full relative', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
|
||||
import { ldMerge } from '../../util'
|
||||
|
||||
import type { Breakpoint } from '../../types'
|
||||
import { SPACE_DEFAULTS , type TWSpaceUnit, type HeadingLevel} from '../def/space-block'
|
||||
import type SpaceBlock from '../def/space-block'
|
||||
import { ApplyTypography } from '../../primitives'
|
||||
|
||||
import type BlockComponentProps from './block-component-props'
|
||||
|
||||
const TAGS = [
|
||||
'div',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
] satisfies React.ElementType[]
|
||||
|
||||
const SpaceBlockComponent: React.FC<BlockComponentProps> = ({
|
||||
block,
|
||||
className=''
|
||||
}) => {
|
||||
|
||||
if (block && block.blockType !== 'space') {
|
||||
return <>space block required</>
|
||||
}
|
||||
|
||||
const b = block as SpaceBlock
|
||||
|
||||
// This code path should handle a undefined or empty sizes field.
|
||||
if (!b.level) {
|
||||
if (typeof b.sizes == 'number') {
|
||||
return <div className={`invisible w-[1px] h-${b.sizes}` } />
|
||||
}
|
||||
const _sizes: {
|
||||
[key in (Breakpoint)]?: TWSpaceUnit
|
||||
} = {}
|
||||
ldMerge(_sizes, SPACE_DEFAULTS, b.sizes)
|
||||
|
||||
let clx = ''
|
||||
for (const [key, value] of Object.entries(_sizes as typeof _sizes)) {
|
||||
clx += `${key}:h-${value} `
|
||||
}
|
||||
|
||||
if (b.test) {
|
||||
console.log(clx)
|
||||
}
|
||||
|
||||
return <div className={'invisible w-[1px] ' + clx} />
|
||||
}
|
||||
|
||||
const Tag = TAGS[b.level]
|
||||
const heightClx = (b.level === (0 satisfies HeadingLevel as HeadingLevel)) ? 'h-4' : ''
|
||||
|
||||
return (
|
||||
<ApplyTypography>
|
||||
<Tag className={'invisible m-0 ' + heightClx} > </Tag>
|
||||
</ApplyTypography>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpaceBlockComponent
|
||||
@@ -4,7 +4,7 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import type { Dimensions, TShirtSize, TShirtDimensions } from '../../types'
|
||||
import { constrain, asNum } from '../../util'
|
||||
import { constrain, asNum, cn } from '../../util'
|
||||
import type { VideoBlock } from '../def'
|
||||
import { VideoPlayer } from '../../primitives'
|
||||
|
||||
@@ -17,6 +17,7 @@ const VideoBlockComponent: React.FC<BlockComponentProps & {
|
||||
}> = ({
|
||||
block,
|
||||
className='',
|
||||
agent,
|
||||
usePoster=false,
|
||||
size='md',
|
||||
constraint
|
||||
@@ -34,7 +35,7 @@ const VideoBlockComponent: React.FC<BlockComponentProps & {
|
||||
const windowDefined = typeof window !== 'undefined'
|
||||
|
||||
useEffect(() => {
|
||||
if (window) {
|
||||
if (window && agent === 'desktop') {
|
||||
window.addEventListener('resize', onResize)
|
||||
return () => window.removeEventListener('resize', onResize)
|
||||
}
|
||||
@@ -50,8 +51,42 @@ const VideoBlockComponent: React.FC<BlockComponentProps & {
|
||||
}
|
||||
|
||||
const b = block as VideoBlock
|
||||
if (b.sizing?.vh) {
|
||||
const ar = asNum(b.dim.md.w) / asNum(b.dim.md.h)
|
||||
const ar = asNum(b.dim.md.w) / asNum(b.dim.md.h)
|
||||
if (agent === 'phone') {
|
||||
if (b.sizing?.mobile?.vw) {
|
||||
// serverside, or at least while the video is loading,
|
||||
// generate the css for the correctly sized poster image
|
||||
if (!_dim) {
|
||||
const width = `${b.sizing.mobile.vw}vw`
|
||||
return <div className='dummy-div' style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '100%',
|
||||
width,
|
||||
height: `calc(${width}/${ar})`,
|
||||
backgroundImage: `url(${b.poster!})`,
|
||||
backgroundSize: 'contain',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
}} />
|
||||
}
|
||||
else {
|
||||
const width = ((b.sizing.mobile.vw / 100) * asNum(_dim.w))
|
||||
const dim = {
|
||||
h: width / ar,
|
||||
w: width
|
||||
}
|
||||
return ((
|
||||
<VideoPlayer
|
||||
className={cn('mx-auto', className)}
|
||||
sources={b.sources}
|
||||
width={dim.w}
|
||||
height={dim.h}
|
||||
{...b.videoProps}
|
||||
/>
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (b.sizing?.vh) {
|
||||
// serverside, generate the css for the correctly sized poster image
|
||||
if (!_dim) {
|
||||
const height = `${b.sizing.vh}vh`
|
||||
@@ -0,0 +1,20 @@
|
||||
import type Block from './block'
|
||||
|
||||
import type { BulletItem, GridDef } from '../../types'
|
||||
|
||||
interface BulletCardsBlock extends Block {
|
||||
blockType: 'bullet-cards'
|
||||
/**
|
||||
* borders-muted-1 / borders-muted-3
|
||||
* default: 2
|
||||
*/
|
||||
specifiers?: string
|
||||
grid: GridDef
|
||||
cards: BulletItem[]
|
||||
/** in px */
|
||||
iconSize?: number
|
||||
}
|
||||
|
||||
export {
|
||||
type BulletCardsBlock as default
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import type Block from './block'
|
||||
import type CTABlock from './cta-block'
|
||||
import type EnhHeadingBlock from './enh-heading-block'
|
||||
|
||||
interface CarteBlancheBlock extends Block {
|
||||
blockType: 'carte-blanche'
|
||||
// big-padding
|
||||
// no-internal-borders
|
||||
// style-ghost (no-internal-borders, no outer border, no padding)
|
||||
specifiers?: string
|
||||
topContent?: Block[]
|
||||
heading?: EnhHeadingBlock
|
||||
content?: Block[]
|
||||
cta?: CTABlock
|
||||
}
|
||||
|
||||
export {
|
||||
type CarteBlancheBlock as default
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { LinkDef, ButtonDef } from '../../types'
|
||||
import type Block from './block'
|
||||
|
||||
interface CTABlock extends Block {
|
||||
blockType: 'cta'
|
||||
// fill: fills the parent width with the elements
|
||||
// left / right: (>= md) left or right justify the elements (default is center)
|
||||
// mobile-2-columns: mobile defaults to rendering each element full width,
|
||||
// on it's own line. This renders them in two columns instead.
|
||||
// mobile-center-first-if-odd: if (mobile-2-columns) and length is odd,
|
||||
// default is to center last
|
||||
// mobile-odd-full-width: fills the full two columns w the centered element
|
||||
specifiers?: string
|
||||
elements: (LinkDef | ButtonDef)[]
|
||||
}
|
||||
|
||||
export {
|
||||
type CTABlock as default
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type Icon from '../../types/icon'
|
||||
import type Block from './block'
|
||||
|
||||
// level is heading tag level. 1 renders as <h1>, etc... 0 renders as <p>
|
||||
//
|
||||
// mb: is bottom margin in tw units. 'mb-1' produces 0.25rem, so a value of 4 would produce 1rem
|
||||
// These are dynamically generated from a template string. mb-1 --> mb-12 have been safelisted.
|
||||
// Any higher values used would have to explicitly safelisted in tailwind config.
|
||||
// The margin will only be applied if the next element down is present.
|
||||
// (eg, heading mb applies only if there is a byline.)
|
||||
|
||||
interface EnhHeadingBlock extends Block {
|
||||
blockType: 'enh-heading'
|
||||
// tbd: icon-w-heading (if preheading is present),
|
||||
// tbd: icon-w-byline (if preheading and heading is present),
|
||||
// tbd: icon-above: above first element
|
||||
// tbd: icon-to-right: to right of corresponding element or right justified if 'icon-above'
|
||||
// mobile-heading-centered
|
||||
// left / right / center for preheading and heading (byline stays left)
|
||||
// byline-left / byline-right / byline-center
|
||||
// preheading-heading-font
|
||||
specifiers?: string
|
||||
// By default, appears inline to left of first element (preheading or heading)
|
||||
// unless indicated otherwise in specifiers
|
||||
icon?: Icon | string // ReactNode or url string to asset
|
||||
iconSize?: number // if url string, this sets the size
|
||||
preheading?: {
|
||||
text: string
|
||||
level?: number // default: 4
|
||||
mb?: number // default: 2
|
||||
}
|
||||
heading: {
|
||||
text: string
|
||||
level?: number // default: 1
|
||||
mb?: number // default: 2
|
||||
}
|
||||
byline?: {
|
||||
text: string
|
||||
level?: number // default: 6
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
type EnhHeadingBlock as default
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type Block from "./block"
|
||||
import type GridDef from "../../types/grid-def"
|
||||
|
||||
interface GridBlock {
|
||||
blockType: 'grid'
|
||||
specifiers?: string
|
||||
grid: GridDef
|
||||
/**
|
||||
* Ignored if children are supplied to the GridBlockComp
|
||||
*/
|
||||
cells?: Block[]
|
||||
}
|
||||
|
||||
export {
|
||||
type GridBlock as default
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ImageDef } from '../../types'
|
||||
import type Block from './block'
|
||||
|
||||
/**
|
||||
* See {@link ImageDef}
|
||||
* see https://nextjs.org/docs/app/api-reference/components/image
|
||||
* as well as React.ImgHTMLAttributes.
|
||||
*/
|
||||
interface ImageBlock extends Block, ImageDef {
|
||||
blockType: 'image'
|
||||
|
||||
/**
|
||||
* Alignement: 'left' (default) / 'right' / 'center' (must be in flex-col parent)
|
||||
* 'mobile-no-scale': By default, scales to 3/4 height (mobile and w < 'md')
|
||||
* 'mobile-full-width': Overrides dim, etc. and renders full width (maintaining aspect ratio)
|
||||
*/
|
||||
specifiers?: string
|
||||
/** @deprecated Please use 'mobile-full-width' in specifiers */
|
||||
fullWidthOnMobile?: boolean
|
||||
/** Next props */
|
||||
props?: {
|
||||
sizes?: string
|
||||
/** if true, any alignement specifiers are ignored */
|
||||
fill?: boolean
|
||||
style?: {
|
||||
objectFit?: string
|
||||
objectPosition?: string
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
type ImageBlock as default,
|
||||
}
|
||||
@@ -1,25 +1,35 @@
|
||||
import type AccordianBlock from './accordian-block'
|
||||
import type Block from './block'
|
||||
import type BulletCardsBlock from './bullet-cards-block'
|
||||
import type CardBlock from './card-block'
|
||||
import type CarteBlancheBlock from './carte-blanche-block'
|
||||
import type CTABlock from './cta-block'
|
||||
import type ElementBlock from './element-block'
|
||||
import type GridBlock from './grid-block'
|
||||
import type GroupBlock from './group-block'
|
||||
import type EnhHeadingBlock from './enh-heading-block'
|
||||
import type HeadingBlock from './heading-block'
|
||||
import type ImageBlock from './image-block'
|
||||
import type VideoBlock from './video-block'
|
||||
import type SpaceBlock from './space-block'
|
||||
import { SPACE_DEFAULTS } from './space-block'
|
||||
import type ScreenfulBlock from './screenful-block'
|
||||
|
||||
export {
|
||||
type AccordianBlock,
|
||||
type Block,
|
||||
type BulletCardsBlock,
|
||||
type CardBlock,
|
||||
type CarteBlancheBlock,
|
||||
type CTABlock,
|
||||
type ElementBlock,
|
||||
type GridBlock,
|
||||
type GroupBlock,
|
||||
type HeadingBlock,
|
||||
type EnhHeadingBlock,
|
||||
type ImageBlock,
|
||||
type VideoBlock,
|
||||
type SpaceBlock,
|
||||
type ScreenfulBlock,
|
||||
SPACE_DEFAULTS
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
import type Block from './block'
|
||||
import type VideoBlock from './video-block'
|
||||
|
||||
/**
|
||||
* A common screenful of content
|
||||
* An optional banner image or video
|
||||
* Content can be in columns
|
||||
*/
|
||||
interface ScreenfulBlock extends Block {
|
||||
blockType: 'screenful'
|
||||
|
||||
|
||||
/**
|
||||
* Either an image URL, or a Video Block
|
||||
*
|
||||
* If VideoBlock, then it's poster will be rendered server-side
|
||||
* and the <video> component will be lazy-loaded client-side.
|
||||
*
|
||||
* If in a scrolling situation, the video will autoplay when 75% in view
|
||||
*/
|
||||
banner?: string | VideoBlock
|
||||
|
||||
/** Specifies rendering and layout hints and variants for block as a whole */
|
||||
specifiers?: string
|
||||
|
||||
/** Specifies rendering and layout hints and variants for corresponding column */
|
||||
columnSpecifiers?: string[]
|
||||
|
||||
/**
|
||||
* Mobile: The order in the single column mobile layout in which the columns appear.
|
||||
* Overrides column order.
|
||||
* eg, if you want the second tile of three at the top: [1, 0, 2]
|
||||
*/
|
||||
mobileOrder?: number[]
|
||||
|
||||
/**
|
||||
* Content Blocks for 1-3 columns.
|
||||
* (More than that is allowed, but impractical at many resolutions!)
|
||||
* If > 1, it will be enclosed in a 'grid grid-cols-<x>' div.
|
||||
*/
|
||||
contentColumns: Block[][]
|
||||
|
||||
/** optional footer element below the grid */
|
||||
footer?: ReactNode
|
||||
}
|
||||
|
||||
export {
|
||||
type ScreenfulBlock as default
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Breakpoint } from '../../types'
|
||||
|
||||
import type Block from './block'
|
||||
|
||||
type TWSpaceUnit = number // TODO, pull from tw conf
|
||||
type HeadingLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
|
||||
const SPACE_DEFAULTS = {
|
||||
xs: 2,
|
||||
sm: 4,
|
||||
md: 5,
|
||||
lg: 6,
|
||||
xl: 8
|
||||
} satisfies {
|
||||
[key in (Breakpoint)]?: TWSpaceUnit
|
||||
}
|
||||
|
||||
|
||||
interface SpaceBlock extends Block {
|
||||
blockType: 'space'
|
||||
|
||||
/**
|
||||
* TW units of vertical space, applied at Breakpoints
|
||||
* or
|
||||
* if just a number, that number at all Breakpoints
|
||||
*
|
||||
* default {
|
||||
* xs: 2,
|
||||
* sm: 4,
|
||||
* md: 5,
|
||||
* lg: 6,
|
||||
* xl: 8
|
||||
* }
|
||||
*
|
||||
* Any provided values will be merge w the defaults
|
||||
* And applied as if they were tw classes in ascending
|
||||
* order.
|
||||
*
|
||||
* impl: <div className='invisible w-[1px] xs:h-<xsval> sm:h-<smval> etc...' />
|
||||
*/
|
||||
sizes?: {
|
||||
[key in (Breakpoint)]?: TWSpaceUnit
|
||||
} | TWSpaceUnit
|
||||
|
||||
/**
|
||||
* Heading levels. Gives the vertical space that the corresponding
|
||||
* h tag would give.
|
||||
* default: 3 (height of <h3>)
|
||||
* 0 = 1rem (plus any gaps),
|
||||
* For example, 1 = <h1 style={visibility: hidden}> </h1>
|
||||
* As <ApplyTypography> would render it, plus any gap.
|
||||
*/
|
||||
level?: HeadingLevel
|
||||
|
||||
test?: boolean
|
||||
|
||||
}
|
||||
|
||||
export {
|
||||
type SpaceBlock as default,
|
||||
type TWSpaceUnit,
|
||||
type HeadingLevel,
|
||||
SPACE_DEFAULTS
|
||||
}
|
||||
@@ -19,6 +19,7 @@ interface VideoBlock extends Block {
|
||||
poster?: string
|
||||
sources?: string[]
|
||||
dim: TShirtDimensions
|
||||
// These are suppored so far: { vh: 60, mobile: {vw: 70} }
|
||||
sizing?: any
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import LuxLogo from './icons/lux-logo'
|
||||
import { Button, Card } from '../primitives'
|
||||
|
||||
const ChatWidget: React.FC<{
|
||||
title: string,
|
||||
chatbotUrl: string,
|
||||
subtitle?: string,
|
||||
question?: string,
|
||||
/*
|
||||
Currently supports these icons from remix icons (https://remixicon.com/):
|
||||
GlobalLineIcon,
|
||||
ShieldFlashLineIcon,
|
||||
BankCardLineIcon,
|
||||
GroupLineIcon,
|
||||
QuestionnaireLineIcon
|
||||
*/
|
||||
suggestedQuestions?: { heading: string, message: string, icon?: string }[]
|
||||
}> = ({
|
||||
title,
|
||||
chatbotUrl,
|
||||
subtitle,
|
||||
question,
|
||||
suggestedQuestions
|
||||
}) => {
|
||||
|
||||
const [showChatbot, setShowChatbot] = React.useState<boolean>(false)
|
||||
|
||||
const onClick = () => { setShowChatbot(!showChatbot) }
|
||||
|
||||
const searchParams = new URLSearchParams()
|
||||
if (question) {
|
||||
searchParams.append('question', question)
|
||||
}
|
||||
if (suggestedQuestions) {
|
||||
searchParams.append('sQuestions', suggestedQuestions.map(({ message }) => message).join(','))
|
||||
searchParams.append('sHeadings', suggestedQuestions.map(({ heading }) => heading).join(','))
|
||||
searchParams.append('sIcons', suggestedQuestions.map(({ icon }) => icon).join(','))
|
||||
}
|
||||
|
||||
const iframeSrc = `${chatbotUrl}?${searchParams.toString()}`
|
||||
|
||||
return (<>
|
||||
<div className={
|
||||
'fixed bottom-0 sm:bottom-20 right-0 w-full h-full ' +
|
||||
'sm:max-w-[400px] sm:max-h-[550px] sm:px-4 z-[1001] ' +
|
||||
(showChatbot ? 'flex' : 'hidden')
|
||||
}>
|
||||
<Card className='flex flex-col h-full w-full'>
|
||||
<div className='flex px-4 py-2 h-12 bg-level-0 items-center justify-between'>
|
||||
<h3 className='font-semibold font-heading'>{title} <span className='opacity-60'>{subtitle}</span></h3>
|
||||
<Button onClick={onClick} variant='link' size='icon' className='w-fit sm:hidden'>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
<iframe src={iframeSrc} className='h-full' />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
size='link'
|
||||
onClick={onClick}
|
||||
className='fixed bottom-4 right-0 h-12 w-12 mx-4 rounded-full'
|
||||
>
|
||||
{showChatbot ? <X /> : <LuxLogo width={24} height={24} className='mt-2' />}
|
||||
</Button>
|
||||
</>)
|
||||
}
|
||||
|
||||
export default ChatWidget
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
const FIRST = 2020
|
||||
|
||||
const Copyright: React.FC<{
|
||||
className?: string
|
||||
}> = ({
|
||||
className=''
|
||||
}) => {
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
const yearString = (year > FIRST) ? `${FIRST} - ${year}` : FIRST.toString()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{`Copyright © ${yearString}`} <br className='sm:hidden'/>Lux Partners Ltd. <br className='md:hidden'/> All rights reserved.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Copyright
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { LinkDef, SiteDef } from '../types'
|
||||
import { Copyright } from '.'
|
||||
import { NavItems } from '../primitives'
|
||||
import { legal } from '../siteDef/footer/legal'
|
||||
|
||||
import Logo from './logo'
|
||||
import { cn } from '../util'
|
||||
|
||||
const Footer: React.FC<{
|
||||
siteDef: SiteDef,
|
||||
className?: string,
|
||||
noHorizPadding?: boolean
|
||||
}> = ({
|
||||
siteDef,
|
||||
className='',
|
||||
noHorizPadding=false
|
||||
}) => {
|
||||
|
||||
const { footer, aboveCopyright } = siteDef
|
||||
const smGridCols = Math.floor(footer.length/2)
|
||||
const smGridColsClx = `sm:grid-cols-${smGridCols} `
|
||||
const _aboveCopyright = (typeof aboveCopyright === 'undefined') ? legal : aboveCopyright
|
||||
|
||||
return (
|
||||
<footer className={cn('grow flex flex-col justify-between gap-6 pb-[2vh]', className)}>
|
||||
<div className={
|
||||
(noHorizPadding ? '' : 'px-5 md:px-8 ') +
|
||||
'grid grid-cols-2 gap-4 gap-y-6 md:gap-x-6 lg:gap-8 ' + smGridColsClx +
|
||||
'md:w-full sm:justify-items-center md:mx-0 lg:w-full max-w-screen-2xl ' +
|
||||
'md:flex md:flex-row md:justify-between '
|
||||
}>
|
||||
<div className='hidden lg:flex flex-col' key={0}>
|
||||
<Logo size='md' />
|
||||
</div>
|
||||
{footer.map((defs: LinkDef[], index) => {
|
||||
|
||||
const xsColSpanClx = ((index === footer.length - 1) && (footer.length % 2 === 1)) ?
|
||||
'xs:col-span-2 xs:mx-auto md:col-span-1 md:mx-0 ' : ''
|
||||
|
||||
return (
|
||||
<NavItems
|
||||
items={defs}
|
||||
currentAs={siteDef.currentAs}
|
||||
as='nav'
|
||||
className={cn('sm:min-w-[150px] md:min-w-0 flex flex-col justify-start items-start ' +
|
||||
'gap-[11px] sm:gap-[12px] md:gap-[15px] ',
|
||||
xsColSpanClx
|
||||
)}
|
||||
key={index + 1}
|
||||
itemClx={(def: LinkDef) => ((def.variant === 'linkFG') ?
|
||||
'font-heading text-[15px]/[1.3] font-medium text-foreground tracking-normal'
|
||||
:
|
||||
'text-[15px]/[1.1] font-normal tracking-[0.2px] text-muted-1'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className='md:mt-[2vh]'>
|
||||
{_aboveCopyright.length > 0 && (
|
||||
<NavItems
|
||||
items={_aboveCopyright}
|
||||
as='div'
|
||||
className={'flex flex-row justify-center gap-4 mb-2'}
|
||||
itemClx={'text-sm text-center text-muted-2 underline hover:text-foreground'}
|
||||
/>
|
||||
)}
|
||||
<Copyright className='text-sm text-center text-muted-3'/>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
// flex flex-col justify-between gap-6
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import type { SiteDef } from '../../types'
|
||||
import Logo from '../logo'
|
||||
import { type ButtonVariants } from '../../primitives'
|
||||
import { NavItems, Icons, DrawerMenu } from '../../common'
|
||||
import MobileNav from './mobile-nav'
|
||||
|
||||
import { NavItems } from '../../primitives'
|
||||
import { Icons, DrawerMenu, Logo } from '..'
|
||||
import { cn } from '../../util'
|
||||
|
||||
import MobileNav from './mobile-nav'
|
||||
|
||||
const Header: React.FC<{
|
||||
siteDef: SiteDef
|
||||
className?: string
|
||||
@@ -16,8 +15,11 @@ const Header: React.FC<{
|
||||
className = ''
|
||||
}) => {
|
||||
|
||||
const { nav: {elements, featuredCTA}} = siteDef
|
||||
const allElements = (featuredCTA) ? [...elements, featuredCTA] : elements
|
||||
const { nav: { common, featured }} = siteDef
|
||||
const allElements = (featured) ? [...common, ...featured] : common
|
||||
// TODO move 13px into a size class and configure twMerge to recognize say, 'text-size-nav'
|
||||
// (vs be beat out by 'text-color-nav')
|
||||
const navItemClx = 'font-heading h-8 tracking-[-0.3px] !text-[13px]/[13px]'
|
||||
|
||||
return (
|
||||
<header className={cn('bg-background sticky z-10 top-0 ', className)} >
|
||||
@@ -25,36 +27,26 @@ const Header: React.FC<{
|
||||
<div className="hidden md:flex flex-row md:h-[80px] items-center justify-between px-[32px] 2xl:mx-auto max-w-screen-2xl">
|
||||
<Logo size='md' className='hidden lg:flex' key='two'/>
|
||||
<Logo size='sm' className='hidden md:flex lg:hidden' key='one'/>
|
||||
{/* lg or larger */}
|
||||
{/* md or larger */}
|
||||
<NavItems
|
||||
currentAs={siteDef.currentAs}
|
||||
items={allElements}
|
||||
className='hidden lg:flex justify-between gap-[24px] text-[13px]/[13px] min-w-[690px]'
|
||||
itemClassName='font-heading h-[32px] tracking-[-0.3px]'
|
||||
itemClassNameFromVariant={(variant: ButtonVariants) => (variant === 'primary' ? 'min-w-[174px]' : '')}
|
||||
className='hidden md:flex md:gap-4 lg:justify-between lg:gap-7 '
|
||||
itemClx={navItemClx}
|
||||
key='three'
|
||||
/>
|
||||
{/* md exactly */}
|
||||
<NavItems
|
||||
currentAs={siteDef.currentAs}
|
||||
items={allElements}
|
||||
className='hidden md:flex lg:hidden gap-4 text-[13px]/[13px] '
|
||||
itemClassName='font-heading h-[32px] tracking-[-0.3px]'
|
||||
itemClassNameFromVariant={(variant: ButtonVariants) => (variant === 'primary' ? 'min-w-0 text-[13px]/[13px]' : '')}
|
||||
key='four'
|
||||
/>
|
||||
</div>
|
||||
{/* smaller than md: mobile style drawer menu */}
|
||||
<div className="flex md:hidden h-[44px] items-center justify-between px-2">
|
||||
<Logo size='sm' />
|
||||
<DrawerMenu
|
||||
className='p-0 text-primary' // ui has 'text-inherit', so need this for close buttons to appear.
|
||||
trigger={<Icons.burger className='h-7 w-7 text-inherit'/>}
|
||||
trigger={<Icons.burger className='h-7 w-7'/>}
|
||||
>
|
||||
<MobileNav
|
||||
siteDef={siteDef}
|
||||
className='pt-12'
|
||||
navElementClasses='px-10 text-l h-10 justify-start border-b rounded-none'
|
||||
commonItemClx='px-10 text-l h-10 justify-start border-b rounded-none'
|
||||
/>
|
||||
</DrawerMenu>
|
||||
</div>
|
||||
@@ -2,64 +2,68 @@
|
||||
import React from 'react'
|
||||
|
||||
import { type ButtonVariants, LinkElement } from '../../primitives'
|
||||
import type { SiteDef } from '../../types'
|
||||
import type { LinkDef, SiteDef } from '../../types'
|
||||
import { cn } from '../../util'
|
||||
|
||||
// onAction is *in addition* to the link's navigation
|
||||
// action itself. eg, for also closing the modal menu
|
||||
const MobileNav: React.FC<{
|
||||
siteDef: SiteDef
|
||||
className?: string
|
||||
navElementClasses?: string
|
||||
commonItemClx?: string | ((def: LinkDef) => string),
|
||||
/**
|
||||
* This is called *in addition* to the link's actual navigation
|
||||
* action. eg, I link is clicked, and the modal menu is closes
|
||||
*/
|
||||
onAction?: () => void
|
||||
}> = ({
|
||||
siteDef,
|
||||
onAction,
|
||||
className='',
|
||||
navElementClasses='',
|
||||
commonItemClx,
|
||||
}) => {
|
||||
|
||||
const {nav: {elements, featuredCTA}} = siteDef
|
||||
const { nav: { common, featured }} = siteDef
|
||||
|
||||
return (elements.length || featuredCTA) ? (
|
||||
return (common.length || featured?.length) ? (
|
||||
<nav className={className} >
|
||||
{elements.map((el, index) => {
|
||||
{common.map((el, index) => {
|
||||
const variant = el.variant ?? 'link'
|
||||
let extraClasses = ''
|
||||
let internalClx = ''
|
||||
// note that linkFG (or any other variant of 'link')
|
||||
// will not get assigned these classes,
|
||||
// and will remain styles is 'foreground' (hence the name)
|
||||
if (variant === 'link') {
|
||||
extraClasses+= ' text-nav hover:text-nav-hover active:text-nav-hover '
|
||||
internalClx+= ' text-nav hover:text-nav-hover active:text-nav-hover '
|
||||
if (siteDef.currentAs && siteDef.currentAs === el.href) {
|
||||
extraClasses += ' text-nav-current'
|
||||
internalClx += ' text-nav-current'
|
||||
}
|
||||
}
|
||||
else {
|
||||
extraClasses+= ' min-w-0'
|
||||
internalClx+= ' min-w-0'
|
||||
}
|
||||
if (siteDef.currentAs && siteDef.currentAs === el.href) {
|
||||
extraClasses += ' pointer-events-none'
|
||||
internalClx += ' pointer-events-none'
|
||||
}
|
||||
const itemClx = (commonItemClx) ? (typeof commonItemClx === 'string' ? commonItemClx : commonItemClx(el)) : ''
|
||||
|
||||
return (
|
||||
<LinkElement
|
||||
def={el}
|
||||
key={index}
|
||||
key={`common-${index}`}
|
||||
size='lg'
|
||||
className={cn(navElementClasses, extraClasses)}
|
||||
className={cn(internalClx, itemClx)}
|
||||
onClick = {onAction}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{featuredCTA && (
|
||||
{featured?.length && (featured.map((el, index) => (
|
||||
<LinkElement
|
||||
def={featuredCTA}
|
||||
key='featuredCTA'
|
||||
def={el}
|
||||
key={`featured-${index}`}
|
||||
size='lg'
|
||||
className='mt-6 w-4/5 mx-auto'
|
||||
onClick = {onAction}
|
||||
onClick={onAction}
|
||||
/>
|
||||
)}
|
||||
)))}
|
||||
</nav>
|
||||
)
|
||||
: null
|
||||
@@ -1,14 +1,14 @@
|
||||
export * as Icons from './icons'
|
||||
export { default as ChatWidget } from './chat-widget'
|
||||
export { default as ContactDialog } from './contact-dialog'
|
||||
export { default as Logo } from './logo'
|
||||
export { default as Main } from './main'
|
||||
export { default as Copyright} from './copyright'
|
||||
export { default as DrawerMenu } from './drawer-menu'
|
||||
export { default as Footer } from './footer'
|
||||
export { default as Header } from './header'
|
||||
export { default as DrawerMenu } from './drawer-menu'
|
||||
export {
|
||||
default as HeadMetadata,
|
||||
getTitleFromTemplateString,
|
||||
TwitterComponent
|
||||
} from './head-metadata'
|
||||
export * as Icons from './icons'
|
||||
export { default as Logo } from './logo'
|
||||
export { default as MiniChart } from './mini-chart'
|
||||
export { default as Copyright} from './copyright'
|
||||
export { default as HeadMetadata, getTitleFromTemplateString, TwitterComponent} from './head-metadata'
|
||||
export { default as NavItems} from './nav-items'
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Link from 'next/link'
|
||||
|
||||
import { type TShirtSize } from '../types'
|
||||
import { cn } from '../util'
|
||||
import { Icons } from '../common'
|
||||
import { Icons } from '.'
|
||||
|
||||
const Logo: React.FC<{
|
||||
size?: TShirtSize
|
||||