Compare commits

...
Author SHA1 Message Date
artem ash 8d0425bbbf minor 2024-02-15 07:25:06 -08:00
artem ash e28cd85934 sort of working 2024-02-14 22:50:18 -08:00
artem ash 9f5a9d31af changed nav order to alpha 2024-02-14 17:54:10 -08:00
artem ash cf8f54fe13 readme 2024-02-14 13:28:33 -08:00
Zach KellingandGitHub 21c84cda35 Update README.md 2024-02-14 22:17:38 +01:00
Zach KellingandGitHub 018e9cd911 Update README.md 2024-02-14 22:17:00 +01:00
Artem AshandGitHub 1714bf76ea @0.5.11 (minor): ImageBlock.props.styles is now type any (#13)
initial published as @hanzo/ui v0.5.10
2024-02-14 13:10:03 -08:00
artem ash 1f1d484eee bump 0.5.10 2024-02-13 19:52:36 -08:00
artem ash 0737ddf80a @0.5.9: ts issue in client code solved 2024-02-13 19:25:00 -08:00
Artem AshandGitHub 5b96b360c8 @0.5.3 tailwind conf lives here, etc. (#12)
@0.5.3 tailwind config streamlining, improved SpaceBlock, etc

tailwind/tailwind.config.base now lives HERE and gets imported
  import { config } from '@luxdefi/ui/tailwind' --> goes in local config.presets[]

tailwind/spacing.tailwind.js: fills in missing values
 in tw config.spacing through 40rem at integral units (not every 4)

BulletCardBlockComp: card layout improvements
ScreenfulBlockComp: shifted from explicit px units to tw units for header calcs
SpaceBlock: major improvement and new prefered usage (non breaking) w tw units
 sizes?: { xs: 4, sm: 6, etc...}
2024-02-13 18:28:33 -08:00
artem ash a5eea26642 bump to 0.5.2 and publish 2024-02-13 03:49:12 -08:00
erikrakuscek eb2f482023 chat widget update 2024-02-13 12:07:15 +01:00
artem ash 04e419ba08 fix: scroll slide bug due to body:h-full 2024-02-13 02:53:49 -08:00
artem ash 54a7de8ab2 re previous: updated footer content 2024-02-13 02:34:15 -08:00
artem ash 754c73c98a @0.5.0
types/SiteDef: (breaking) cleaner structure that supports more options
  aboveCopyright: legal links by default
  featureCTA is now featured: LinkDef[]
siteDef/*: exported footer columns individually and as a common set.
  (breaking) new community content requires @svgr/webpack as devDep
  in host app

Main, NavItems: common --> primitives  (breaking)

common/copyright: xs, sm improvements
common/footer:  siteDef.aboveCopyright support, moved 'legal'
  to above copyright by default, some cleanup
common/header/mobile-nav: sideDef.featured array support, cleanup
common/DrawerMenu: opens to w-5/6 on mobile and w-2/3 on sm

primitives/button: minor changes to 'default' and 'lg' width
primitives/table: added as requested

next/root-layout: body tag: added 'flex flex-col h-full' to
  support footer and copyright layout
tailwind/screens.tailwind: sm: 450 --> 480
tailwind/typo-plugin: remove demo dir
2024-02-13 02:24:42 -08:00
artem ash c72a1c09b7 removed min-w constraint on specialCTA 2024-02-12 08:17:47 -08:00
artem ash 254cfc9791 0.4.11 previous 2024-02-11 16:38:26 -08:00
artem ash db24824250 @0.4.10: updates to shared nav and footers; minor fixes 2024-02-11 16:28:41 -08:00
artem ash 8c5a9f32be @0.4.8
BulletCardsBlock: added specifiers; added 'border-muted-1' / 'border-muted-3' to override default of 2
CTABlock, ScreenfulBlock: fixed agent === 'phone' over-specificity
InlineIcons from blocks/components --> primitives
2024-02-11 14:16:54 -08:00
Artem AshandGitHub 42dc6f2d6f @0.4.7: GridBlock enhancements and other good things (#11)
GridBlock enhancements:
  Mutator mechanism for applying classes to each sell and to gap
   based on an arbitrary specifier key
  Supports rendering children

ScreenfulBlock: Added optional footer below grid
ScreenfulBlock: cleanups in structure
Def objects are now in /types (not /blocks/def)

new: types/ImageDef for defining images in blocks without needing to 
  embed another block
  ImageBlock inherits it

fix: cta: mobile: odd num of buttons  
fix: image: mobile: has max-w for "full screen image" option (tablets!)
2024-02-11 00:07:48 -08:00
artem ash 09a94343ce @0.4.6: ts issues 2024-02-09 19:18:03 -08:00
artem ash cd481922d9 (build issue?) bumped to 0.4.2 2024-02-09 18:18:20 -08:00
Artem AshandGitHub 77767dfbea @0.4.1: Requested primitives added (#10)
See linked issue for a complete list and comments
2024-02-09 17:43:37 -08:00
f388383190 @0.4.0: Streamlined and enhanced components (#9)
CartBlancheBlock, a better CardBlock, 
EnhHeadingBlock a better HeadingBlock, 
GridBlock, a more complete and useful GroupBlock 
BulletCardsBlock: common simple case
InlineIcon

ImageBlock now auto scales to 0.75 on mobile
VideoBlock: support for mobile sizing using 'vw'  eg: {mobile: {vw: 60}}

common/ChatWidget (from PR #7)
Removed esbuild step and single entry point 
  (client code must import using subdirs)
Minor cleanup in tsconfig
---------

Co-authored-by: artem ash <artemisprimedev@gmail.com>

---------

Co-authored-by: Erik Rakušček <erik.rakuscek@gmail.com>
2024-02-09 15:34:53 -08:00
222 changed files with 4927 additions and 2346 deletions
-8
View File
@@ -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)
-11
View File
@@ -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"]
}
+3 -3
View File
@@ -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"
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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:
+25 -17
View File
@@ -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.
+4 -4
View File
@@ -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.
![hero](apps/www/public/og.jpg)
## 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
View File
@@ -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
+1 -1
View File
@@ -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>
+3 -3
View File
@@ -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>
+4 -4
View File
@@ -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",
+7 -7
View File
@@ -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",
},
]
+2 -2
View File
@@ -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: {
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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"
}
```
+1 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
{
"name": "@luxdefi/ui-demo-www",
"name": "@hanzoai/ui-demo-www",
"version": "0.0.1",
"private": true,
"scripts": {
View File
-33
View File
@@ -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"
}
+9 -4
View File
@@ -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} >&nbsp;</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
}
+19
View File
@@ -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
}
+16
View File
@@ -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
}
+36
View File
@@ -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
}
+64
View File
@@ -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}>&nbsp;</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
}
+75
View File
@@ -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
+21
View File
@@ -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}`}&nbsp;<br className='sm:hidden'/>Lux Partners Ltd.&nbsp;<br className='md:hidden'/>&nbsp;All rights reserved.
</div>
)
}
export default Copyright
+77
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More