Full Code of hzeyuan/x-cards for AI

master a6c94888a84f cached
125 files
476.8 KB
133.8k tokens
164 symbols
1 requests
Download .txt
Showing preview only (515K chars total). Download the full file or copy to clipboard to get everything.
Repository: hzeyuan/x-cards
Branch: master
Commit: a6c94888a84f
Files: 125
Total size: 476.8 KB

Directory structure:
gitextract_nbnj719d/

├── .github/
│   └── workflows/
│       └── submit.yml
├── .gitignore
├── .prettierrc.mjs
├── LICENSE
├── README.md
├── README_ZH.md
├── components/
│   └── ui/
│       ├── EditableButton.tsx
│       ├── accordion.tsx
│       ├── alert-dialog.tsx
│       ├── badge.tsx
│       ├── button.tsx
│       ├── color-picker.tsx
│       ├── dropdown-menu.tsx
│       ├── input.tsx
│       ├── popover.tsx
│       ├── radio-group.tsx
│       ├── select-position.tsx
│       ├── select.tsx
│       ├── slider.tsx
│       ├── sonner.tsx
│       ├── switch.tsx
│       ├── tabs.tsx
│       ├── textarea.tsx
│       ├── toast.tsx
│       ├── toaster.tsx
│       └── use-toast.ts
├── components.json
├── lib/
│   └── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── popup.tsx
├── postcss.config.js
├── src/
│   ├── app/
│   │   ├── (app)/
│   │   │   ├── components/
│   │   │   │   ├── FrequentlyAskedQuestions.tsx
│   │   │   │   ├── GoogleFontSelector.tsx
│   │   │   │   ├── ImageLayout.tsx
│   │   │   │   ├── LazyLoadAnimatedSection.tsx
│   │   │   │   ├── ResultIcon.tsx
│   │   │   │   ├── card-generator/
│   │   │   │   │   ├── color.tsx
│   │   │   │   │   ├── controller/
│   │   │   │   │   │   ├── background-controller.tsx
│   │   │   │   │   │   ├── card-controller.tsx
│   │   │   │   │   │   ├── font-controller.tsx
│   │   │   │   │   │   ├── iframe-controller.tsx
│   │   │   │   │   │   └── input-controller.tsx
│   │   │   │   │   ├── display.tsx
│   │   │   │   │   ├── export-tab.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── twitter-card.tsx
│   │   │   │   │   └── wechat-card.tsx
│   │   │   │   ├── dynamic-style-tippy.tsx
│   │   │   │   ├── hero.tsx
│   │   │   │   ├── save-as-template-button.tsx
│   │   │   │   ├── sections/
│   │   │   │   │   ├── FeaturesGridSection.tsx
│   │   │   │   │   ├── features2.tsx
│   │   │   │   │   ├── footer.tsx
│   │   │   │   │   └── video.tsx
│   │   │   │   ├── template-list.tsx
│   │   │   │   └── x-form.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   └── request.tsx
│   │   ├── (extension)/
│   │   │   ├── independent/
│   │   │   │   ├── components/
│   │   │   │   │   └── index.tsx
│   │   │   │   └── page.tsx
│   │   │   └── welcome/
│   │   │       └── page.tsx
│   │   ├── api/
│   │   │   ├── license/
│   │   │   │   └── route.ts
│   │   │   └── x/
│   │   │       └── route.ts
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── utils/
│   │       ├── IFrameMessageSystem.ts
│   │       ├── element.ts
│   │       ├── export.ts
│   │       ├── format.ts
│   │       ├── image.ts
│   │       └── index.ts
│   ├── background/
│   │   ├── index.ts
│   │   └── messages/
│   │       ├── code.ts
│   │       └── tweet.ts
│   ├── components/
│   │   ├── extension/
│   │   │   ├── card-button.tsx
│   │   │   ├── input-code.tsx
│   │   │   ├── label-with-icon.tsx
│   │   │   ├── layout-options.tsx
│   │   │   ├── preset-color-list.tsx
│   │   │   ├── tabs.tsx
│   │   │   ├── tweet-manager.tsx
│   │   │   ├── use-tweet-collection.ts
│   │   │   └── x-cards-toast/
│   │   │       ├── font-control.tsx
│   │   │       ├── image-preview.tsx
│   │   │       ├── index.module.css
│   │   │       ├── index.tsx
│   │   │       ├── padding-control.tsx
│   │   │       ├── scale-control.tsx
│   │   │       └── tweet-control.tsx
│   │   ├── sortableList.tsx
│   │   └── ui/
│   │       ├── BentoGrid.tsx
│   │       ├── DotPattern.tsx
│   │       ├── acetenity-tabs.tsx
│   │       ├── animated-list.tsx
│   │       ├── animatedBeam.tsx
│   │       ├── api-key-panel.tsx
│   │       ├── bold-copy.tsx
│   │       ├── burnIn.tsx
│   │       ├── card.tsx
│   │       ├── chart.tsx
│   │       ├── fade-text.tsx
│   │       ├── grid-pattern.tsx
│   │       ├── grid.tsx
│   │       ├── icon.tsx
│   │       ├── loading-spinner.tsx
│   │       ├── marquee.tsx
│   │       ├── scroll-based-velocity.tsx
│   │       ├── shimmer-button.tsx
│   │       ├── text-generate-effect.tsx
│   │       └── underline-hover-text.tsx
│   ├── config/
│   │   └── site.ts
│   ├── contents/
│   │   ├── plasmo-overlay.css
│   │   ├── plasmo-overlay.tsx
│   │   ├── x-home.tsx
│   │   └── x.css
│   ├── hooks/
│   │   ├── useCardStore.tsx
│   │   └── useTemplatesStore.tsx
│   ├── lib/
│   │   └── BlurGradientBg.module.js
│   └── sandbox.tsx
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/submit.yml
================================================
name: "Submit to Web Store"
on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Cache pnpm modules
        uses: actions/cache@v3
        with:
          path: ~/.pnpm-store
          key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-
      - uses: pnpm/action-setup@v2.2.4
        with:
          version: latest
          run_install: true
      - name: Use Node.js 16.x
        uses: actions/setup-node@v3.4.1
        with:
          node-version: 16.x
          cache: "pnpm"
      - name: Build the extension
        run: pnpm build
      - name: Package the extension into a zip artifact
        run: pnpm package
      - name: Browser Platform Publish
        uses: PlasmoHQ/bpp@v3
        with:
          keys: ${{ secrets.SUBMIT_KEYS }}
          artifact: build/chrome-mv3-prod.zip


================================================
FILE: .gitignore
================================================

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

out/
build/
dist/

# plasmo
.plasmo

# typescript
.tsbuildinfo
.next

================================================
FILE: .prettierrc.mjs
================================================
/**
 * @type {import('prettier').Options}
 */
export default {
  printWidth: 80,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: false,
  trailingComma: "none",
  bracketSpacing: true,
  bracketSameLine: true,
  plugins: ["@ianvs/prettier-plugin-sort-imports"],
  importOrder: [
    "<BUILTIN_MODULES>", // Node.js built-in modules
    "<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups.
    "", // Empty line
    "^@plasmo/(.*)$",
    "",
    "^@plasmohq/(.*)$",
    "",
    "^~(.*)$",
    "",
    "^[./]"
  ]
}


================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================

1. Definitions
--------------

1.1. "Contributor"
    means each individual or legal entity that creates, contributes to
    the creation of, or owns Covered Software.

1.2. "Contributor Version"
    means the combination of the Contributions of others (if any) used
    by a Contributor and that particular Contributor's Contribution.

1.3. "Contribution"
    means Covered Software of a particular Contributor.

1.4. "Covered Software"
    means Source Code Form to which the initial Contributor has attached
    the notice in Exhibit A, the Executable Form of such Source Code
    Form, and Modifications of such Source Code Form, in each case
    including portions thereof.

1.5. "Incompatible With Secondary Licenses"
    means

    (a) that the initial Contributor has attached the notice described
        in Exhibit B to the Covered Software; or

    (b) that the Covered Software was made available under the terms of
        version 1.1 or earlier of the License, but not also under the
        terms of a Secondary License.

1.6. "Executable Form"
    means any form of the work other than Source Code Form.

1.7. "Larger Work"
    means a work that combines Covered Software with other material, in
    a separate file or files, that is not Covered Software.

1.8. "License"
    means this document.

1.9. "Licensable"
    means having the right to grant, to the maximum extent possible,
    whether at the time of the initial grant or subsequently, any and
    all of the rights conveyed by this License.

1.10. "Modifications"
    means any of the following:

    (a) any file in Source Code Form that results from an addition to,
        deletion from, or modification of the contents of Covered
        Software; or

    (b) any new file in Source Code Form that contains any Covered
        Software.

1.11. "Patent Claims" of a Contributor
    means any patent claim(s), including without limitation, method,
    process, and apparatus claims, in any patent Licensable by such
    Contributor that would be infringed, but for the grant of the
    License, by the making, using, selling, offering for sale, having
    made, import, or transfer of either its Contributions or its
    Contributor Version.

1.12. "Secondary License"
    means either the GNU General Public License, Version 2.0, the GNU
    Lesser General Public License, Version 2.1, the GNU Affero General
    Public License, Version 3.0, or any later versions of those
    licenses.

1.13. "Source Code Form"
    means the form of the work preferred for making modifications.

1.14. "You" (or "Your")
    means an individual or a legal entity exercising rights under this
    License. For legal entities, "You" includes any entity that
    controls, is controlled by, or is under common control with You. For
    purposes of this definition, "control" means (a) the power, direct
    or indirect, to cause the direction or management of such entity,
    whether by contract or otherwise, or (b) ownership of more than
    fifty percent (50%) of the outstanding shares or beneficial
    ownership of such entity.

2. License Grants and Conditions
--------------------------------

2.1. Grants

Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:

(a) under intellectual property rights (other than patent or trademark)
    Licensable by such Contributor to use, reproduce, make available,
    modify, display, perform, distribute, and otherwise exploit its
    Contributions, either on an unmodified basis, with Modifications, or
    as part of a Larger Work; and

(b) under Patent Claims of such Contributor to make, use, sell, offer
    for sale, have made, import, and otherwise transfer either its
    Contributions or its Contributor Version.

2.2. Effective Date

The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.

2.3. Limitations on Grant Scope

The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:

(a) for any code that a Contributor has removed from Covered Software;
    or

(b) for infringements caused by: (i) Your and any other third party's
    modifications of Covered Software, or (ii) the combination of its
    Contributions with other software (except as part of its Contributor
    Version); or

(c) under Patent Claims infringed by Covered Software in the absence of
    its Contributions.

This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).

2.4. Subsequent Licenses

No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).

2.5. Representation

Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.

2.6. Fair Use

This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.

2.7. Conditions

Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.

3. Responsibilities
-------------------

3.1. Distribution of Source Form

All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.

3.2. Distribution of Executable Form

If You distribute Covered Software in Executable Form then:

(a) such Covered Software must also be made available in Source Code
    Form, as described in Section 3.1, and You must inform recipients of
    the Executable Form how they can obtain a copy of such Source Code
    Form by reasonable means in a timely manner, at a charge no more
    than the cost of distribution to the recipient; and

(b) You may distribute such Executable Form under the terms of this
    License, or sublicense it under different terms, provided that the
    license for the Executable Form does not attempt to limit or alter
    the recipients' rights in the Source Code Form under this License.

3.3. Distribution of a Larger Work

You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).

3.4. Notices

You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.

3.5. Application of Additional Terms

You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.

4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------

If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.

5. Termination
--------------

5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.

5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.

5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.

************************************************************************
*                                                                      *
*  6. Disclaimer of Warranty                                           *
*  -------------------------                                           *
*                                                                      *
*  Covered Software is provided under this License on an "as is"       *
*  basis, without warranty of any kind, either expressed, implied, or  *
*  statutory, including, without limitation, warranties that the       *
*  Covered Software is free of defects, merchantable, fit for a        *
*  particular purpose or non-infringing. The entire risk as to the     *
*  quality and performance of the Covered Software is with You.        *
*  Should any Covered Software prove defective in any respect, You     *
*  (not any Contributor) assume the cost of any necessary servicing,   *
*  repair, or correction. This disclaimer of warranty constitutes an   *
*  essential part of this License. No use of any Covered Software is   *
*  authorized under this License except under this disclaimer.         *
*                                                                      *
************************************************************************

************************************************************************
*                                                                      *
*  7. Limitation of Liability                                          *
*  --------------------------                                          *
*                                                                      *
*  Under no circumstances and under no legal theory, whether tort      *
*  (including negligence), contract, or otherwise, shall any           *
*  Contributor, or anyone who distributes Covered Software as          *
*  permitted above, be liable to You for any direct, indirect,         *
*  special, incidental, or consequential damages of any character      *
*  including, without limitation, damages for lost profits, loss of    *
*  goodwill, work stoppage, computer failure or malfunction, or any    *
*  and all other commercial damages or losses, even if such party      *
*  shall have been informed of the possibility of such damages. This   *
*  limitation of liability shall not apply to liability for death or   *
*  personal injury resulting from such party's negligence to the       *
*  extent applicable law prohibits such limitation. Some               *
*  jurisdictions do not allow the exclusion or limitation of           *
*  incidental or consequential damages, so this exclusion and          *
*  limitation may not apply to You.                                    *
*                                                                      *
************************************************************************

8. Litigation
-------------

Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.

9. Miscellaneous
----------------

This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.

10. Versions of the License
---------------------------

10.1. New Versions

Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.

10.2. Effect of New Versions

You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.

10.3. Modified Versions

If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).

10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses

If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.

Exhibit A - Source Code Form License Notice
-------------------------------------------

  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at http://mozilla.org/MPL/2.0/.

If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.

You may add additional accurate notices of copyright ownership.

Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------

  This Source Code Form is "Incompatible With Secondary Licenses", as
  defined by the Mozilla Public License, v. 2.0.

================================================
FILE: README.md
================================================
<a name="readme-top"></a>

<div align="center">
<img src="assets/icon.png" width="32" >
<h1>X Cards</h1>

[English](README.md) | [中文](README_ZH.md)

[![x-cards intro](https://img.youtube.com/vi/v8iQV8ZoVBk/0.jpg)](https://youtu.be/okCIZrFrTCE)

easy to use x-cards in x.com

[![][vercel-shield]][vercel-link]

[![][share-x-shield]][share-x-link]
[![][share-whatsapp-shield]][share-whatsapp-link]
[![][share-reddit-shield]][share-reddit-link]
[![][share-weibo-shield]][share-weibo-link]

[![][share-linkedin-shield]][share-linkedin-link]

[github-issues-link]: https://github.com/hzeyuan/x-cards/issues
[github-contributors-shield]: https://img.shields.io/github/contributors/hzeyuan/OpenGPTS?color=c4f042&labelColor=black&style=flat-square
[github-contributors-link]: https://github.com/hzeyuan/OpenGPTS/graphs/contributors
[vercel-link]: https://x-cards.net
[vercel-shield]: https://img.shields.io/website?down_message=offline&label=vercel&labelColor=black&logo=vercel&style=flat-square&up_message=online&url=https://x-cards.net
[share-linkedin-link]: https://linkedin.com/feed
[share-linkedin-shield]: https://img.shields.io/badge/-share%20on%20linkedin-black?labelColor=black&logo=linkedin&logoColor=white&style=flat-square
[share-reddit-link]: https://www.reddit.com/submit?title=x-cards&url=https://github.com/hzeyuan/x-cards
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
[share-telegram-link]: https://t.me/share/url?text=x-cards&url=https://github.com/hzeyuan/x-cards
[share-telegram-shield]: https://img.shields.io/badge/-share%20on%20telegram-black?labelColor=black&logo=telegram&logoColor=white&style=flat-square
[share-weibo-link]: http://service.weibo.com/share/share.php?sharesource=weibo&title=x-cards
[share-weibo-shield]: https://img.shields.io/badge/-share%20on%20weibo-black?labelColor=black&logo=sinaweibo&logoColor=white&style=flat-square
[share-whatsapp-link]: https://api.whatsapp.com/send?text=x-cards
[share-whatsapp-shield]: https://img.shields.io/badge/-share%20on%20whatsapp-black?labelColor=black&logo=whatsapp&logoColor=white&style=flat-square
[share-x-link]: https://x.com/intent/tweet?hashtags=chatbot%2CchatGPT%2CopenAI&url=https://github.com/hzeyuan/x-cards
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square

</div>

⚡ **X Cards** Share Tweet anywhere ,any format,

## Project Background

X is a source of information for many platforms,So this project came into being

## Changelog

<details>
<summary><strong>v0.0.3</strong></summary>

* Performance Optimization: Utilized web workers for image generation, addressing issues with blank spaces on X.com after scrolling down.
* Enhanced Padding Settings: Added the ability to adjust padding within the card for better layout control.
* Image Quality Settings: Introduced options to set the quality of generated images for export.
* Markdown Export: Now supports exporting content in Markdown format for easy integration into documentation or blogs.
* Font Size Adjustment: Added support for modifying font sizes to improve readability and customization.
* Interaction Optimization: After installing the plugin or clicking the icon, redirect to the welcome page.

![Download Extension](./assets/v0.02-demo.jpg)

</details>

<details>
<summary><strong>v0.0.2</strong></summary>

- Added real-time preview feature, now a toast in the upper right corner allows you to observe the generated card.

- Introduced customization for card background color.

- Customizable card width.

- Improved: Now clicking defaults to copying the image, rather than downloading the image.

- Fixed the issue of unable to fetch cover image for videos.

- Added support for fetching continuous posts.

- Enabled dynamic addition, deletion, dragging, and management of posts.

![Download Extension](./assets/v0.02-demo.jpg)

</details>

<details>
<summary><strong>v0.0.1</strong></summary>

- Easy to access, just a simple click away.
- Obtain videos, images, text, likes。
- Export in multiple formats, including JSON, Markdown, PNG, JPEG, and SVG.

</details>

## features

- Added real-time preview feature, now a toast in the upper right corner allows you to observe the generated card.

- Introduced customization for card background color.

- Customizable card width.

- Improved: Now clicking defaults to copying the image, rather than downloading the image.

- Fixed the issue of unable to fetch cover image for videos.

- Added support for fetching continuous posts.

- Enabled dynamic addition, deletion, dragging, and management of posts.

<br/>

## How to Use

### [chrome web Store](https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop)

## or

1. Download Extension

![Download Extension](./assets/install_guide/1.download.png)

2. url input:chrome://extensions/ in your chrome browser, and open the developer mode

3. unzip and Drag the extension file to the page

![Drag the extension file to the page](./assets/install_guide/2.install.png)

4. open x.com and browse the post, you will find your card button in the bottom right corner

## Development Guide

1. The project uses the Plasmo framework for rapid Chrome extension development.
2. Uses Next.js for frontend development.
3. Tailwind CSS and Shadcn as CSS frameworks.
4. Langchain for developing agents.
5. Deployed on Vercel.

Local development:

```bash
pnpm install

# Run frontend
npm run dev:next
# Run plugin
npm run dev:plasmo
```

## Starchart

[![Star History Chart](https://api.star-history.com/svg?repos=hzeyuan/x-cards&type=Date)](https://star-history.com/#hzeyuan/x-cards&Date)


================================================
FILE: README_ZH.md
================================================
<a name="readme-top"></a>

<div align="center">
<img src="assets/icon.png" width="32" >
<h1>X Cards</h1>

[English](README.md) | [中文](README_ZH.md)

[![x-cards intro](https://img.youtube.com/vi/v8iQV8ZoVBk/0.jpg)](https://www.youtube.com/watch?v=v8iQV8ZoVBk)

轻松的X平台上使用x-cards

[![][vercel-shield]][vercel-link]

[![][share-x-shield]][share-x-link]
[![][share-whatsapp-shield]][share-whatsapp-link]
[![][share-reddit-shield]][share-reddit-link]
[![][share-weibo-shield]][share-weibo-link]

[![][share-linkedin-shield]][share-linkedin-link]

[github-issues-link]: https://github.com/hzeyuan/x-cards/issues
[github-contributors-shield]: https://img.shields.io/github/contributors/hzeyuan/OpenGPTS?color=c4f042&labelColor=black&style=flat-square
[github-contributors-link]: https://github.com/hzeyuan/OpenGPTS/graphs/contributors
[vercel-link]: https://x-cards.net
[vercel-shield]: https://img.shields.io/website?down_message=offline&label=vercel&labelColor=black&logo=vercel&style=flat-square&up_message=online&url=https://x-cards.net
[share-linkedin-link]: https://linkedin.com/feed
[share-linkedin-shield]: https://img.shields.io/badge/-share%20on%20linkedin-black?labelColor=black&logo=linkedin&logoColor=white&style=flat-square
[share-reddit-link]: https://www.reddit.com/submit?title=x-cards&url=https://github.com/hzeyuan/x-cards
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
[share-telegram-link]: https://t.me/share/url?text=x-cards&url=https://github.com/hzeyuan/x-cards
[share-telegram-shield]: https://img.shields.io/badge/-share%20on%20telegram-black?labelColor=black&logo=telegram&logoColor=white&style=flat-square
[share-weibo-link]: http://service.weibo.com/share/share.php?sharesource=weibo&title=x-cards
[share-weibo-shield]: https://img.shields.io/badge/-share%20on%20weibo-black?labelColor=black&logo=sinaweibo&logoColor=white&style=flat-square
[share-whatsapp-link]: https://api.whatsapp.com/send?text=x-cards
[share-whatsapp-shield]: https://img.shields.io/badge/-share%20on%20whatsapp-black?labelColor=black&logo=whatsapp&logoColor=white&style=flat-square
[share-x-link]: https://x.com/intent/tweet?hashtags=chatbot%2CchatGPT%2CopenAI&url=https://github.com/hzeyuan/x-cards
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square

</div>

⚡ **X Cards** Share X anywhere ,any format,

## 项目背景

X是许多平台的信息来源,因此产生了这个项目。

## Changelog

<details>
<summary><strong>v0.0.3</strong></summary>

- 优化性能,使用web worker生成图片, 处理下拉后x.com空白问题。
- 增加padding设置,调整卡片内边距。
- 增加生成图片质量设置。
- 增加md格式导出。
- 支持调整字体大小。
- 交互优化,安装插件后或者,点击icon,跳转到welcome页面。

<details>
<summary><strong>v0.0.2</strong></summary>

- 新增实时预览功能,现在右上角有个toast可以观察到生成的卡片。
- 引入了自定义卡片背景颜色。
- 可自定义卡片宽度。
- 改进:现在点击默认复制图片,而不是下载图片。
- 修复了视频获取封面图的问题。
- 增加了连续帖子获取的支持。
- 实现了帖子的动态添加、删除、拖拽和管理。

</details>

<details>
<summary><strong>v0.0.1</strong></summary>

- 点击即可轻松访问。
- 获取视频、图片、文本、点赞等。
- 导出多种格式,包括JSON、Markdown、PNG、JPEG和SVG。

</details>

## features

- 简单易用,只需点击一下即生成卡片。
- 轻松获取视频、图片、文字、点赞和浏览历史。
- 支持多种格式导出,包括JSON、Markdown、PNG、JPEG和SVG。
- 模板功能,保存您经常使用的卡片样式。

<br/>

## How to Use

1. 点击下载插件
   ![Download Extension](./assets/install_guide/1.download.png)

2. 浏览器输入 chrome://extensions/ 并打开开发者模式

3. 解压并,拖动整个文件夹到页面,如图:
   ![Drag the extension file to the page](./assets/install_guide/2.install.png)

4. 打开x.com并浏览帖子,您将在右下角找到您的卡片按钮,参考上方视频。

## 开发指南

本项目使用 Plasmo 框架进行快速 Chrome 扩展开发。
使用 Next.js 进行前端开发。
采用 Tailwind CSS 和 Shadcn 作为 CSS 框架。
使用 Langchain 开发智能代理。
部署在 Vercel 平台上。

Local development:

```bash
pnpm install

# Run frontend
npm run dev:next
# Run plugin
npm run dev:plasmo
```

## Starchart

[![Star History Chart](https://api.star-history.com/svg?repos=hzeyuan/x-cards&type=Date)](https://star-history.com/#hzeyuan/x-cards&Date)


================================================
FILE: components/ui/EditableButton.tsx
================================================
import React, { useState, useRef } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

const EditableButton = ({ text }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [value, setValue] = useState(text);
  const inputRef = useRef(null);

  const handleClick = () => {
    if (!isEditing) {
      setIsEditing(true);
      setTimeout(() => inputRef.current?.focus(), 0);
    } else {
      setIsEditing(false);
      // 在这里可以添加保存或提交的逻辑
      console.log('Submitted:', value);
    }
  };

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      handleClick();
    }
  };

  return (
    <div className="relative inline-block">
      {isEditing ? (
        <Input
          ref={inputRef}
          type="text"
          value={value}
          onChange={handleChange}
          onKeyDown={handleKeyDown}
          className="pr-20"
        />
      ) : (
        <Button size="sm" onClick={handleClick} variant="outline">
          {value}
        </Button>
      )}
      {isEditing && (
        <Button
          onClick={handleClick}
          className="absolute right-1 top-1/2 transform -translate-y-1/2"
          size="sm"
        >
          保存
        </Button>
      )}
    </div>
  );
};

export default EditableButton;

================================================
FILE: components/ui/accordion.tsx
================================================
"use client"

import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

const Accordion = AccordionPrimitive.Root

const AccordionItem = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
  <AccordionPrimitive.Item
    ref={ref}
    // className={cn("border-b", className)}
    className={cn(
      "w-full bg-secondary items-center  cursor-pointer text-[13px] mb-3 px-4 py-0 rounded-md",
      className,
    )}
    {...props}
  />
))
AccordionItem.displayName = "AccordionItem"

const AccordionTrigger = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <AccordionPrimitive.Header className="flex">
    <AccordionPrimitive.Trigger
      ref={ref}
      className={cn(
        "flex flex-1 items-center justify-between py-2 font-medium transition-all  [&[data-state=open]>svg]:rotate-180",
        className
      )}
      {...props}
    >
      {children}
      <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
    </AccordionPrimitive.Trigger>
  </AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName

const AccordionContent = React.forwardRef<
  React.ElementRef<typeof AccordionPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <AccordionPrimitive.Content
    ref={ref}
    className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
    {...props}
  >
    <div className={cn("pb-4 pt-0", className)}>{children}</div>
  </AccordionPrimitive.Content>
))

AccordionContent.displayName = AccordionPrimitive.Content.displayName

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }


================================================
FILE: components/ui/alert-dialog.tsx
================================================
"use client"

import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"

const AlertDialog = AlertDialogPrimitive.Root

const AlertDialogTrigger = AlertDialogPrimitive.Trigger

const AlertDialogPortal = AlertDialogPrimitive.Portal

const AlertDialogOverlay = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Overlay
    className={cn(
      "fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
      className
    )}
    {...props}
    ref={ref}
  />
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName

const AlertDialogContent = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
        className
      )}
      {...props}
    />
  </AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName

const AlertDialogHeader = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col space-y-2 text-center sm:text-left",
      className
    )}
    {...props}
  />
)
AlertDialogHeader.displayName = "AlertDialogHeader"

const AlertDialogFooter = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) => (
  <div
    className={cn(
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
      className
    )}
    {...props}
  />
)
AlertDialogFooter.displayName = "AlertDialogFooter"

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Title
    ref={ref}
    className={cn("text-lg font-semibold", className)}
    {...props}
  />
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Description
    ref={ref}
    className={cn("text-sm text-muted-foreground", className)}
    {...props}
  />
))
AlertDialogDescription.displayName =
  AlertDialogPrimitive.Description.displayName

const AlertDialogAction = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Action>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Action
    ref={ref}
    className={cn(buttonVariants(), className)}
    {...props}
  />
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName

const AlertDialogCancel = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
  <AlertDialogPrimitive.Cancel
    ref={ref}
    className={cn(
      buttonVariants({ variant: "outline" }),
      "mt-2 sm:mt-0",
      className
    )}
    {...props}
  />
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName

export {
  AlertDialog,
  AlertDialogPortal,
  AlertDialogOverlay,
  AlertDialogTrigger,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogAction,
  AlertDialogCancel,
}


================================================
FILE: components/ui/badge.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const badgeVariants = cva(
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
        secondary:
          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive:
          "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  )
}

export { Badge, badgeVariants }


================================================
FILE: components/ui/button.tsx
================================================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 rounded-md px-3",
        lg: "h-11 rounded-md px-8",
        icon: "h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"

export { Button, buttonVariants }


================================================
FILE: components/ui/color-picker.tsx
================================================
'use client';

import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { cn } from '@/lib/utils';
import type { ButtonProps } from '@/components/ui/button';
import { Button } from '@/components/ui/button';

import { Input } from '@/components/ui/input';
import { PopoverContent, PopoverTrigger, Popover } from './popover';



export function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
    const innerRef = useRef<T>(null);

    useEffect(() => {
        if (!ref) return;
        if (typeof ref === 'function') {
            ref(innerRef.current);
        } else {
            ref.current = innerRef.current;
        }
    });

    return innerRef;
}


interface ColorPickerProps {
    value: string;
    onChange: (value: string) => void;
    onBlur?: () => void;
}

const ColorPicker = forwardRef<
    HTMLInputElement,
    Omit<ButtonProps, 'value' | 'onChange' | 'onBlur'> & ColorPickerProps
>(
    (
        { disabled, value, onChange, onBlur, name, className, ...props },
        forwardedRef
    ) => {
        const ref = useForwardedRef(forwardedRef);
        const [open, setOpen] = useState(false);

        const parsedValue = useMemo(() => {
            return value || '#FFFFFF';
        }, [value]);

        return (
            <Popover onOpenChange={setOpen} open={open}>
                <PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
                    <Button
                        {...props}
                        className={cn('block', className)}
                        name={name}
                        onClick={() => {
                            setOpen(true);
                        }}
                        size='icon'
                        style={{
                            backgroundColor: parsedValue,
                        }}
                        variant='outline'
                    >
                        <div />
                    </Button>
                </PopoverTrigger>
                <PopoverContent className='w-full flex flex-col gap-y-2'>
                    <HexColorPicker color={parsedValue} onChange={onChange} />
                    <Input
                        maxLength={7}
                        onChange={(e) => {
                            onChange(e?.currentTarget?.value);
                        }}
                        ref={ref}
                        value={parsedValue}
                    />
                </PopoverContent>
            </Popover>
        );
    }
);
ColorPicker.displayName = 'ColorPicker';

export { ColorPicker };

================================================
FILE: components/ui/dropdown-menu.tsx
================================================
"use client"

import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"

import { cn } from "@/lib/utils"

const DropdownMenu = DropdownMenuPrimitive.Root

const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger

const DropdownMenuGroup = DropdownMenuPrimitive.Group

const DropdownMenuPortal = DropdownMenuPrimitive.Portal

const DropdownMenuSub = DropdownMenuPrimitive.Sub

const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup

const DropdownMenuSubTrigger = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
    inset?: boolean
  }
>(({ className, inset, children, ...props }, ref) => (
  <DropdownMenuPrimitive.SubTrigger
    ref={ref}
    className={cn(
      "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
      inset && "pl-8",
      className
    )}
    {...props}
  >
    {children}
    <ChevronRight className="ml-auto h-4 w-4" />
  </DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
  DropdownMenuPrimitive.SubTrigger.displayName

const DropdownMenuSubContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.SubContent
    ref={ref}
    className={cn(
      "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
      className
    )}
    {...props}
  />
))
DropdownMenuSubContent.displayName =
  DropdownMenuPrimitive.SubContent.displayName

const DropdownMenuContent = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
  <DropdownMenuPrimitive.Portal>
    <DropdownMenuPrimitive.Content
      ref={ref}
      sideOffset={sideOffset}
      className={cn(
        "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className
      )}
      {...props}
    />
  </DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName

const DropdownMenuItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName

const DropdownMenuCheckboxItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
  <DropdownMenuPrimitive.CheckboxItem
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    checked={checked}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <Check className="h-4 w-4" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
  DropdownMenuPrimitive.CheckboxItem.displayName

const DropdownMenuRadioItem = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
  <DropdownMenuPrimitive.RadioItem
    ref={ref}
    className={cn(
      "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <DropdownMenuPrimitive.ItemIndicator>
        <Circle className="h-2 w-2 fill-current" />
      </DropdownMenuPrimitive.ItemIndicator>
    </span>
    {children}
  </DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName

const DropdownMenuLabel = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
    inset?: boolean
  }
>(({ className, inset, ...props }, ref) => (
  <DropdownMenuPrimitive.Label
    ref={ref}
    className={cn(
      "px-2 py-1.5 text-sm font-semibold",
      inset && "pl-8",
      className
    )}
    {...props}
  />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName

const DropdownMenuSeparator = React.forwardRef<
  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <DropdownMenuPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName

const DropdownMenuShortcut = ({
  className,
  ...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
  return (
    <span
      className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
      {...props}
    />
  )
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"

export {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuCheckboxItem,
  DropdownMenuRadioItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuGroup,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuRadioGroup,
}


================================================
FILE: components/ui/input.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = "Input"

export { Input }


================================================
FILE: components/ui/popover.tsx
================================================
"use client"

import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"

import { cn } from "@/lib/utils"

const Popover = PopoverPrimitive.Root

const PopoverTrigger = PopoverPrimitive.Trigger

const PopoverContent = React.forwardRef<
  React.ElementRef<typeof PopoverPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
  <PopoverPrimitive.Portal>
    <PopoverPrimitive.Content
      ref={ref}
      align={align}
      sideOffset={sideOffset}
      className={cn(
        "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        className
      )}
      {...props}
    />
  </PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName

export { Popover, PopoverTrigger, PopoverContent }


================================================
FILE: components/ui/radio-group.tsx
================================================
"use client"

import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"

import { cn } from "@/lib/utils"

const RadioGroup = React.forwardRef<
  React.ElementRef<typeof RadioGroupPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
  return (
    <RadioGroupPrimitive.Root
      className={cn("grid gap-2", className)}
      {...props}
      ref={ref}
    />
  )
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName

const RadioGroupItem = React.forwardRef<
  React.ElementRef<typeof RadioGroupPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
  return (
    <RadioGroupPrimitive.Item
      ref={ref}
      className={cn(
        "aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
        className
      )}
      {...props}
    >
      <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
        <Circle className="h-2.5 w-2.5 fill-current text-current" />
      </RadioGroupPrimitive.Indicator>
    </RadioGroupPrimitive.Item>
  )
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName

export { RadioGroup, RadioGroupItem }


================================================
FILE: components/ui/select-position.tsx
================================================
"use client"
import { cn } from "@lib/utils"
import { useState } from "react"

export const SelectBackgroundPosition = ({ onChange }) => {
    const [position, setPosition] = useState('center center')

    const positions = [
        { value: "left top", title: "Top left" },
        { value: "center top", title: "Top center" },
        { value: "right top", title: "Top right" },
        { value: "left center", title: "Left center" },
        { value: "center center", title: "Center" },
        { value: "right center", title: "Right center" },
        { value: "left bottom", title: "Bottom left" },
        { value: "center bottom", title: "Bottom center" },
        { value: "right bottom", title: "Bottom right" }
    ]

    const handleClick = (newPosition) => {
        setPosition(newPosition)
        onChange(newPosition)
    }

    return (
        <div className="relative grid w-12 h-12 grid-cols-3 p-1 bg-white border border-gray-200 rounded-lg dark:border-gray-700 place-content-around place-items-center aspect-square dark:bg-gray-900 shadow hover:scale-[1.4] duration-300 ease-[cubic-bezier(.75,-0.5,0,1.75)]">
            {positions.map(({ value, title }) => (
                <div
                    key={value}
                    onClick={() => handleClick(value)}
                    title={title}
                    className={cn(
                        "w-[8px] h-[8px] rounded-full cursor-pointer",
                        position === value
                            ? "bg-gray-800 dark:bg-gray-200"
                            : "bg-gray-300 hover:bg-gray-500 dark:hover:bg-gray-400 dark:bg-gray-600/50"
                    )}
                />
            ))}
        </div>
    )
}

================================================
FILE: components/ui/select.tsx
================================================
"use client"

import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"

import { cn } from "@/lib/utils"

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Trigger
    ref={ref}
    className={cn(
      "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
      className
    )}
    {...props}
  >
    {children}
    <SelectPrimitive.Icon asChild>
      <ChevronDown className="h-4 w-4 opacity-50" />
    </SelectPrimitive.Icon>
  </SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollUpButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronUp className="h-4 w-4" />
  </SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.ScrollDownButton
    ref={ref}
    className={cn(
      "flex cursor-default items-center justify-center py-1",
      className
    )}
    {...props}
  >
    <ChevronDown className="h-4 w-4" />
  </SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
  SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
  <SelectPrimitive.Portal>
    <SelectPrimitive.Content
      ref={ref}
      className={cn(
        "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
        position === "popper" &&
          "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
        className
      )}
      position={position}
      {...props}
    >
      <SelectScrollUpButton />
      <SelectPrimitive.Viewport
        className={cn(
          "p-1",
          position === "popper" &&
            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
        )}
      >
        {children}
      </SelectPrimitive.Viewport>
      <SelectScrollDownButton />
    </SelectPrimitive.Content>
  </SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Label>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Label
    ref={ref}
    className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
    {...props}
  />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Item>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
  <SelectPrimitive.Item
    ref={ref}
    className={cn(
      "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
      className
    )}
    {...props}
  >
    <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
      <SelectPrimitive.ItemIndicator>
        <Check className="h-4 w-4" />
      </SelectPrimitive.ItemIndicator>
    </span>

    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
  </SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
  React.ElementRef<typeof SelectPrimitive.Separator>,
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
  <SelectPrimitive.Separator
    ref={ref}
    className={cn("-mx-1 my-1 h-px bg-muted", className)}
    {...props}
  />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
  Select,
  SelectGroup,
  SelectValue,
  SelectTrigger,
  SelectContent,
  SelectLabel,
  SelectItem,
  SelectSeparator,
  SelectScrollUpButton,
  SelectScrollDownButton,
}


================================================
FILE: components/ui/slider.tsx
================================================
"use client"

import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"

import { cn } from "@/lib/utils"

const Slider = React.forwardRef<
  React.ElementRef<typeof SliderPrimitive.Root>,
  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
  <SliderPrimitive.Root
    ref={ref}
    className={cn(
      "relative flex w-full touch-none select-none items-center",
      className
    )}
    {...props}
  >
    {/*  bg-secondary */}
    <SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white">
      <SliderPrimitive.Range className="absolute h-full bg-primary" />
    </SliderPrimitive.Track>
    <SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
  </SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

export { Slider }


================================================
FILE: components/ui/sonner.tsx
================================================
"use client"

import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"

type ToasterProps = React.ComponentProps<typeof Sonner>

const Toaster = ({ ...props }: ToasterProps) => {
  const { theme = "system" } = useTheme()

  return (
    <Sonner
      theme={theme as ToasterProps["theme"]}
      className="toaster group"
      toastOptions={{
        classNames: {
          toast:
            "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
          description: "group-[.toast]:text-muted-foreground",
          actionButton:
            "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
          cancelButton:
            "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
        },
      }}
      {...props}
    />
  )
}

export { Toaster }


================================================
FILE: components/ui/switch.tsx
================================================
"use client"

import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"

import { cn } from "@/lib/utils"

const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
  <SwitchPrimitives.Root
    className={cn(
      "peer inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
      className
    )}
    {...props}
    ref={ref}
  >
    <SwitchPrimitives.Thumb
      className={cn(
        "pointer-events-none block h-2.5 w-2.5 rounded-full bg-white ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0 duration-250"
      )}
    />
  </SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }


================================================
FILE: components/ui/tabs.tsx
================================================
"use client"

import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"

import { cn } from "@/lib/utils"

const Tabs = TabsPrimitive.Root

const TabsList = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.List>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.List
    ref={ref}
    className={cn(
      "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
      className
    )}
    {...props}
  />
))
TabsList.displayName = TabsPrimitive.List.displayName

const TabsTrigger = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Trigger>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Trigger
    ref={ref}
    className={cn(
      "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
      className
    )}
    {...props}
  />
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName

const TabsContent = React.forwardRef<
  React.ElementRef<typeof TabsPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
  <TabsPrimitive.Content
    ref={ref}
    className={cn(
      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
      className
    )}
    {...props}
  />
))
TabsContent.displayName = TabsPrimitive.Content.displayName

export { Tabs, TabsList, TabsTrigger, TabsContent }


================================================
FILE: components/ui/textarea.tsx
================================================
import * as React from "react"

import { cn } from "@/lib/utils"

export interface TextareaProps
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  ({ className, ...props }, ref) => {
    return (
      <textarea
        className={cn(
          "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Textarea.displayName = "Textarea"

export { Textarea }


================================================
FILE: components/ui/toast.tsx
================================================
"use client"

import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

const ToastProvider = ToastPrimitives.Provider

const ToastViewport = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Viewport>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Viewport
    ref={ref}
    className={cn(
      "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
      className
    )}
    {...props}
  />
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName

const toastVariants = cva(
  "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
  {
    variants: {
      variant: {
        default: "border bg-background text-foreground",
        destructive:
          "destructive group border-destructive bg-destructive text-destructive-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

const Toast = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
    VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
  return (
    <ToastPrimitives.Root
      ref={ref}
      className={cn(toastVariants({ variant }), className)}
      {...props}
    />
  )
})
Toast.displayName = ToastPrimitives.Root.displayName

const ToastAction = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Action>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Action
    ref={ref}
    className={cn(
      "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
      className
    )}
    {...props}
  />
))
ToastAction.displayName = ToastPrimitives.Action.displayName

const ToastClose = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Close>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Close
    ref={ref}
    className={cn(
      "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
      className
    )}
    toast-close=""
    {...props}
  >
    <X className="h-4 w-4" />
  </ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName

const ToastTitle = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Title>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Title
    ref={ref}
    className={cn("text-sm font-semibold", className)}
    {...props}
  />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName

const ToastDescription = React.forwardRef<
  React.ElementRef<typeof ToastPrimitives.Description>,
  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
  <ToastPrimitives.Description
    ref={ref}
    className={cn("text-sm opacity-90", className)}
    {...props}
  />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName

type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>

type ToastActionElement = React.ReactElement<typeof ToastAction>

export {
  type ToastProps,
  type ToastActionElement,
  ToastProvider,
  ToastViewport,
  Toast,
  ToastTitle,
  ToastDescription,
  ToastClose,
  ToastAction,
}


================================================
FILE: components/ui/toaster.tsx
================================================
"use client"

import {
  Toast,
  ToastClose,
  ToastDescription,
  ToastProvider,
  ToastTitle,
  ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"

export function Toaster() {
  const { toasts } = useToast()

  return (
    <ToastProvider>
      {toasts.map(function ({ id, title, description, action, ...props }) {
        return (
          <Toast key={id} {...props}>
            <div className="grid gap-1">
              {title && <ToastTitle>{title}</ToastTitle>}
              {description && (
                <ToastDescription>{description}</ToastDescription>
              )}
            </div>
            {action}
            <ToastClose />
          </Toast>
        )
      })}
      <ToastViewport />
    </ToastProvider>
  )
}


================================================
FILE: components/ui/use-toast.ts
================================================
"use client"

// Inspired by react-hot-toast library
import * as React from "react"

import type {
  ToastActionElement,
  ToastProps,
} from "@/components/ui/toast"

const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000

type ToasterToast = ToastProps & {
  id: string
  title?: React.ReactNode
  description?: React.ReactNode
  action?: ToastActionElement
}

const actionTypes = {
  ADD_TOAST: "ADD_TOAST",
  UPDATE_TOAST: "UPDATE_TOAST",
  DISMISS_TOAST: "DISMISS_TOAST",
  REMOVE_TOAST: "REMOVE_TOAST",
} as const

let count = 0

function genId() {
  count = (count + 1) % Number.MAX_SAFE_INTEGER
  return count.toString()
}

type ActionType = typeof actionTypes

type Action =
  | {
      type: ActionType["ADD_TOAST"]
      toast: ToasterToast
    }
  | {
      type: ActionType["UPDATE_TOAST"]
      toast: Partial<ToasterToast>
    }
  | {
      type: ActionType["DISMISS_TOAST"]
      toastId?: ToasterToast["id"]
    }
  | {
      type: ActionType["REMOVE_TOAST"]
      toastId?: ToasterToast["id"]
    }

interface State {
  toasts: ToasterToast[]
}

const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()

const addToRemoveQueue = (toastId: string) => {
  if (toastTimeouts.has(toastId)) {
    return
  }

  const timeout = setTimeout(() => {
    toastTimeouts.delete(toastId)
    dispatch({
      type: "REMOVE_TOAST",
      toastId: toastId,
    })
  }, TOAST_REMOVE_DELAY)

  toastTimeouts.set(toastId, timeout)
}

export const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "ADD_TOAST":
      return {
        ...state,
        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
      }

    case "UPDATE_TOAST":
      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === action.toast.id ? { ...t, ...action.toast } : t
        ),
      }

    case "DISMISS_TOAST": {
      const { toastId } = action

      // ! Side effects ! - This could be extracted into a dismissToast() action,
      // but I'll keep it here for simplicity
      if (toastId) {
        addToRemoveQueue(toastId)
      } else {
        state.toasts.forEach((toast) => {
          addToRemoveQueue(toast.id)
        })
      }

      return {
        ...state,
        toasts: state.toasts.map((t) =>
          t.id === toastId || toastId === undefined
            ? {
                ...t,
                open: false,
              }
            : t
        ),
      }
    }
    case "REMOVE_TOAST":
      if (action.toastId === undefined) {
        return {
          ...state,
          toasts: [],
        }
      }
      return {
        ...state,
        toasts: state.toasts.filter((t) => t.id !== action.toastId),
      }
  }
}

const listeners: Array<(state: State) => void> = []

let memoryState: State = { toasts: [] }

function dispatch(action: Action) {
  memoryState = reducer(memoryState, action)
  listeners.forEach((listener) => {
    listener(memoryState)
  })
}

type Toast = Omit<ToasterToast, "id">

function toast({ ...props }: Toast) {
  const id = genId()

  const update = (props: ToasterToast) =>
    dispatch({
      type: "UPDATE_TOAST",
      toast: { ...props, id },
    })
  const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })

  dispatch({
    type: "ADD_TOAST",
    toast: {
      ...props,
      id,
      open: true,
      onOpenChange: (open) => {
        if (!open) dismiss()
      },
    },
  })

  return {
    id: id,
    dismiss,
    update,
  }
}

function useToast() {
  const [state, setState] = React.useState<State>(memoryState)

  React.useEffect(() => {
    listeners.push(setState)
    return () => {
      const index = listeners.indexOf(setState)
      if (index > -1) {
        listeners.splice(index, 1)
      }
    }
  }, [state])

  return {
    ...state,
    toast,
    dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
  }
}

export { useToast, toast }


================================================
FILE: components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "default",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

================================================
FILE: lib/utils.ts
================================================
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}


================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: next.config.js
================================================
// const isProd = process.env.NODE_ENV === 'production'
const isProd = false;
const bundleAnalyzer = require('@next/bundle-analyzer')
const withBundleAnalyzer = bundleAnalyzer({
    enabled: false,
    openAnalyzer: true,
})


module.exports = withBundleAnalyzer({
    swcMinify: true,
    crossOrigin: 'anonymous',
    reactStrictMode: false,
    env: {
        STATIC_URL: isProd ? STATIC_URL : "http://localhost:3000",
    },
    // typescript: {
    //     ignoreBuildErrors: true,
    // },
    // webpack: (config) => {
    //     config.externals = [...config.externals, { canvas: 'canvas' }];
    //     return config;
    // },
    // experimental: {
    //     serverActions: {
    //         allowedOrigins: []
    //     },
    // },

    // async redirects() {
    //     return [
    //         {
    //             source: "/home",
    //             destination: "/",
    //             permanent: false,
    //         }]
    // }


    typescript: {
        ignoreBuildErrors: true,
    },
    output: 'export',
    // 禁用图像优化,因为它需要 Next.js 服务器
    images: {
        unoptimized: true,
    },
    webpack: (config, { isServer }) => {
        if (isServer) {
            // 在服务器端构建时忽略 API 路由
            config.externals = config.externals || [];
            config.externals.push((context, request, callback) => {
                if (request.startsWith('pages/api/') || request.startsWith('app/api/')) {
                    return callback(null, `commonjs ${request}`);
                }
                callback();
            });
        }
        return config;
    },

})



================================================
FILE: package.json
================================================
{
  "name": "X Cards Native Tweet Card service for X",
  "displayName": "Seamlessly integrate card services directly on your X",
  "version": "0.0.3",
  "description": "Create stunning tweet cards effortlessly with X Cards on Twitter. Share your thoughts, ideas, and creations with style and flair",
  "keywords": [
    "x",
    "card",
    "share",
    "anywhere"
  ],
  "author": "yixtotieq@gmail.com",
  "scripts": {
    "dev": "run-p dev:*",
    "dev:plasmo": "plasmo dev",
    "dev:next": "next dev --port 1947",
    "build": "plasmo build",
    "start:next": "next start",
    "build:next": "next build",
    "package": "plasmo package"
  },
  "dependencies": {
    "@csstools/convert-colors": "^2.0.0",
    "@langchain/core": "^0.2.16",
    "@langchain/openai": "^0.2.2",
    "@next/env": "^14.2.5",
    "@plasmohq/messaging": "0.6.0",
    "@radix-ui/react-accordion": "^1.2.0",
    "@radix-ui/react-alert-dialog": "^1.1.1",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-popover": "^1.1.1",
    "@radix-ui/react-radio-group": "^1.2.0",
    "@radix-ui/react-select": "^2.1.1",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.1",
    "@supabase/supabase-js": "^2.45.1",
    "@tippyjs/react": "^4.2.6",
    "@vercel/analytics": "^1.3.1",
    "@visactor/react-vchart": "^1.11.9",
    "add": "^2.0.6",
    "class-variance-authority": "^0.7.0",
    "classnames": "^2.5.1",
    "clsx": "^2.1.1",
    "date-fns": "^3.6.0",
    "file-saver": "^2.0.5",
    "framer-motion": "^11.3.7",
    "html-to-image": "^1.11.11",
    "idb-keyval": "3.0.0",
    "jsonrepair": "^3.8.0",
    "jszip": "^3.10.1",
    "langchain": "^0.2.10",
    "lodash-es": "^4.17.21",
    "lucide-react": "^0.408.0",
    "markmap-common": "^0.17.0",
    "markmap-lib": "^0.17.0",
    "markmap-view": "^0.17.0",
    "mini-svg-data-uri": "^1.4.4",
    "modern-screenshot": "^4.4.39",
    "next": "14.1.0",
    "next-themes": "^0.3.0",
    "p-limit": "^6.1.0",
    "p-retry": "^6.2.0",
    "plasmo": "0.88.0",
    "popover": "^2.4.1",
    "randomcolor": "^0.6.2",
    "react": "18.2.0",
    "react-accessible-treeview": "^2.9.1",
    "react-color": "^2.19.3",
    "react-colorful": "^5.6.1",
    "react-day-picker": "^9.0.0",
    "react-dom": "18.2.0",
    "react-hot-toast": "^2.4.1",
    "react-icon-cloud": "^4.1.4",
    "react-intersection-observer": "^9.13.0",
    "react-player": "^2.16.0",
    "react-scroll-parallax": "^3.4.5",
    "react-use-measure": "^2.1.1",
    "react-virtual": "^2.10.4",
    "recharts": "^2.12.7",
    "sonner": "^1.5.0",
    "styled-components": "^6.1.12",
    "tailwind-merge": "^2.4.0",
    "tailwindcss-animate": "^1.0.7",
    "uuid": "^10.0.0",
    "webextension-polyfill": "^0.12.0",
    "zod": "^3.23.8",
    "zustand": "^4.5.4"
  },
  "devDependencies": {
    "@ianvs/prettier-plugin-sort-imports": "4.1.1",
    "@next/bundle-analyzer": "^14.2.5",
    "@types/chrome": "0.0.258",
    "@types/lodash-es": "^4.17.12",
    "@types/node": "20.11.5",
    "@types/react": "18.2.48",
    "@types/react-dom": "18.2.18",
    "@types/webextension-polyfill": "^0.10.7",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.39",
    "postcss-import": "^15.1.0",
    "postcss-nested": "^6.2.0",
    "prettier": "3.2.4",
    "tailwindcss": "^3.3.6",
    "typescript": "5.3.3"
  },
  "manifest": {
    "permissions": [
      "storage",
       "tabs"
    ],
    "key": "$CRX_PUBLIC_KEY"
  }
}

================================================
FILE: popup.tsx
================================================
// export const Popup = () => {
//     return (
//         <div className="w-4 h-4">
//         </div>
//     )
// }

================================================
FILE: postcss.config.js
================================================
/**
 * @type {import('postcss').ProcessOptions}
 */
module.exports = {
    plugins: {
        "postcss-import": {},
        "postcss-nested": {},
        tailwindcss: {},
        autoprefixer: {}
    }
}

================================================
FILE: src/app/(app)/components/FrequentlyAskedQuestions.tsx
================================================
"use client"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion"
import BoldCopy from "@src/components/ui/bold-copy"

export const FrequentlyAskedQuestions = () => {
    return (
        <div>
            <BoldCopy
                className="border border-gray-200 dark:border-zinc-800"
                text="FAQ">

            </BoldCopy>
            <div className="not-prose mt-4 flex flex-col gap-4 md:mt-8 text-2xl">
                <Accordion type="single" collapsible className="w-full">
                    <AccordionItem value="item-1">
                        <AccordionTrigger>1.Why was this product created?</AccordionTrigger>
                        <AccordionContent className="py-8 text-xl">
                            Our product was born out of a common workplace challenge. Many of us find ourselves juggling numerous websites throughout our workday. With an ever-growing list of URLs to remember, it's easy to get overwhelmed. This tool was developed to streamline your digital workflow and keep all your important web resources organized in one place.
                        </AccordionContent>
                    </AccordionItem>
                    <AccordionItem value="item-2">
                        <AccordionTrigger>2.Do you collect or share my data?</AccordionTrigger>
                        <AccordionContent className="py-8 text-xl">
                            Your privacy is our top priority. All data is stored locally on your device. We do not have access to, collect, or share any of your personal information or browsing history. Your data remains entirely under your control.
                        </AccordionContent>
                    </AccordionItem>
                    <AccordionItem value="item-3">
                        <AccordionTrigger>3. What are your future plans for the product?</AccordionTrigger>
                        <AccordionContent className="py-8 text-xl">
                            We're constantly working on improvements and new features! To stay up-to-date with our latest developments:
                            <li>Follow us on Twitter/X for real-time updates</li>
                            <li>Check our website regularly for announcements</li>
                            <li>Join our mailing list for exclusive news and features</li>
                        </AccordionContent>
                    </AccordionItem>
                </Accordion>
            </div>

        </div>

    )
}




================================================
FILE: src/app/(app)/components/GoogleFontSelector.tsx
================================================
import React, { useState, useEffect } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useCardStore } from '@src/hooks/useCardStore';
import { RadioGroup, RadioGroupItem } from '@components/ui/radio-group';
// import { Label } from '@radix-ui/react-select';

const googleFonts = [
    { name: 'Default', value: 'sans-serif' },
    { name: 'Roboto', value: 'Roboto' },
    { name: 'Open Sans', value: 'Open Sans' },
    { name: 'Lato', value: 'Lato' },
    { name: 'Montserrat', value: 'Montserrat' },
    { name: 'Noto Sans SC', value: 'Noto Sans SC' },
    { name: 'Playfair Display', value: 'Playfair Display' },
    { name: 'Merriweather', value: 'Merriweather' },
    { name: 'Source Sans Pro', value: 'Source Sans Pro' },
    { name: 'PT Sans', value: 'PT Sans' },
    { name: 'Raleway', value: 'Raleway' },
    { name: 'Oswald', value: 'Oswald' },
    { name: 'Nunito', value: 'Nunito' },
    { name: 'Ubuntu', value: 'Ubuntu' },
    { name: 'Poppins', value: 'Poppins' },
    { name: 'Quicksand', value: 'Quicksand' },
    { name: 'Rubik', value: 'Rubik' },
    { name: 'Work Sans', value: 'Work Sans' },
    { name: 'Fira Sans', value: 'Fira Sans' },
    { name: 'Noto Serif', value: 'Noto Serif' },
    // 中文字体
    { name: 'Noto Sans SC', value: 'Noto Sans SC' },
    { name: 'Noto Serif SC', value: 'Noto Serif SC' },
    { name: 'ZCOOL XiaoWei', value: 'ZCOOL XiaoWei' },
    { name: 'ZCOOL QingKe HuangYou', value: 'ZCOOL QingKe HuangYou' },
    { name: 'Ma Shan Zheng', value: 'Ma Shan Zheng' },
];

export default function GoogleFontSelector({ onFontChange }) {
    // const [selectedFont, setSelectedFont] = useState('sans-serif');
    const setFontStyles = useCardStore((state) => state.updateCardStyles);
    const fontFamily = useCardStore((state) => state.cardStyles.fontFamily);


    const loadFont = (fontName) => {
        if (fontName === 'sans-serif') return;
        const link = document.createElement('link');
        link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}:wght@400;700&display=swap`;
        link.rel = 'stylesheet';
        document.head.appendChild(link);
    };

    useEffect(() => {
        // 动态加载谷歌字体
        const link = document.createElement('link');
        link.href = 'https://fonts.googleapis.com/css2?family=Roboto&family=Open+Sans&family=Lato&family=Montserrat&family=Noto+Sans+SC&display=swap';
        link.rel = 'stylesheet';
        document.head.appendChild(link);

        return () => {
            document.head.removeChild(link);
        };
    }, []);


    const handleFontChange = (value) => {
        setFontStyles({
            fontFamily: value
        })
        loadFont(value);
        onFontChange?.(value);
    };


    return (
        <div className="">
            <RadioGroup value={fontFamily} onValueChange={handleFontChange}>
                {googleFonts.map((font) => (
                    <div key={font.value} className="flex items-center space-x-2">
                        <RadioGroupItem value={font.value} id={font.value} />
                        <span>{font.name}</span>
                    </div>
                ))}
            </RadioGroup>
        </div>
    );
}

================================================
FILE: src/app/(app)/components/ImageLayout.tsx
================================================
import { useCardStore } from '@src/hooks/useCardStore';
import React, { useEffect, useRef, useState } from 'react';

const imageCache: { [key: string]: string } = {};

const ImageLayout: React.FC<{
    images: string[],
    layout: 'vertical' | 'grid2' | 'grid4',
    onAllImagesLoaded?: () => void

}> = ({ images, layout }) => {
    if (images.length === 0) return null;


    const [loadedImages, setLoadedImages] = useState<string[]>([]);
    const isMounted = useRef(true);

    useEffect(() => {
        return () => {
            isMounted.current = false;
        };
    }, []);

    useEffect(() => {
        const loadImages = async () => {
            const loadPromises = images.map(src =>
                new Promise<string>(async (resolve, reject) => {
                    if (imageCache[src]) {
                        console.log('Image loaded from cache:', src);
                        resolve(src);
                        return;
                    }
                    try {
                        // 使用 fetch 来利用浏览器的缓存机制
                        const response = await fetch(src, { cache: 'force-cache' });
                        const blob = await response.blob();
                        const objectURL = URL.createObjectURL(blob);

                        // 将图片添加到内存缓存
                        imageCache[src] = objectURL;

                        const img = new Image();
                        img.src = objectURL;
                        img.onload = () => {
                            useCardStore.getState().addLoadedImage(src, 'success');
                            resolve(src);
                        }
                        img.onerror = () => {
                            useCardStore.getState().addLoadedImage(src, 'error');
                            reject();
                        };
                    } catch (error) {
                        reject(error);
                        useCardStore.getState().addLoadedImage(src, 'error');
                    }
                })
            );

            try {
                const loaded = await Promise.all(loadPromises);
                if (isMounted.current) {
                    setLoadedImages(loaded);
                }
            } catch (error) {
                console.error('Failed to load one or more images:', error);
            }
        };

        loadImages();
    }, [images]);

    if (loadedImages.length === 0) return <div>Loading...</div>;


    const renderImage = (src: string, index: number, className: string = "w-full h-full object-contain") => (
        <img
            key={index}
            src={imageCache[src] || src}
            alt={`Image ${index + 1}`}
            className={className}
        />
    );


    const layoutStyles = {
        vertical: "flex flex-col space-y-2",
        grid2: "grid grid-cols-2 gap-2",
        grid4: "grid grid-cols-2 grid-rows-2 gap-2"
    };

    if (images.length === 2) {
        return (
            <div className="w-full flex flex-row gap-2 ">
                <div className="w-1/2">
                    {renderImage(images[0], 0)}
                </div>
                <div className="w-1/2">
                    {renderImage(images[1], 1)}
                </div>
            </div>
        );
    }

    if (images.length === 3) {
        return (
            <div className="w-full flex flex-row gap-2 ">
                <div className="w-1/2">
                    {renderImage(images[0], 0)}
                </div>
                <div className="w-1/4 flex flex-col gap-2">
                    <div className="h-1/2">
                        {renderImage(images[1], 1)}
                    </div>
                    <div className="h-1/2">
                        {renderImage(images[2], 2)}
                    </div>
                </div>
            </div>
        );
    }

    return (
        <div className={`w-full ${layoutStyles[layout]}`}>
            {images.slice(0, layout === 'vertical' ? undefined : (layout === 'grid2' ? 2 : 4))
                .map((src, index) => renderImage(src, index))}
        </div>
    );
};

export default ImageLayout;

================================================
FILE: src/app/(app)/components/LazyLoadAnimatedSection.tsx
================================================
import { useInView } from 'react-intersection-observer';
import { motion} from "framer-motion"
const LazyLoadAnimatedSection = ({ children, animation = 'fadeIn' }) => {
    const [ref, inView] = useInView({
        triggerOnce: true,
        threshold: 0.1, // Adjust this value to control when the animation triggers
    });

    const animations = {
        fadeIn: {
            opacity: inView ? 1 : 0,
            y: inView ? 0 : 50,
            transition: { duration: 0.5 }
        },
        slideIn: {
            x: inView ? 0 : -100,
            opacity: inView ? 1 : 0,
            transition: { duration: 0.5 }
        },
        scaleIn: {
            scale: inView ? 1 : 0.8,
            opacity: inView ? 1 : 0,
            transition: { duration: 0.5 }
        }
    };

    return (
        <motion.div
            ref={ref}
            initial={false}
            animate={animations[animation]}
        >
            {children}
        </motion.div>
    );
};


export default LazyLoadAnimatedSection;

================================================
FILE: src/app/(app)/components/ResultIcon.tsx
================================================
import React, { useId } from "react";

// import noisePicture from "../assets/noise.inline.png";

// import { SettingsType } from "../lib/types";

type PropTypes = {
    //   settings: SettingsType;
    size?: number;
    isPreview?: boolean;
    // TODO: fix icon type?
    IconComponent?: React.FC<React.SVGProps<SVGSVGElement>>;
};

const ResultIcon = React.forwardRef<SVGSVGElement, PropTypes>(
    ({ settings, size = 512, isPreview, IconComponent }, svgRef) => {
        const strokeSize = isPreview ? 0 : settings.backgroundStrokeSize;
        const strokeWidth = isNaN(parseInt(strokeSize.toString())) ? 0 : parseInt(strokeSize.toString());

        const rectId = useId().replace(/:/g, "");
        const gradientId = useId().replace(/:/g, "");
        const radialGlareGradientId = useId().replace(/:/g, "");
        const gradientX = settings.backgroundPosition?.split(",")[0];
        const gradientY = settings.backgroundPosition?.split(",")[1];

        return (
            <svg
                ref={svgRef}
                width={size}
                height={size}
                viewBox={`0 0 ${size} ${size}`}
                fill="none"
                xmlns="http://www.w3.org/2000/svg"
                xmlnsXlink="http://www.w3.org/1999/xlink"
            >
                <rect
                    id={rectId}
                    width={size - strokeSize}
                    height={size - strokeSize}
                    x={strokeSize / 2}
                    y={strokeSize / 2}
                    rx={settings.backgroundRadius}
                    fill={settings.backgroundFillType === "Solid" ? settings.backgroundStartColor : `url(#${gradientId})`}
                    stroke={settings.backgroundStrokeColor}
                    strokeWidth={strokeWidth}
                    strokeOpacity={`${settings.backgroundStrokeOpacity}%`}
                    paintOrder="stroke"
                />

                {settings.backgroundRadialGlare ? (
                    <rect
                        width={size - strokeSize}
                        height={size - strokeSize}
                        x={strokeSize / 2}
                        y={strokeSize / 2}
                        fill={`url(#${radialGlareGradientId})`}
                        rx={settings.backgroundRadius}
                        style={{ mixBlendMode: "overlay" }}
                    />
                ) : null}

                {settings.backgroundNoiseTexture && !isPreview ? (
                    <image
                        // href={noisePicture as unknown as string}
                        width={size - strokeSize}
                        height={size - strokeSize}
                        x={strokeSize / 2}
                        y={strokeSize / 2}
                        clipPath="url(#clip)"
                        opacity={`${settings.backgroundNoiseTextureOpacity}%`}
                    />
                ) : null}
                <clipPath id="clip">
                    <use xlinkHref={`#${rectId}`} />
                </clipPath>

                <defs>
                    {settings.backgroundFillType === "Radial" ? (
                        <radialGradient
                            id={gradientId}
                            cx="50%"
                            cy="50%"
                            r="100%"
                            fx={gradientX}
                            fy={gradientY}
                            gradientUnits="objectBoundingBox"
                        >
                            <stop stopColor={settings.backgroundStartColor} />
                            <stop offset={settings.backgroundSpread / 100} stopColor={settings.backgroundEndColor} />
                        </radialGradient>
                    ) : (
                        <linearGradient
                            id={gradientId}
                            gradientUnits="userSpaceOnUse"
                            gradientTransform={`rotate(${settings.backgroundAngle})`}
                            style={{ transformOrigin: "center" }}
                        >
                            <stop stopColor={settings.backgroundStartColor} />
                            <stop offset="1" stopColor={settings.backgroundEndColor} />
                        </linearGradient>
                    )}
                    <radialGradient
                        id={radialGlareGradientId}
                        cx="0"
                        cy="0"
                        r="1"
                        gradientUnits="userSpaceOnUse"
                        gradientTransform={`translate(${size / 2}) rotate(90) scale(${size})`}
                    >
                        <stop stopColor="white" />
                        <stop offset="1" stopColor="white" stopOpacity="0" />
                    </radialGradient>
                </defs>

                {IconComponent ? (
                    <IconComponent
                        width={settings.iconSize}
                        height={settings.iconSize}
                        x={(size - settings.iconSize) / 2 + +settings.iconOffsetX}
                        y={(size - settings.iconSize) / 2 + +settings.iconOffsetY}
                        style={{ color: settings.iconColor }}
                        alignmentBaseline="middle"
                    />
                ) : null}
            </svg>
        );
    }
);

ResultIcon.displayName = "ResultIcon";

export default ResultIcon;

================================================
FILE: src/app/(app)/components/card-generator/color.tsx
================================================
import { cn } from "@lib/utils";
import { ColorChangeHandler, SketchPicker } from "react-color";
import ResultIcon from "../ResultIcon";

import { AccordionContent, AccordionTrigger, AccordionItem } from "@components/ui/accordion";
import { useCardStore } from "@src/hooks/useCardStore";
import { ColorPicker } from "@components/ui/color-picker";

export const presets: PresetType[] = [
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#FF7DB4",
        backgroundEndColor: "#654EA3",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#8E2DE2",
        backgroundEndColor: "#4A00E0",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#99F2C8",
        backgroundEndColor: "#1F4037",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#F953C6",
        backgroundEndColor: "#B91D73",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#91EAE4",
        backgroundEndColor: "#7F7FD5",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#F5AF19",
        backgroundEndColor: "#F12711",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#EAAFC8",
        backgroundEndColor: "#EC2F4B",
        backgroundAngle: 45,
    },

    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#00B4DB",
        backgroundEndColor: "#003357",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#A8C0FF",
        backgroundEndColor: "#3F2B96",
        backgroundAngle: 90,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#DD1818",
        backgroundEndColor: "#380202",
        backgroundAngle: 135,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#DECBA4",
        backgroundEndColor: "#3E5151",
        backgroundAngle: 45,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#FC466B",
        backgroundEndColor: "#3F5EFB",
        backgroundAngle: 180,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#CCCFE2",
        backgroundEndColor: "#25242B",
        backgroundAngle: 180,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#68AEFF",
        backgroundEndColor: "#003EB7",
        backgroundAngle: 180,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#C9D6FF",
        backgroundEndColor: "#596AA1",
        backgroundAngle: 180,
    },
    {
        backgroundFillType: "Linear",
        backgroundStartColor: "#5C5C5C",
        backgroundEndColor: "#0F1015",
        backgroundAngle: 180,
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#695BF8",
        backgroundEndColor: "#131308",
        backgroundPosition: "50%,0%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#4d4d4d",
        backgroundEndColor: "#000000",
        backgroundPosition: "50%,0%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#f5af19",
        backgroundEndColor: "#f12711",
        backgroundPosition: "50%,50%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#1D6E47",
        backgroundEndColor: "#041B11",
        backgroundPosition: "50%,0%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#ffffff",
        backgroundEndColor: "#666666",
        backgroundPosition: "50%,100%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#d9f1f8",
        backgroundEndColor: "#002069",
        backgroundPosition: "50%,100%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#f95356",
        backgroundEndColor: "#7e0000",
        backgroundPosition: "50%,50%",
    },
    {
        backgroundFillType: "Radial",
        backgroundStartColor: "#ffbb00",
        backgroundEndColor: "#ffe74b",
        backgroundPosition: "50%,0%",
    },
];

type ColorInputPropTypes = {
    value: string;
    name: string;
    recentColors: string[];
    onChange: ColorChangeHandler;
    disabled?: boolean;
};


export const ColorSelect = () => {

    const colorIndex = useCardStore(state => state.colorIndex);
    const setColorIndex = useCardStore(state => state.setColorIndex);
    const updateCardStyles = useCardStore((state) => state.updateCardStyles);
    // const filStyles = useCardStore(state => state.filStyles);


    return (
        <div className={cn('flex flex-wrap gap-4 px-2 py-2')}>

            {presets.map((preset, index) => {
                return (
                    <label key={index}
                        className={cn(
                            'relative overflow-hidden w-5 h-5 shrink-0 rounded-[5px] ',
                            colorIndex === index ? "dark:ring-primary-500 dark:ring-opacity-100 dark:ring-2 dark:ring-offset-2 dark:ring-offset-gray-700 ring-primary-500 ring-2 ring-offset-2 ring-offset-white  inline-flex  active:scale-95 transition  " : ""

                        )}
                    >
                        <input
                            className={cn(
                                "absolute w-full h-full appearance-none opacity-0 inset-0 cursor-pointer",
                            )}
                            type="radio"
                            name="preset"
                            value={index}
                            checked={colorIndex === index}
                            onChange={() => {
                                setColorIndex(index);
                            }}
                        />

                        <ResultIcon size={20} isPreview settings={{ ...preset, backgroundRadius: 0 }} />
                    </label>
                );
            })}
            {/* <ColorPicker className=" w-[20px] h-[20px]" value={cardStyles.borderColor}
                onChange={(v) => {
                    updateCardStyles({
                        borderColor: v,
                    });
                }}
            ></ColorPicker> */}
        </div>

    )
}

================================================
FILE: src/app/(app)/components/card-generator/controller/background-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { ColorPicker } from "@components/ui/color-picker";
import { ColorSelect } from "../color";
import { Switch } from "@components/ui/switch";
import { useCardStore } from "@src/hooks/useCardStore";
import { Slider } from "@components/ui/slider";
import { Input } from "@components/ui/input";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";

export const BackgroundController = (props) => {

    const tabConfig = useCardStore((state) => state.tabConfig);
    const setTabConfig = useCardStore((state) => state.setTabConfig);
    const backgroundStyles = useCardStore((state) => state.backgroundStyles);
    const updateBackgroundStyles = useCardStore((state) => state.updateBackgroundStyles);


    const handleImageUpload = (event) => {
        const file = event.target.files[0];
        if (file) {
            const reader = new FileReader();
            reader.onload = (e) => updateBackgroundStyles({
                backgroundImage: e.target.result as string,
            })
            reader.readAsDataURL(file);
        }
    };

    return (
        <AccordionItem value={'background'}>
            <AccordionTrigger>Background</AccordionTrigger>
            <AccordionContent>
                {/* preset color */}
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Preset Color</span>
                    <div className=" w-full">
                        <ColorSelect></ColorSelect>
                    </div>
                </label>
                {/* custom Color */}
                <div>
                    <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className="grow-[2] text-[13px]">Custom Color</span>
                        <div className="inline-flex">
                            <Switch checked={tabConfig.openCustomColor}
                                onCheckedChange={v => setTabConfig({ openCustomColor: v })}
                            />
                        </div>
                    </label>
                    <AnimatePresence>
                        {tabConfig?.openCustomColor && <motion.div
                            initial={{ opacity: 0 }}
                            animate={{ opacity: 1 }}
                            exit={{ opacity: 0 }}
                        >
                            <div className="py-2">
                                <ColorPicker className=" w-[20px] h-[20px]" value={backgroundStyles.backgroundColor}
                                    onChange={(v) => {
                                        updateBackgroundStyles({
                                            backgroundColor: v,
                                        });
                                    }}
                                ></ColorPicker>
                            </div>
                            {/* Background Opacity */}
                            <label className="flex flex-col gap-y-2">
                                <span className="text-[13px]">Background Opacity</span>
                                <div className="py-2">
                                    <Slider
                                        min={0}
                                        max={1}
                                        step={0.1}
                                        value={[backgroundStyles.backgroundOpacity]}
                                        onValueChange={(value) => {
                                            updateBackgroundStyles({
                                                backgroundOpacity: value,
                                            })
                                        }}
                                    />
                                </div>
                            </label>
                            {/* Background Blur  */}
                            <label className="flex flex-col gap-y-2">
                                <span className="text-[13px]">Background Blur</span>
                                <div className="py-2">
                                    <Slider
                                        min={0}
                                        max={20}
                                        step={1}
                                        value={[backgroundStyles.backgroundBlur]}
                                        onValueChange={(value) => updateBackgroundStyles({
                                            backgroundBlur: value,
                                        })}
                                    />
                                </div>
                            </label>
                        </motion.div>}
                    </AnimatePresence>


                    {/* Background Gradient */}
                    <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
                        <span className="grow-[2] text-[13px]">Use Gradient</span>
                        <div className="inline-flex">
                            <Switch
                                checked={backgroundStyles.useGradient}
                                onCheckedChange={(v) => updateBackgroundStyles({ useGradient: v })}
                            />
                        </div>
                    </label>
                    <AnimatePresence>
                        {backgroundStyles.useGradient && (
                            <motion.div
                                initial={{ opacity: 0 }}
                                animate={{ opacity: 1 }}
                                exit={{ opacity: 0 }}
                            >
                                <label className="flex flex-col gap-y-2">
                                    <span className="text-[13px]">Gradient Angle</span>
                                    <div className="py-2">
                                        <Slider
                                            min={0}
                                            max={360}
                                            step={1}
                                            value={[backgroundStyles.backgroundGradientAngle]}
                                            onValueChange={(value) => updateBackgroundStyles({ backgroundGradientAngle: value[0] })}
                                        />
                                    </div>
                                </label>
                                <label className="flex flex-col gap-y-2">
                                    <span className="text-[13px]">Gradient Start Color</span>
                                    <ColorPicker
                                        value={backgroundStyles.backgroundStartColor}
                                        onChange={(v) => updateBackgroundStyles({ backgroundStartColor: v })}
                                    />
                                </label>
                                <label className="flex flex-col gap-y-2">
                                    <span className="text-[13px]">Gradient End Color</span>
                                    <ColorPicker
                                        value={backgroundStyles.backgroundEndColor}
                                        onChange={(v) => updateBackgroundStyles({ backgroundEndColor: v })}
                                    />
                                </label>
                            </motion.div>
                        )}
                    </AnimatePresence>
                </div>
                {/* preset  Image */}
                <label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
                    <span className="text-[13px]">Image</span>
                    <div className="w-full">
                        <Input
                            onChange={handleImageUpload}
                            accept="image/*"
                            type="file"
                            multiple
                        />
                    </div>
                </label>



                {/* <label className="flex flex-col gap-y-2">
                    <span className="text-[13px]">Background Position</span>
                    <Select
                        value={backgroundStyles.backgroundPosition}
                        onValueChange={(value) => updateBackgroundStyles({ backgroundPosition: value })}
                    >
                        <Select.Option value="center center">Center</Select.Option>
                        <Select.Option value="top left">Top Left</Select.Option>
                        <Select.Option value="top right">Top Right</Select.Option>
                        <Select.Option value="bottom left">Bottom Left</Select.Option>
                        <Select.Option value="bottom right">Bottom Right</Select.Option>
                    </Select>
                </label> */}

                {/* Background Repeat */}
                {/* <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
                    <span className="grow-[2] text-[13px]">Background Repeat</span>
                    <div className="inline-flex">
                        <Switch
                            checked={backgroundStyles.backgroundRepeat === 'repeat'}
                            onCheckedChange={(v) => updateBackgroundStyles({ backgroundRepeat: v ? 'repeat' : 'no-repeat' })}
                        />
                    </div>
                </label> */}




                {/* Background Width */}
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Width</span>
                    <div className=" w-full">
                        <Slider step={1}
                            value={[backgroundStyles.backgroundWidth]}
                            max={100}
                            min={50}
                            onValueChange={(v) => {
                                updateBackgroundStyles({
                                    backgroundWidth: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>
                {/* background padding */}
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Padding</span>
                    <div className=" w-full">
                        <Slider step={1}
                            value={[backgroundStyles.padding]}
                            max={100}
                            min={0}
                            onValueChange={(v) => {
                                updateBackgroundStyles({
                                    padding: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>
                {/* <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Noise</span>
                    <div className="inline-flex">
                        <Switch checked={cardStyles.hasNoiseTexture}
                            onCheckedChange={v => updateCardStyles({ hasNoiseTexture: v })}
                        />
                    </div>
                </label>
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Opacity</span>
                    <div className=" w-full">
                        <Slider step={0.01}
                            value={[cardStyles.noiseTextureOpacity]}
                            max={1}
                            min={0}
                            onValueChange={(v) => {
                                console.log('v', v);
                                updateCardStyles({
                                    noiseTextureOpacity: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>
                <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Noise</span>
                    <SelectBackgroundPosition onChange={v => {
                        updateCardStyles({
                            texturePosition: v,
                        });
                    }}></SelectBackgroundPosition>
                </label> */}
            </AccordionContent>
        </AccordionItem>
    )
}

================================================
FILE: src/app/(app)/components/card-generator/controller/card-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { ColorPicker } from "@components/ui/color-picker";
import { Slider } from "@components/ui/slider";
import { useCardStore } from "@src/hooks/useCardStore";
import { useState } from "react";
import { RadioGroup, RadioGroupItem } from "@components/ui/radio-group";
import LayoutOptions from "@src/components/extension/layout-options";
export const CardController = () => {
    const cardStyles = useCardStore((state) => state.cardStyles);
    const updateCardStyles = useCardStore((state) => state.updateCardStyles);

    const [selectedFiles, setSelectedFiles] = useState<File[]>([]);


    const handleImageUpload = (event) => {
        // const file = event.target.files[0];
        // if (file) {
        //     const reader = new FileReader();
        //     reader.onload = (e) => updateBackgroundStyles({
        //         backgroundImage: e.target.result as string,
        //     })
        //     reader.readAsDataURL(file);
        // }
    };

    const handleLayoutChange = () => {

    }

    return (
        <AccordionItem value={'card'}>
            <AccordionTrigger>Card</AccordionTrigger>
            <AccordionContent className="">

                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Width</span>
                    <div className=" w-full">
                        <Slider step={1}
                            value={[cardStyles.width]}
                            max={1080}
                            min={379}
                            onValueChange={(v) => {
                                updateCardStyles({
                                    width: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>

                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]"> Scale</span>
                    <div className=" w-full">
                        <Slider step={1}
                            value={[cardStyles.scale]}
                            max={150}
                            min={1}
                            onValueChange={(v) => {
                                console.log('v', v);
                                updateCardStyles({
                                    scale: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>


                <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Border Width</span>
                    <div className=" w-full">
                        <Slider step={1}
                            value={[cardStyles.borderWidth]}
                            max={10}
                            min={0}
                            onValueChange={(v) => {
                                updateCardStyles({
                                    borderWidth: v[0],
                                });
                            }}

                        ></Slider>
                    </div>
                </label>
                <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Border Color</span>
                    <ColorPicker className=" w-[20px] h-[20px]" value={cardStyles.borderColor}
                        onChange={(v) => {
                            updateCardStyles({
                                borderColor: v,
                            });
                        }}
                    ></ColorPicker>
                </label>

                <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Style</span>
                    <RadioGroup value={cardStyles.style} onValueChange={(v) => {
                        updateCardStyles({
                            style: v,
                        });
                    }}>
                        <div className="flex items-center space-x-2">
                            <RadioGroupItem value="article" id="article" />
                            <span>article</span>
                        </div>
                        <div className="flex items-center space-x-2">
                            <RadioGroupItem value="posts" id="posts" />
                            <span>posts</span>
                        </div>
                    </RadioGroup>
                </label>

                <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Border Radius</span>
                    <Slider step={1}
                        value={[cardStyles.borderRadius]}
                        min={0}
                        onValueChange={(v) => {
                            updateCardStyles({
                                borderRadius: v[0],
                            });
                        }}

                    ></Slider>
                </label>

                {/* <label className="flex flex-col min-h-[40px] justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className="grow-[2] text-[13px]">Resize</span>
                    <LayoutOptions onSelect={(option) => {
                        updateCardStyles({
                            width: option.dimensions.width,
                            height: option.dimensions.height,
                        })
                    }}></LayoutOptions>
                </label> */}



                {selectedFiles.length > 0 && (
                    <div className="mt-2">
                        <p className="text-sm mb-1">Selected files:</p>
                        <ul className="list-disc list-inside">
                            {selectedFiles.map((file, index) => (
                                <li key={index} className="text-sm">{file.name}</li>
                            ))}
                        </ul>
                    </div>
                )}


                <label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
                    <span className="text-[13px]">Image Layout</span>
                    <div className="w-full">
                        <RadioGroup value={cardStyles.imageLayout} onValueChange={(v) => {
                            updateCardStyles({
                                imageLayout: v,
                            })
                        }}>
                            {[
                                'vertical',
                                'grid2',
                                'grid4',
                            ].map((layout) => (
                                <div key={layout} className="flex items-center space-x-2">
                                    <RadioGroupItem value={layout} id={layout} />
                                    <span>{layout}</span>
                                </div>
                            ))}
                        </RadioGroup>
                    </div>
                </label>

            </AccordionContent>
        </AccordionItem>
    )

}

================================================
FILE: src/app/(app)/components/card-generator/controller/font-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { Slider } from "@components/ui/slider";
import { useCardStore } from "@src/hooks/useCardStore";
import GoogleFontSelector from "../../GoogleFontSelector";

interface FontControllerProps {

}

const googleFonts = [
    { name: 'Default', value: 'sans-serif' },
    { name: 'Roboto', value: "'Roboto', sans-serif" },
    { name: 'Open Sans', value: "'Open Sans', sans-serif" },
    { name: 'Lato', value: "'Lato', sans-serif" },
    { name: 'Montserrat', value: "'Montserrat', sans-serif" },
    { name: 'Noto Sans SC', value: "'Noto Sans SC', sans-serif" }, // 中文字体
];

export const FontController = (props: FontControllerProps) => {
    const updateCardStyles = useCardStore((state) => state.updateCardStyles);
    const cardStyles = useCardStore((state) => state.cardStyles);
    return (
        <AccordionItem value={'Font'}>
            <AccordionTrigger>Font</AccordionTrigger>
            <AccordionContent className="">
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">font-size</span>
                    <div className=" w-full">
                        <Slider step={0.5}
                            value={[cardStyles.fontSize]}
                            max={24}
                            min={12}
                            onValueChange={(v) => {
                                updateCardStyles({
                                    fontSize: v[0],
                                });
                            }}
                        ></Slider>
                    </div>
                </label>
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">Select Font</span>
                    <div className=" w-full">
                        <GoogleFontSelector></GoogleFontSelector>
                    </div>
                </label>

            </AccordionContent>
        </AccordionItem>
    )
}

================================================
FILE: src/app/(app)/components/card-generator/controller/iframe-controller.tsx
================================================
 {/* <AccordionItem value={'frames'}>
                        <AccordionTrigger>Frames</AccordionTrigger>
                        <AccordionContent>
                            <div className="py-2">
                                <div className=" max-w-xl  rounded-xl  flex flex-col">
                                    <div className="max-h-96 overflow-y-auto">
                                        <div className="  grid  grid-cols-3 gap-4  items-center p-4">
                                            <button
                                                name="None"
                                                className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray  transition-all ease-in-out active:scale-95 "
                                            >
                                                <svg
                                                    stroke="currentColor"
                                                    fill="currentColor"
                                                    strokeWidth={0}
                                                    viewBox="0 0 16 16"
                                                    className="w-10 h-10"
                                                    height="1em"
                                                    width="1em"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                >
                                                    <path d="M2.5 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1ZM4 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1Zm2-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Z" />
                                                    <path d="M0 4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v4a.5.5 0 0 1-1 0V7H1v5a1 1 0 0 0 1 1h5.5a.5.5 0 0 1 0 1H2a2 2 0 0 1-2-2V4Zm1 2h13V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2Z" />
                                                    <path d="M16 12.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Zm-4.854-1.354a.5.5 0 0 0 0 .708l.647.646-.647.646a.5.5 0 0 0 .708.708l.646-.647.646.647a.5.5 0 0 0 .708-.708l-.647-.646.647-.646a.5.5 0 0 0-.708-.708l-.646.647-.646-.647a.5.5 0 0 0-.708 0Z" />
                                                </svg>
                                                <span className="text-sm">None</span>
                                            </button>
                                            <button
                                                name="MacOS"
                                                className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray  transition-all ease-in-out active:scale-95 "
                                            >
                                                <svg
                                                    stroke="currentColor"
                                                    fill="currentColor"
                                                    strokeWidth={0}
                                                    viewBox="0 0 16 16"
                                                    className="w-10 h-10"
                                                    height="1em"
                                                    width="1em"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                >
                                                    <path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
                                                    <path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
                                                </svg>
                                                <span className="text-sm">MacOS</span>
                                            </button>
                                            <button
                                                name="Windows"
                                                className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray  transition-all ease-in-out active:scale-95 "
                                            >
                                                <svg
                                                    stroke="currentColor"
                                                    fill="currentColor"
                                                    strokeWidth={0}
                                                    viewBox="0 0 16 16"
                                                    className="w-10 h-10"
                                                    height="1em"
                                                    width="1em"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                >
                                                    <path d="M6.555 1.375 0 2.237v5.45h6.555V1.375zM0 13.795l6.555.933V8.313H0v5.482zm7.278-5.4.026 6.378L16 16V8.395H7.278zM16 0 7.33 1.244v6.414H16V0z" />
                                                </svg>
                                                <span className="text-sm">Windows</span>
                                            </button>
                                            <button
                                                name="Fimojis"
                                                className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray  transition-all ease-in-out active:scale-95 "
                                            >
                                                <svg
                                                    stroke="currentColor"
                                                    fill="currentColor"
                                                    strokeWidth={0}
                                                    viewBox="0 0 16 16"
                                                    className="w-10 h-10"
                                                    height="1em"
                                                    width="1em"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                >
                                                    <path d="M4.968 9.75a.5.5 0 1 0-.866.5A4.498 4.498 0 0 0 8 12.5a4.5 4.5 0 0 0 3.898-2.25.5.5 0 1 0-.866-.5A3.498 3.498 0 0 1 8 11.5a3.498 3.498 0 0 1-3.032-1.75zM7 5.116V5a1 1 0 0 0-1-1H3.28a1 1 0 0 0-.97 1.243l.311 1.242A2 2 0 0 0 4.561 8H5a2 2 0 0 0 1.994-1.839A2.99 2.99 0 0 1 8 6c.393 0 .74.064 1.006.161A2 2 0 0 0 11 8h.438a2 2 0 0 0 1.94-1.515l.311-1.242A1 1 0 0 0 12.72 4H10a1 1 0 0 0-1 1v.116A4.22 4.22 0 0 0 8 5c-.35 0-.69.04-1 .116z" />
                                                    <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-1 0A7 7 0 1 0 1 8a7 7 0 0 0 14 0z" />
                                                </svg>
                                                <span className="text-sm">Fimojis</span>
                                            </button>
                                            <button
                                                name="Emojis"
                                                className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray  transition-all ease-in-out active:scale-95 "
                                            >
                                                <svg
                                                    stroke="currentColor"
                                                    fill="currentColor"
                                                    strokeWidth={0}
                                                    viewBox="0 0 16 16"
                                                    className="w-10 h-10"
                                                    height="1em"
                                                    width="1em"
                                                    xmlns="http://www.w3.org/2000/svg"
                                                >
                                                    <path d="M8 16c3.314 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .25 1.5-1.25 2-1.25 2C11 4 9 .5 6 0c.357 2 .5 4-2 6-1.25 1-2 2.729-2 4.5C2 14 4.686 16 8 16Zm0-1c-1.657 0-3-1-3-2.75 0-.75.25-2 1.25-3C6.125 10 7 10.5 7 10.5c-.375-1.25.5-3.25 2-3.5-.179 1-.25 2 1 3 .625.5 1 1.364 1 2.25C11 14 9.657 15 8 15Z" />
                                                </svg>
                                                <span className="text-sm">Emojis</span>
                                            </button>
                                        </div>
                                    </div>
                                </div>

                            </div>
                        </AccordionContent>
                    </AccordionItem> */}

================================================
FILE: src/app/(app)/components/card-generator/controller/input-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { Input } from "@components/ui/input";
import { Textarea } from "@components/ui/textarea";
import { useCardStore } from "@src/hooks/useCardStore";

export const InputController = () => {
    const xConfig = useCardStore(state => state.xConfig);
    return (
        <div>
            <AccordionItem value={'Input'}>
                <AccordionTrigger>Input</AccordionTrigger>
                <AccordionContent className="">
                    <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className=" text-[13px]">username</span>
                        <div className=" px-1.5">
                            <Input maxLength={200} minLength={2} value={xConfig.username} onChange={(e) => {
                                useCardStore.setState({
                                    xConfig: {
                                        ...xConfig,
                                        username: e.target.value
                                    }
                                })
                            }}></Input>
                        </div>
                    </label>
                    <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className=" text-[13px]">text</span>
                        <div className=" px-1.5">
                            <Textarea rows={10} value={xConfig.text} onChange={(e) => {
                                useCardStore.setState({
                                    xConfig: {
                                        ...xConfig,
                                        text: e.target.value
                                    }
                                })
                            }}></Textarea>
                        </div>
                    </label>
                  
                    <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className=" text-[13px]">replies</span>
                        <div className=" px-1.5">
                            <Input type="number" value={xConfig.replies} onChange={(e) => {
                                useCardStore.setState({
                                    xConfig: {
                                        ...xConfig,
                                        replies: Number(e.target.value)
                                    }
                                })
                            }}></Input>
                        </div>
                    </label>
                    <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className=" text-[13px]">shares</span>
                        <div className=" px-1.5">
                            <Input type="number" value={xConfig.shares} onChange={(e) => {
                                useCardStore.setState({
                                    xConfig: {
                                        ...xConfig,
                                        shares: Number(e.target.value)
                                    }
                                })
                            }}></Input>
                        </div>
                    </label>
                    <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                        <span className=" text-[13px]">shares</span>
                        <div className=" px-1.5">
                            <Input type="number" value={xConfig.likes} onChange={(e) => {
                                useCardStore.setState({
                                    xConfig: {
                                        ...xConfig,
                                        likes: Number(e.target.value)
                                    }
                                })
                            }}></Input>
                        </div>
                    </label>
                </AccordionContent>
            </AccordionItem>
        </div>
    )
}

================================================
FILE: src/app/(app)/components/card-generator/display.tsx
================================================
import { CommonLayouts, useCardStore } from "@src/hooks/useCardStore"
import { useMemo } from "react"


import { presets } from "./color"
import { TwitterCard } from "./twitter-card"
import { WeChatCard } from "./wechat-card"

export const Display = () => {
    const colorIndex = useCardStore((state) => state.colorIndex)
    const cardStyles = useCardStore((state) => state.cardStyles)
    const tabConfig = useCardStore((state) => state.tabConfig);
    const backgroundStyles = useCardStore((state) => state.backgroundStyles)
    const xConfig = useCardStore((state) => state.xConfig)

    const finalBackgroundStyles = useMemo(() => {
        const color = presets[colorIndex]

        let borderRadius = cardStyles.borderRadius;
        if (backgroundStyles.padding === 0) {
            borderRadius = 0;
        }

        if (backgroundStyles?.useGradient) {
            return {
                ...backgroundStyles,
                borderRadius,
                backgroundImage: `linear-gradient(${backgroundStyles.backgroundGradientAngle}deg, ${backgroundStyles.backgroundStartColor}, ${backgroundStyles.backgroundEndColor})`,
            }
        }

        if (tabConfig.openCustomColor) {
            return {
                ...backgroundStyles,
                borderRadius,
                backgroundColor: backgroundStyles.backgroundColor,
            }

        }
        if (color.backgroundFillType === "Linear") {
            return {
                ...backgroundStyles,
                borderRadius,
                backgroundImage: `linear-gradient(${color.backgroundAngle}deg, ${color.backgroundStartColor}, ${color.backgroundEndColor})`,
                backgroundRepeat: "no-repeat",
            }
        } else if (color.backgroundFillType === "Radial") {
            return {
                ...backgroundStyles,
                borderRadius,
                backgroundImage: `radial-gradient(${color.backgroundStartColor}, ${color.backgroundEndColor})`,
                backgroundRepeat: "no-repeat",
                backgroundPosition: color.backgroundPosition,

            }
        } else {
            return {
                ...backgroundStyles,
                borderRadius,
            }
        }
    }, [colorIndex, backgroundStyles, tabConfig.openCustomColor])

    const calculateScale = (width, height) => {
        const maxWidth = 1920 / 2; // Maximum width of the card in the display
        const maxHeight = 1080 / 2; // Maximum height of the card in the display
        const widthScale = maxWidth / width;
        const heightScale = maxHeight / height;
        return Math.min(widthScale, heightScale);
    };

    return (
        <div
            className="w-full  justify-center  flex  gap-x-4 relative mt-0">

            <TwitterCard
                xConfig={xConfig}
                cardStyles={{
                    ...cardStyles,
                    // aspectRatio: "16/9",
                    // width: '1920px',
                    // height: '1080px',
                }}
                backgroundStyles={finalBackgroundStyles}
            ></TwitterCard>

            {/* <WeChatCard xConfig={xConfig}></WeChatCard> */}
        </div>
    )
}





================================================
FILE: src/app/(app)/components/card-generator/export-tab.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";

import { Button } from "@components/ui/button";
import { exportImage } from "@src/app/utils/export";
export const ExportTab = () => {


    return (
        <AccordionItem value={'export'} className=" border  border-destructive ">
            <AccordionTrigger className="">Export </AccordionTrigger>
            <AccordionContent className=" ">
                <label className="flex min-h-[40px] flex-col gap-y-2  justify-start transition-opacity duration-[0.15s] ease-[ease-in-out]  py-1">
                    <span className=" text-[13px]">image type</span>
                    <div className="mt-4 flex space-x-2">
                        <Button className="" size="sm" onClick={() => exportImage('png')}>As PNG</Button>
                        <Button size="sm" onClick={() => exportImage('jpeg')}>As JPEG</Button>
                        <Button size="sm" onClick={() => exportImage('svg')}>As SVG</Button>
                    </div>
                </label>

            </AccordionContent>
        </AccordionItem>
    )
}

================================================
FILE: src/app/(app)/components/card-generator/index.tsx
================================================
import { Accordion, } from "@components/ui/accordion";
import { Display } from "./display";
import { useCardStore } from "@src/hooks/useCardStore";
import { BackgroundController } from "./controller/background-controller";
import { FontController } from "./controller/font-controller";
import { ExportTab } from "./export-tab";
import { Button } from "@components/ui/button";
import { InputController } from "./controller/input-controller";
import { SaveAsTemplateButton } from "../save-as-template-button";
import { CardController } from "./controller/card-controller";

export const CardGenerator = () => {
    const resetAll = useCardStore((state) => state.resetAll);

    return (

        <div className="flex   mx-auto  w-full   md:py-5 px-0 md:px-6">
            <Display></Display>
            <div className="flex-1 min-w-[320px]  h-[578px] px-4 pb-4 overflow-auto  sm:block hidden ">
                <div className="py-2 flex gap-x-2">
                    <Button onClick={() => resetAll()} size="sm" variant="secondary">Reset</Button>
                    <SaveAsTemplateButton></SaveAsTemplateButton>

                </div>
                <Accordion type="multiple" className="w-full">
                    <ExportTab></ExportTab>
                    <InputController></InputController>
                    <BackgroundController></BackgroundController>

                    <CardController></CardController>
                    <FontController></FontController>
                </Accordion>
            </div>
        </div >
    )
}

================================================
FILE: src/app/(app)/components/card-generator/twitter-card.tsx
================================================
import React, { useMemo, useState } from 'react';
import { cn } from '@/lib/utils'; // Assuming you have a utility for class names
import ImageLayout from '../ImageLayout';
import XLogo from '@assets/x-logo.svg';
import * as _ from "lodash-es"
import { formatTimestamp } from '@src/app/utils/format';
import type { CardStore, XConfig } from '@src/hooks/useCardStore';

interface TwitterCardProps {
    xConfig: XConfig[],
    backgroundStyles: CardStore['backgroundStyles'],
    cardStyles: CardStore['cardStyles'],
}

export const TwitterCard: React.FC<TwitterCardProps> = ({ xConfig, backgroundStyles, cardStyles }) => {
    const card = useMemo(() => {
        const controls = cardStyles.controls;
        return (
            <div className='flex flex-col gap-y-4 relative'>
                {
                    xConfig.map((config, index) => (
                        <div className="flex flex-col h-full" key={`xConfig-${index}`}>
                            <CardHeader xConfig={config} controls={controls} />
                            <CardBody xConfig={config} cardStyles={cardStyles} />
                            {controls.showFooter && (<CardFooter xConfig={config} />)}
                        </div>
                    ))
                }
                <div className=' absolute right-0 bottom-0 opacity-40 text-[##6d6d6d]'>
                    ∙ Made with x-cards.net
                </div>
            </div>
        )
    }, [backgroundStyles, xConfig, cardStyles]);

    return (
        <div
            id="card"
            //  aspect-video
            className="flex h-fit"
            style={{
                boxShadow: 'rgba(245, 208, 254, 0.3) 0px 0px 200px',
                width: cardStyles.width,
            }}
        >
            <div className="relative w-full grid place-items-center mobile-scaling pointer-events-none">

                <div className=" overflow-hidden w-full h-full relative content-shadow">
                    <div
                        id="content"
                        className="grid place-items-center content-container transition-colors h-full w-full"
                        style={{
                            padding: backgroundStyles.padding || 0,
                            fontSize: 14,
                            perspective: 1000
                        }}
                    >
                        {/* Background layers */}
                        <BackgroundLayers backgroundStyles={backgroundStyles} cardStyles={cardStyles} />

                        {/* Card container */}
                        <div
                            className="relative z-20 transition-all w-full card-holder"
                            style={{
                                transform: `scale(${cardStyles.scale}%)`,
                                // pointer-events: none;
                                // fontFamily: cardStyles?.fontFamily && `'${cardStyles?.fontFamily}', sans-serif`,
                                // fontFamily: 'ui-sans-serif',
                            }}
                        >
                            {/* Card body */}
                            <div
                                className={cn(
                                    "select-none relative transition-all",
                                    "h-full w-full backdrop-blur-[18px] backdrop-saturate-[177%] pt-[2em] pb-[1.5em] px-[2em]",
                                    'card-background-light transition-colors  inset-0 rounded-[inherit]',
                                    // absolute
                                )}

                                style={{
                                    overflow: 'visible',
                                    zIndex: -1,
                                    background: `linear-gradient(150deg, rgba(255,255,255,0.5), rgba(255,255,255,0.95) 80%)`,
                                    // boxShadow: 'inset 0 0 0 2px rgba(255,255,255,0.15)',
                                    borderRadius: `${backgroundStyles.borderRadius}px`,
                                }}
                            >
                                {card}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div >
    );
};


const BackgroundLayers = ({ backgroundStyles, cardStyles }) => (
    <>
        <div
            className="absolute inset-0"
            style={{
                ..._.omit(backgroundStyles, 'borderRadius'),
                backgroundRepeat: "no-repeat",
            }}
        />
        {backgroundStyles?.backgroundImage && (
            <div
                className="absolute inset-0"
                style={{
                    backgroundRepeat: backgroundStyles.backgroundRepeat,
                    backgroundImage: `url(${backgroundStyles.backgroundImage})`,
                    filter: `blur(${backgroundStyles.backgroundBlur}px)`,
                    opacity: backgroundStyles.backgroundOpacity,
                }}
            />
        )}
        {cardStyles.hasNoiseTexture && (
            <div
                className="absolute inset-0 mix-blend-overlay"
                style={{
                    backgroundImage: `url(/noise1.png)`, // Assuming noise1 is now a public asset
                    backgroundRepeat: "repeat",
                    opacity: cardStyles.noiseTextureOpacity,
                    backgroundPosition: cardStyles.texturePosition,
                    zIndex: 10,
                    borderRadius: `${cardStyles.borderRadius}px`,
                }}
            />
        )}
    </>
);

const CardHeader: React.FC<{
    xConfig: XConfig,
    controls: CardStore['cardStyles']['controls'],
}> = ({ xConfig, controls }) => {

    return (
        <div className='flex w-full'>
            {controls.showUser ? (<div className="flex  flex-grow items-center pb-3">
                <img
                    src={xConfig?.avatar || ''}
                    className="inline object-cover rounded-full transition-all duration-150"
                    alt="Profile image"
                    style={{
                        width: "3em",
                        height: "3em",
                        marginRight: "0.75em"
                    }}
                />
                <div>
                    <div className="flex text-secondary-foreground" style={{ fontWeight: 600, lineHeight: "1.2" }}>
                        <div className="whitespace-nowrap" style={{ paddingRight: "0.375em", fontSize: 18 }}>
                            {xConfig.username}
                        </div>
                    </div>
                    <div className="whitespace-nowrap text-secondary" style={{ fontSize: "1em", fontWeight: 400, lineHeight: "1.2" }} />
                </div>
            </div>) : <div className='flex w-full pb-3'></div>}
            {
                controls.showLogo ? <CardLogo xConfig={xConfig} /> : <></>
            }
        </div>
    );
}

const CardLogo = ({ xConfig }) => {
    return (<div className='flex justify-center'>
        <img
            id="x-logo"
            src={XLogo.src}
            alt=""
            className=" h-[20px] w-[20px] right-4 top-4 opacity-30 dark:invert saturate-0 cursor-alias active:scale-90 transition-all ease-in-out"
        />
    </div>
    )
}

const Watermark = () => {

}

const CardBody: React.FC<{
    xConfig: XConfig,
    cardStyles: CardStore['cardStyles'],
}> = ({ xConfig, cardStyles }) => {
    //  cardStyles.imageLayout ||
    const layout = xConfig.images.length >= 2 ? 'grid4' : 'vertical';

    const images = _.compact(
        [
            ...xConfig.images,
            xConfig?.video?.poster,
            ...(xConfig?.links?.map((link) => link.src) || []),
        ]
    )

    return (
        <div className="flex-grow flex flex-col justify-center">
            <div className={cn("tweet-content text-lg leading-normal pointer-events-none mb-[1em]    ")} style={{ fontSize: cardStyles.fontSize, overflowWrap: 'anywhere' }}>
                <div className="content whitespace-pre-wrap" dir="auto">
                    {xConfig.text}
                </div>
            </div>
            {images && images.length > 0 && (
                <ImageLayout
                    images={images}
                    layout={layout}
                />
            )}
        </div>
    )
}

const CardFooter = ({ xConfig }) => {
    const time = _.isArray(xConfig) ? _.last(xConfig).time : xConfig.time;
    return (
        <div>
            <div className="text-secondary-foreground flex items-center " style={{ paddingBottom: "0.5em", paddingTop: '0.5rem' }}>
                <span>{formatTimestamp(time)}</span>

            </div>
            <div className='flex  items-center justify-between'>
                <div className="flex">
                    {['replies', 'shares', 'likes'].map((stat) => (
                        <div key={stat} className="whitespace-nowrap text-secondary-foreground" style={{ marginRight: "1em" }}>
                            <span className="font-medium" style={{ color: "var(--textPrimary)" }}>
                                {xConfig[stat]}
                            </span>{" "}
                            {stat}
                        </div>
                    ))}
                </div>
            </div>
        </div>
    );
}

================================================
FILE: src/app/(app)/components/card-generator/wechat-card.tsx
================================================
import type { CardStore, XConfig } from "@src/hooks/useCardStore"

interface WechatCardProps {
    xConfig: XConfig,
    backgroundStyles: CardStore['backgroundStyles'],
    cardStyles: CardStore['cardStyles'],
    fontStyles: CardStore['fontStyles'],
}

export const WeChatCard: React.FC<WechatCardProps> = () => {
    return (
        <div
            name="tempB"
            className="sm:w-full w-[100vw] bg-[#1B1C1E] text-[#E3D2B3] flex flex-col justify-between tempB"
            style={{
                transition: "padding 500ms",
                padding: 30,
                fontFamily: "SourceHanSerifCN_SemiBold",
                minHeight: "auto"
            }}
        >
            <div>
                <div
                    className="n-collapse-transition"
                    style={{ nBezier: "cubic-bezier(.4, 0, .2, 1)" }}
                >
                    <div className="mb-2">
                        <div className="cursor-pointer flex w-max">
                            <input type="file" accept=".jpg, .jpeg, .png" className="hidden" />
                            <img
                                src="/_nuxt/default-avatar.C85jNu88.png"
                                id="icon"
                                className="rounded-[50%]"
                                style={{ width: 40, height: 40 }}
                            />
                        </div>
                    </div>
                </div>
                <div
                    className="leading-[1.4] opacity-90 font-bold custom-transition-2 mb-1.5"
                    style={{ fontSize: "calc(1.25rem)" }}
                >
                    <div className="bg-[transparent]">
                        <div contentEditable="true" translate="no" className="editable-element">
                            <p>Space</p>
                        </div>
                    </div>
                </div>
                <div
                    className="leading-[1.5] opacity-90 custom-transition-2 mb-6 mt-2"
                    style={{ fontSize: "calc(0.875rem)" }}
                >
                    <div>
                        <div className="bg-[transparent]">
                            <div
                                contentEditable="true"
                                translate="no"
                                className="editable-element"
                            >
                                <p>摘录于2024/8/18</p>
                            </div>
                        </div>
                    </div>
                </div>
                <div
                    name="showContent"
                    className="content-body custom-transition-2 opacity-90"
                    style={{ fontSize: "calc(1.25rem)" }}
                >
                    <div className="bg-[transparent]">
                        <div
                            contentEditable="true"
                            translate="no"
                            className="editable-element md-class"
                        >
                            <p>
                                人性和商业的能力,这样才能更好地发现市场痛点,了解用户需求,并用商业来完善产品想法,而不是仅仅作为一名员工去实现公司指定的产品路线。很多产品经理的工作多年间都无法得到进一步提升,其实相当一部分人是卡在了商业洞察这一块。于是很多人在问,是不是多读几本书就可以系统训练商业知识和商业嗅觉,从而增强自己的商业洞察能力?
                            </p>
                        </div>
                    </div>
                </div>
            </div>

            <div>
                <div className="mt-6" />
                <div
                    className="color-[#BBBD9E] font-light opacity-70 leading-[1.4] custom-transition-2 mb-1.5"
                    style={{ fontSize: "calc(1rem)" }}
                >
                    <div className="bg-[transparent]">
                        <div contentEditable="true" translate="no" className="editable-element">
                            <p>/产品之旅:产品经理的方法论与实战进阶</p>
                        </div>
                    </div>
                </div>
                <div
                    className="custom-transition-2 mb-1.5 opacity-70"
                    style={{ fontSize: "calc(0.9rem)" }}
                >
                    <div className="bg-[transparent]">
                        <div contentEditable="true" translate="no" className="editable-element">
                            <p>赖京露</p>
                        </div>
                    </div>
                </div>
                <div>
                    <div className="divider bg-[#F6ECD4] opacity-10 h-px my-5" />
                    <div
                        className="qr-code-section flex items-center justify-between pt-2"
                        style={{ fontFamily: '"Noto Serif SC"' }}
                    >
                        <div>
                            <div
                                className="leading-[1.4] font-bold custom-transition-2"
                                style={{ fontSize: "calc(0.8rem)" }}
                            >
                                <div className="bg-[transparent]">
                                    <div
                                        contentEditable="true"
                                        translate="no"
                                        className="editable-element"
                                    >
                                        <p>微信读书</p>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div className="bg-[#F6E2B5] p-1">
                            <div className="flex" id="qr-code">
                                <div
                                    className="n-qr-code cursor-pointer"
                                    style={{
                                        padding: 0,
                                        backgroundColor: "transparent",
                                        width: 58,
                                        height: 58,
                                        borderRadius: 3
                                    }}
                                >
                                    <canvas
                                        width={116}
                                        height={116}
                                        style={{ width: 58, height: 58 }}
                                    />
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    )
}

================================================
FILE: src/app/(app)/components/dynamic-style-tippy.tsx
================================================
import Tippy from "@tippyjs/react";
import { useEffect, useRef } from "react";

export const DynamicStyleTippyComponent = ({ children, content, tippyOptions = {} }) => {
  const styleId = 'x-cards-dropdown-styles';
  const tippyRef = useRef(null);

  useEffect(() => {
    // Check if the style element already exists
    let styleElement = document.getElementById(styleId);

    if (!styleElement) {
      // If it doesn't exist, create it
      styleElement = document.createElement('style');
      styleElement.id = styleId;
      document.head.appendChild(styleElement);
    }

    // Define your styles
    const styles = `
     .tippy-box[data-theme~='custom'] {
 --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --muted: 240 3.7% 15.9%;
    --muted-foreground: 240 5% 64.9%;
    --popover: 240 10% 3.9%;
    --popover-foreground: 0 0% 98%;
    --card: 240 10% 3.9%;
    --card-foreground: 0 0% 98%;
    --border: 240 3.7% 15.9%;
    --input: 240 3.7% 15.9%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    --secondary: 240 3.7% 15.9%;
    --secondary-foreground: 0 0% 98%;
    --accent: 240 3.7% 15.9%;
    --accent-foreground: 0 0% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 0% 98%;
    --ring: 240 4.9% 83.9%;

  background-color: black;
  color: hsl(var(--foreground));
  border: 1px solid hsl(var(--border));
  border-radius: 6px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  overflow: hidden;
  animation: scaleIn 0.2s ease-out;
}

.tippy-box[data-theme~='custom'] .tippy-content {
  padding: 4px;
  min-width: 152px;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  font-size: 14px;
  color: hsl(var(--foreground));
  cursor: pointer;
  transition: background-color 0.2s, color 0.2s;
  border-radius: 4px;
  user-select: none;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item:hover {
  background-color: hsl(var(--accent));
  color: hsl(var(--accent-foreground));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item.no-hover:hover {
  background-color: transparent;
  color: hsl(var(--foreground));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item:active {
  background-color: hsl(var(--muted));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-icon  {
  width: 16px;
  height: 16px;
  margin-right: 8px;
  color: var(--muted-foreground);
  transition: color 0.2s;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-icon:hover  {
  color: hsl(var(--accent-foreground));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-separator {
  display: flex;
  align-items: center;
  text-align: center;
  // margin: 10px 0;
}


.tippy-box[data-theme~='custom'] .dropdown-menu-separator::before,
.tippy-box[data-theme~='custom'] .dropdown-menu-separator::after {
  content: '';
  flex: 1;
  border-bottom: 1px solid hsl(var(--border));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-separator::before {
  margin-right: 0.5em;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-separator::after {
  margin-left: 0.5em;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-separator-text {
  padding: 0 10px;
  font-size: 0.85em;
  color: hsl(var(--muted-foreground));
  white-space: nowrap;
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger {
  color: hsl(var(--destructive-foreground));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger:hover {
  background-color: hsl(var(--destructive));
}

.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger svg {
  color: var(--destructive-foreground);
}

@keyframes scaleIn {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}
        `;

    // Set the styles
    styleElement.textContent = styles;

    // Cleanup function
    return () => {
    };
  }, []);




  return (
    <Tippy
      ref={tippyRef}
      content={content}
      interactive={true}
      appendTo={document.body}
      placement="top-start"
      theme="custom" // Use our custom theme
      {...tippyOptions}
    >
      {children}
    </Tippy>
  );
};

================================================
FILE: src/app/(app)/components/hero.tsx
================================================

import chromeSvg from '@assets/chrome.svg';
import Image from "next/image";

const Hero = () => {
    return (
        <>
            <div className='flex flex-col gap-y-8 justify-center'>
                <div className="relative mx-auto flex max-w-2xl flex-col items-center">
                    <h2 className="text-center text-3xl font-medium text-gray-900 dark:text-gray-50 sm:text-6xl">
                        X Cards  {' '}
                        <span className="animate-text-gradient inline-flex bg-gradient-to-r from-neutral-900 via-slate-500 to-neutral-500 bg-[200%_auto] bg-clip-text leading-tight text-transparent dark:from-neutral-100 dark:via-slate-400 dark:to-neutral-400">
                            Native Tweet Card service for X
                        </span>
                    </h2>
                    <p className="mt-6 text-center text-lg leading-6 text-gray-600 dark:text-gray-200">
                        Capture and share posts as beautiful images, cards, Markdown, or JSON, making sharing Twitter posts on other platforms more visual and attention-grabbing.
                        {' '}
                    </p>
                    <div className="mt-10 flex gap-4">
                        <a target="_blank" href="https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop"
                            className="p-[3px] relative group">
                            <div className="absolute inset-0 bg-gradient-to-r from-[#8F01FF] to-[#FDB83B] rounded-lg" />
                            <div className="px-8 py-2  group-hover:scale-105  flex  gap-x-3   rounded-[6px]  relative group transition duration-200 text-white ">
                                <Image src={chromeSvg} alt="Chrome Extension" className="w-6 h-6" />
                                Download Extension
                            </div>

                        </a>

                    </div>
                    <a
                        className='mt-10'
                        href="https://www.producthunt.com/posts/x-cards?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-x-cards"
                        target="_blank"
                    >
                        <img
                            src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=481821&theme=light"
                            alt="X Cards - Simple, Beautiful Tweets with Card Integration on X.com | Product Hunt"
                            style={{ width: 250, height: 54 }}
                            width={250}
                            height={54}
                        />
                    </a>
                </div>
                {/* <VideoSection /> */}

            </div>

        </>

    )
}

export default Hero




================================================
FILE: src/app/(app)/components/save-as-template-button.tsx
================================================
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@components/ui/alert-dialog";
import { Button } from "@components/ui/button";
import { Input } from "@components/ui/input";
import { useCardStore } from "@src/hooks/useCardStore";
import { useTemplatesStore } from "@src/hooks/useTemplatesStore";
import React from "react";

export const SaveAsTemplateButton = () => {
    const cardStyles = useCardStore((state) => state.cardStyles);
    const addTemplate = useTemplatesStore((state) => state.addTemplate);
    const backgroundStyles = useCardStore((state) => state.backgroundStyles);
    const [name, setName] = React.useState('');

    return (<AlertDialog>
        <AlertDialogTrigger asChild>
            <Button size="sm" >Save as Template</Button>
        </AlertDialogTrigger>
        <AlertDialogContent>
            <AlertDialogHeader>
                <AlertDialogTitle>Input Template Name</AlertDialogTitle>
                {/* <AlertDialogDescription>
    This action cannot be undone. This will permanently delete your
    account and remove your data from our servers.
</AlertDialogDescription> */}
                <Input value={name} onChange={(e) => {
                    setName(e.target.value)
                }}></Input>
            </AlertDialogHeader>
            <AlertDialogFooter>
                <AlertDialogCancel>Cancel</AlertDialogCancel>
                <AlertDialogAction onClick={() => addTemplate({
                    name: name,
                    colorIndex: useCardStore.getState().colorIndex,
                    backgroundStyles: backgroundStyles,
                    tabConfig: useCardStore.getState().tabConfig,
                    cardStyles: cardStyles
                })}>Confirm</AlertDialogAction>
            </AlertDialogFooter>
        </AlertDialogContent>
    </AlertDialog>)
}

================================================
FILE: src/app/(app)/components/sections/FeaturesGridSection.tsx
================================================
// import yellowWaveDown from "@/public/images/banner-wave.png";
// import Image from "next/image";
import { BookCopy, LayoutGrid, ListCollapse, Palette } from "lucide-react";
import { useId } from "react";

 function FeaturesGridSection() {
	return (
		<>
			<div className="relative py-32">
				<div className="flex flex-col items-center justify-center ">
					<h1 className="m-[33.2px_0px_8px] h-[46px] max-w-screen-xl text-center font-semibold text-[40px]  leading-[46px]">
						Features
					</h1>
					<div className="max-w-screen-xl p-[0px_25px] text-center  text-xl tracking-[-0.2px]">
						<p>
							We provide a variety of features to help you create beautiful
							images, cards, Markdown, or JSON from tweets, making sharing
							Twitter posts on other platforms more visual and
							attention-grabbing.
						</p>
					</div>
				</div>

				<div className="m-auto mx-auto mt-[50px] flex max-w-7xl flex-row flex-wrap justify-center overflow-x-hidden ">
					{grid.map((feature) => (
						<div
							key={feature.title}
							className="relative m-4 w-[260px] min-w-[260px] overflow-hidden rounded-3xl bg-gradient-to-b from-neutral-100 to-white p-6 dark:from-neutral-900 dark:to-neutral-950"
						>
							<Grid size={20} />
							<div className="mb-2 p-0">{feature?.icon}</div>
							<p className="relative z-20 font-bold text-base text-neutral-800 dark:text-white">
								{feature.title}
							</p>
							<p className="relative z-20 mt-4 font-normal text-base text-neutral-600 dark:text-neutral-400">
								{feature.description}
							</p>
						</div>
					))}
				</div>
			</div>
			{/* <Image src={yellowWaveDown} alt="" className=" -mt-32 absolute" /> */}
		</>
	);
}

const grid = [
	{
		icon: (
			<BookCopy className="w-8 h-8" />
		),
		title: "Fast capture",
		// description:
		// 	"快速将任何帖子转换为卡片,只需要亲亲点击"

		description:
			"Capture and share posts as beautiful images, cards, Markdown, or JSON, making sharing Twitter posts on other platforms more visual and attention-grabbing."
	},
	{
		icon: (
			<Palette className="w-8 h-8" />
		),
		title: "Custom Styles",
		// description:
		// 	"多种卡片风格,自定义颜色,边距等等"

		description:
			"Multiple card styles, custom colors, margins, and more."
	},
	{
		icon: (
			<ListCollapse className="w-8 h-8" />
		),
		title: "Combining multiple posts",
		// description:
		// 	"拼接多个tweet内容,生成长卡片",

		description:
			"Combine multiple tweet contents to generate long cards."
	},
	{
		icon: (
			<LayoutGrid className="w-8 h-8" />
		),
		title: "Dynamic management",
		// description:
		// 	"动态管理你的帖子,随时删除,添加",

		description:
			"Manage your posts dynamically, delete or add them at any time."

	},

];

const Grid = ({
	pattern,
	size,
}: {
	pattern?: number[][];
	size?: number;
}) => {
	const p = pattern ?? [
		[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
		[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
		[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
		[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
		[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
	];
	return (
		<div className="-ml-20 -mt-2 pointer-events-none absolute top-0 left-1/2 h-full [mask-image:linear-gradient(white,transparent)]">
			<div className="absolute inset-0 bg-gradient-to-r from-zinc-100/30 to-zinc-300/30 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 dark:to-zinc-900/30">
				<GridPattern
					width={size ?? 20}
					height={size ?? 20}
					x="-12"
					y="4"
					squares={p}
					className="absolute inset-0 h-full w-full fill-black/10 stroke-black/10 mix-blend-overlay dark:fill-white/10 dark:stroke-white/10"
				/>
			</div>
		</div>
	);
};

function GridPattern({ width, height, x, y, squares, ...props }: any) {
	const patternId = useId();

	return (
		<svg aria-hidden="true" {...props}>
			<defs>
				<pattern
					id={patternId}
					width={width}
					height={height}
					patternUnits="userSpaceOnUse"
					x={x}
					y={y}
				>
					<path d={`M.5 ${height}V.5H${width}`} fill="none" />
				</pattern>
			</defs>
			<rect
				width="100%"
				height="100%"
				strokeWidth={0}
				fill={`url(#${patternId})`}
			/>
			{squares && (
				<svg x={x} y={y} className="overflow-visible">
					<title>noise</title>
					{squares.map(([x, y]: any) => (
						<rect
							strokeWidth="0"
							key={`${x}-${y}`}
							width={width + 1}
							height={height + 1}
							x={x * width}
							y={y * height}
						/>
					))}
				</svg>
			)}
		</svg>
	);
}


export default FeaturesGridSection;

================================================
FILE: src/app/(app)/components/sections/features2.tsx
================================================
// import { Icon } from "@/components/ui/icon";
import { Card, CardContent, CardHeader, CardTitle } from "@src/components/ui/card";
import { Icon } from "@src/components/ui/icon";
import { icons } from "lucide-react";
interface FeaturesProps {
    icon: string;
    title: string;
    description: string;
}

const featureList: FeaturesProps[] = [
    {
        icon: "Globe",
        title: "Seamless Integration",
        description: "Integrate directly on x.com without any hassle. Our product works smoothly within the platform you're already using.",
    },
    {
        icon: "MousePointerClick",
        title: "One-Click Generation",
        description: "Simple and easy to use. Generate and copy cards with just a single click, streamlining your workflow.",
    },
    {
        icon: "Palette",
        title: "Customizable Design",
        description: "Personalize your cards by modifying styles, background colors, and more to match your brand or preferences.",
    },
    {
        icon: "Eye",
        title: "Real-Time Preview",
        description: "See your changes instantly and manage your tweets dynamically, ensuring your content looks perfect before posting.",
    },
    {
        icon: "FileText",
        title: "Long-Form Support",
        description: "Create and manage longer posts effortlessly, perfect for in-depth content or thread creation.",
    },
    {
        icon: "Lock",
        title: "Open Source & Secure",
        description: "Our product is open source, ensuring transparency and allowing for community contributions while maintaining high security standards.",
    },
];

export const FeaturesSection = () => {
    return (
        <section id="features" className="container py-24 sm:py-32">
            <h2 className="text-lg text-primary text-center mb-2 tracking-wider">
                Features
            </h2>

            <h2 className="text-3xl md:text-4xl text-center font-bold mb-4">
                Why Choose Our Twitter Card Generator
            </h2>

            <h3 className="md:w-1/2 mx-auto text-xl text-center text-muted-foreground mb-8">
                Elevate your Twitter presence with our seamless, customizable, and secure card generator.
                Create eye-catching content directly on x.com with just a few clicks.
            </h3>

            <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
                {featureList.map(({ icon, title, description }) => (
                    <div key={title}>
                        <Card className="h-full bg-background border-0 shadow-none">
                            <CardHeader className="flex justify-center items-center">
                                <div className="bg-primary/20 p-2 rounded-full ring-8 ring-primary/10 mb-4">
                                    <Icon
                                        name={icon as keyof typeof icons}
                                        size={24}
                                        color="hsl(var(--primary))"
                                        className="text-primary"
                                    />
                                </div>

                                <CardTitle>{title}</CardTitle>
                            </CardHeader>

                            <CardContent className="text-muted-foreground text-center">
                                {description}
                            </CardContent>
                        </Card>
                    </div>
                ))}
            </div>
        </section>
    );
};

================================================
FILE: src/app/(app)/components/sections/footer.tsx
================================================
import Link from "next/link";
import Logo from "@assets/icon.png";

export const FooterSection = () => {
    return (
        <footer id="footer" className="container py-24 sm:py-32">
            <div className="p-10 bg-card border border-secondary rounded-2xl">
                <div className="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-x-12 gap-y-8">
                    <div className="col-span-full xl:col-span-2">
                        <Link href="#" className="flex font-bold items-center">
                            {/* <ChevronsDownIcon className="w-9 h-9 mr-2 bg-gradient-to-tr from-primary via-primary/70 to-primary rounded-lg border border-secondary" /> */}
                            <img className="w-9 h-9 mr-2 " src={Logo.src} alt="xcards" ></img>
                            <h3 className="text-2xl">X Cards</h3>
                        </Link>
                    </div>

                    <div className="flex flex-col gap-2">
                        <h3 className="font-bold text-lg">Contact</h3>
                        <div>
                            <Link href="https://github.com/hzeyuan/x-cards" className="opacity-60 hover:opacity-100">
                                Github
                            </Link>
                        </div>

                        <div>
                            <Link href="https://x.com/FeigelC35583" className="opacity-60 hover:opacity-100">
                                Twitter
                            </Link>
                        </div>
                    </div>



                    <div className="flex flex-col gap-2">
                        <h3 className="font-bold text-lg">Socials</h3>

                        <div>
                            <Link href="https://discord.gg/Prjas7Qh" className="opacity-60 hover:opacity-100">
                                Discord
                            </Link>
                        </div>
                    </div>
                </div>

                {/* <Separator className="my-6" /> */}
                <section className="">
                    <h3 className="">
                        &copy; 2024 Designed and developed by
                        <Link
                            target="_blank"
                            href="https://x.com/FeigelC35583"
                            className="text-primary transition-all border-primary hover:border-b-2 ml-1"
                        >
                            Zane Ryan
                        </Link>
                    </h3>
                </section>
            </div>
        </footer>
    );
};

================================================
FILE: src/app/(app)/components/sections/video.tsx
================================================
"use client"
import ReactPlayer from 'react-player'


const VideoSection = () => {
    return (<div className='mx-auto'>
        <ReactPlayer url="https://youtu.be/okCIZrFrTCE" />
    </div>)
}

export default VideoSection

================================================
FILE: src/app/(app)/components/template-list.tsx
================================================
import { Button } from "@components/ui/button";
import { useCardStore } from "@src/hooks/useCardStore";
import { useTemplatesStore } from "@src/hooks/useTemplatesStore";
import { X } from "lucide-react";

export const TemplateList = () => {
    const templates = useTemplatesStore(state => state.templates);
    const delTemplate = useTemplatesStore(state => state.delTemplate);
    const setFontStyles = useCardStore(state => state.setFontStyles);
    const setColorIndex = useCardStore(state => state.setColorIndex);
    const updateBackgroundStyles = useCardStore(state => state.updateBackgroundStyles);
    const updateCardStyles = useCardStore(state => state.updateCardStyles);
    return (
        <div className="flex gap-4 py-4">
            {templates.map((t, index) => {
                return (
                    <Button size="sm" key={index} variant="secondary"
                        onClick={() => {
                            setColorIndex(t.colorIndex)
                            updateBackgroundStyles(t.backgroundStyles)
                            updateCardStyles(t.cardStyles)
                        }
                        }
                        className="hover:shadow-md">
                        <span>
                            <div>
                                <div className="action-btn flex-shrink-0 flex items-center justify-center cursor-pointer rounded-[4px] text-accent-foreground   ">
                                    {t.name}
                                </div>
                            </div>
                        </span>
                        <X className=" cursor-pointer hover:shadw-md ml-1 w-3 h-3" onClick={(e) => {
                            e.preventDefault()
                            e.stopPropagation()
                            delTemplate(index)

                        }} />
                    </Button>

                )
            })}

        </div>
    )
}

================================================
FILE: src/app/(app)/components/x-form.tsx
================================================
import { Button } from "@components/ui/button";
import { Input } from "@components/ui/input";
import { useCardStore, type XConfig } from "@src/hooks/useCardStore";
import { useState } from "react";

interface XFormProps {
}

export const XForm = (props: XFormProps) => {

    const [url, setUrl] = useState<string>('');
    const setXconfig = useCardStore(state => state.setXConfig);
    const handleGetX = async (url: string) => {
        const res = await fetch(`/api/x?url=${url}`, {
            method: 'GET',
        })
        const data = await res.json();
        console.log('res', res);
        setXconfig({
            ...data.data,
            images: [data.data.imageUrl]
        } as XConfig)
    }



    return (
        <div className="flex w-full  pt-8 items-center space-x-2">
            <Input onChange={(e) => {
                setUrl(e.target.value);
            }} value={url} type="url" placeholder="input X url" />
            <Button type="submit" onClick={() => {
                handleGetX(url);
            }}>Get Tweet →</Button>
        </div>
    )
}

================================================
FILE: src/app/(app)/layout.tsx
================================================
// "use client"
import Image from 'next/image';
import Logo from '@assets/icon.png';

const AppLayout = ({ children }) => {

    return (
        <div>
            <header
                style={{
                    zIndex: 9999
                }}
                className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
                <div className="bg-gradient-to-br from-green-300 via-blue-500 to-purple-600 fixed w-full px-4 py-2 text-white !bg-gradient-to-r">
                    <p className="flex items-center justify-center text-sm font-medium !text-white">
                        {" "}
                        X cards also support the chrome extension version.{" "}
                        <a
                            className="underline flex items-center"
                            href="https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop"
                            target="_blank"
                        >
                            <span className="ml-2 text-xs">Get Chrome Extension →</span>
                        </a>
                    </p>
                </div>
                <div className=" relative  top-[57px] container flex h-14 max-w-screen-2xl items-center">
                    <div className="mr-4 hidden md:flex">
                        <a className="mr-6 flex items-center space-x-2" href="/">
                            <Image className='w-6 h-6' alt="logo" src={Logo} width={24} ></Image>
                            <span className="hidden  sm:inline-block  whitespace-nowrap">X Cards</span>
                        </a>
                    </div>
                    <div className="w-full flex justify-center">
                        <nav className="flex items-center gap-4 text-sm lg:gap-6">
                        </nav>
                    </div>
                    <button
                        className="inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:text-accent-foreground h-9 py-2 mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
                        type="button"
                        aria-haspopup="dialog"
                        aria-expanded="false"
                        aria-controls="radix-:R4mja:"
                        data-state="closed"
                    >
                        <svg
                            strokeWidth="1.5"
                            viewBox="0 0 24 24"
                            fill="none"
                            xmlns="http://www.w3.org/2000/svg"
                            className="h-5 w-5"
                        >
                            <path
                                d="M3 5H11"
                                stroke="currentColor"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                            <path
                                d="M3 12H16"
                                stroke="currentColor"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                            <path
                                d="M3 19H21"
                                stroke="currentColor"
                                strokeWidth="1.5"
                                strokeLinecap="round"
                                strokeLinejoin="round"
                            />
                        </svg>
                        <
Download .txt
gitextract_nbnj719d/

├── .github/
│   └── workflows/
│       └── submit.yml
├── .gitignore
├── .prettierrc.mjs
├── LICENSE
├── README.md
├── README_ZH.md
├── components/
│   └── ui/
│       ├── EditableButton.tsx
│       ├── accordion.tsx
│       ├── alert-dialog.tsx
│       ├── badge.tsx
│       ├── button.tsx
│       ├── color-picker.tsx
│       ├── dropdown-menu.tsx
│       ├── input.tsx
│       ├── popover.tsx
│       ├── radio-group.tsx
│       ├── select-position.tsx
│       ├── select.tsx
│       ├── slider.tsx
│       ├── sonner.tsx
│       ├── switch.tsx
│       ├── tabs.tsx
│       ├── textarea.tsx
│       ├── toast.tsx
│       ├── toaster.tsx
│       └── use-toast.ts
├── components.json
├── lib/
│   └── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── popup.tsx
├── postcss.config.js
├── src/
│   ├── app/
│   │   ├── (app)/
│   │   │   ├── components/
│   │   │   │   ├── FrequentlyAskedQuestions.tsx
│   │   │   │   ├── GoogleFontSelector.tsx
│   │   │   │   ├── ImageLayout.tsx
│   │   │   │   ├── LazyLoadAnimatedSection.tsx
│   │   │   │   ├── ResultIcon.tsx
│   │   │   │   ├── card-generator/
│   │   │   │   │   ├── color.tsx
│   │   │   │   │   ├── controller/
│   │   │   │   │   │   ├── background-controller.tsx
│   │   │   │   │   │   ├── card-controller.tsx
│   │   │   │   │   │   ├── font-controller.tsx
│   │   │   │   │   │   ├── iframe-controller.tsx
│   │   │   │   │   │   └── input-controller.tsx
│   │   │   │   │   ├── display.tsx
│   │   │   │   │   ├── export-tab.tsx
│   │   │   │   │   ├── index.tsx
│   │   │   │   │   ├── twitter-card.tsx
│   │   │   │   │   └── wechat-card.tsx
│   │   │   │   ├── dynamic-style-tippy.tsx
│   │   │   │   ├── hero.tsx
│   │   │   │   ├── save-as-template-button.tsx
│   │   │   │   ├── sections/
│   │   │   │   │   ├── FeaturesGridSection.tsx
│   │   │   │   │   ├── features2.tsx
│   │   │   │   │   ├── footer.tsx
│   │   │   │   │   └── video.tsx
│   │   │   │   ├── template-list.tsx
│   │   │   │   └── x-form.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── page.tsx
│   │   │   └── request.tsx
│   │   ├── (extension)/
│   │   │   ├── independent/
│   │   │   │   ├── components/
│   │   │   │   │   └── index.tsx
│   │   │   │   └── page.tsx
│   │   │   └── welcome/
│   │   │       └── page.tsx
│   │   ├── api/
│   │   │   ├── license/
│   │   │   │   └── route.ts
│   │   │   └── x/
│   │   │       └── route.ts
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── utils/
│   │       ├── IFrameMessageSystem.ts
│   │       ├── element.ts
│   │       ├── export.ts
│   │       ├── format.ts
│   │       ├── image.ts
│   │       └── index.ts
│   ├── background/
│   │   ├── index.ts
│   │   └── messages/
│   │       ├── code.ts
│   │       └── tweet.ts
│   ├── components/
│   │   ├── extension/
│   │   │   ├── card-button.tsx
│   │   │   ├── input-code.tsx
│   │   │   ├── label-with-icon.tsx
│   │   │   ├── layout-options.tsx
│   │   │   ├── preset-color-list.tsx
│   │   │   ├── tabs.tsx
│   │   │   ├── tweet-manager.tsx
│   │   │   ├── use-tweet-collection.ts
│   │   │   └── x-cards-toast/
│   │   │       ├── font-control.tsx
│   │   │       ├── image-preview.tsx
│   │   │       ├── index.module.css
│   │   │       ├── index.tsx
│   │   │       ├── padding-control.tsx
│   │   │       ├── scale-control.tsx
│   │   │       └── tweet-control.tsx
│   │   ├── sortableList.tsx
│   │   └── ui/
│   │       ├── BentoGrid.tsx
│   │       ├── DotPattern.tsx
│   │       ├── acetenity-tabs.tsx
│   │       ├── animated-list.tsx
│   │       ├── animatedBeam.tsx
│   │       ├── api-key-panel.tsx
│   │       ├── bold-copy.tsx
│   │       ├── burnIn.tsx
│   │       ├── card.tsx
│   │       ├── chart.tsx
│   │       ├── fade-text.tsx
│   │       ├── grid-pattern.tsx
│   │       ├── grid.tsx
│   │       ├── icon.tsx
│   │       ├── loading-spinner.tsx
│   │       ├── marquee.tsx
│   │       ├── scroll-based-velocity.tsx
│   │       ├── shimmer-button.tsx
│   │       ├── text-generate-effect.tsx
│   │       └── underline-hover-text.tsx
│   ├── config/
│   │   └── site.ts
│   ├── contents/
│   │   ├── plasmo-overlay.css
│   │   ├── plasmo-overlay.tsx
│   │   ├── x-home.tsx
│   │   └── x.css
│   ├── hooks/
│   │   ├── useCardStore.tsx
│   │   └── useTemplatesStore.tsx
│   ├── lib/
│   │   └── BlurGradientBg.module.js
│   └── sandbox.tsx
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json
Download .txt
SYMBOL INDEX (164 symbols across 50 files)

FILE: components/ui/badge.tsx
  type BadgeProps (line 26) | interface BadgeProps
  function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {

FILE: components/ui/button.tsx
  type ButtonProps (line 36) | interface ButtonProps

FILE: components/ui/color-picker.tsx
  function useForwardedRef (line 14) | function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
  type ColorPickerProps (line 30) | interface ColorPickerProps {

FILE: components/ui/input.tsx
  type InputProps (line 5) | interface InputProps

FILE: components/ui/sonner.tsx
  type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: components/ui/textarea.tsx
  type TextareaProps (line 5) | interface TextareaProps

FILE: components/ui/toast.tsx
  type ToastProps (line 115) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
  type ToastActionElement (line 117) | type ToastActionElement = React.ReactElement<typeof ToastAction>

FILE: components/ui/toaster.tsx
  function Toaster (line 13) | function Toaster() {

FILE: components/ui/use-toast.ts
  constant TOAST_LIMIT (line 11) | const TOAST_LIMIT = 1
  constant TOAST_REMOVE_DELAY (line 12) | const TOAST_REMOVE_DELAY = 1000000
  type ToasterToast (line 14) | type ToasterToast = ToastProps & {
  function genId (line 30) | function genId() {
  type ActionType (line 35) | type ActionType = typeof actionTypes
  type Action (line 37) | type Action =
  type State (line 55) | interface State {
  function dispatch (line 136) | function dispatch(action: Action) {
  type Toast (line 143) | type Toast = Omit<ToasterToast, "id">
  function toast (line 145) | function toast({ ...props }: Toast) {
  function useToast (line 174) | function useToast() {

FILE: lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: src/app/(app)/components/GoogleFontSelector.tsx
  function GoogleFontSelector (line 36) | function GoogleFontSelector({ onFontChange }) {

FILE: src/app/(app)/components/ResultIcon.tsx
  type PropTypes (line 7) | type PropTypes = {

FILE: src/app/(app)/components/card-generator/color.tsx
  type ColorInputPropTypes (line 157) | type ColorInputPropTypes = {

FILE: src/app/(app)/components/card-generator/controller/font-controller.tsx
  type FontControllerProps (line 6) | interface FontControllerProps {

FILE: src/app/(app)/components/card-generator/twitter-card.tsx
  type TwitterCardProps (line 9) | interface TwitterCardProps {

FILE: src/app/(app)/components/card-generator/wechat-card.tsx
  type WechatCardProps (line 3) | interface WechatCardProps {

FILE: src/app/(app)/components/sections/FeaturesGridSection.tsx
  function FeaturesGridSection (line 6) | function FeaturesGridSection() {
  function GridPattern (line 126) | function GridPattern({ width, height, x, y, squares, ...props }: any) {

FILE: src/app/(app)/components/sections/features2.tsx
  type FeaturesProps (line 5) | interface FeaturesProps {

FILE: src/app/(app)/components/x-form.tsx
  type XFormProps (line 6) | interface XFormProps {

FILE: src/app/(extension)/independent/components/index.tsx
  function generateStaticParams (line 11) | async function generateStaticParams() {

FILE: src/app/(extension)/independent/page.tsx
  function generateStaticParams (line 3) | async function generateStaticParams() {

FILE: src/app/layout.tsx
  function RootLayout (line 22) | function RootLayout({

FILE: src/app/utils/IFrameMessageSystem.ts
  type MessageCallback (line 1) | type MessageCallback = (value: any) => void;
  type MessageEvent (line 3) | interface MessageEvent {
  class IFrameMessageSystem (line 10) | class IFrameMessageSystem {
    method constructor (line 13) | constructor() {
    method subscribe (line 19) | subscribe(action: string, callback: MessageCallback): () => void {
    method unsubscribe (line 27) | unsubscribe(action: string, callback: MessageCallback): void {
    method publish (line 33) | publish(iframe: HTMLIFrameElement, action: string, data: any, timeout:...
    method handleMessage (line 56) | private handleMessage(event: MessageEvent): void {

FILE: src/app/utils/element.ts
  function traverseAndCheck (line 1) | function traverseAndCheck(element, condition) {
  function getAdjacentCellDiv (line 19) | function getAdjacentCellDiv(element, direction) {
  function checkAppliedStyle (line 30) | function checkAppliedStyle(element, properties) {

FILE: src/app/utils/format.ts
  function formatTimestamp (line 1) | function formatTimestamp(timestamp: number): string {

FILE: src/app/utils/index.ts
  function checkTheVerticalLine (line 10) | function checkTheVerticalLine(element) {
  function isElementVisible (line 20) | function isElementVisible(element) {
  type CopyImage (line 91) | type CopyImage = (tweetInfo: XConfig | XConfig[], cardConfig: any) => Pr...
  function extractTweetInfo (line 159) | function extractTweetInfo(postElement) {

FILE: src/components/extension/label-with-icon.tsx
  type LabelWithIconProps (line 4) | interface LabelWithIconProps {

FILE: src/components/extension/use-tweet-collection.ts
  type TweetControlState (line 5) | interface TweetControlState {
  type CardConfig (line 13) | interface CardConfig {
  type TweetCollection (line 24) | interface TweetCollection {

FILE: src/components/extension/x-cards-toast/index.tsx
  type PreviewToastProps (line 24) | interface PreviewToastProps {

FILE: src/components/extension/x-cards-toast/scale-control.tsx
  type ScaleControlProps (line 6) | interface ScaleControlProps {

FILE: src/components/extension/x-cards-toast/tweet-control.tsx
  type TweetControlProps (line 8) | interface TweetControlProps {
  type ControlOptionProps (line 66) | interface ControlOptionProps {

FILE: src/components/sortableList.tsx
  type Item (line 17) | type Item<T> = T & {
  type SortableListItemProps (line 21) | interface SortableListItemProps<T> {
  function SortableListItem (line 33) | function SortableListItem<T>({
  type SortableListProps (line 130) | interface SortableListProps<T> {
  function SortableList (line 142) | function SortableList<T>({

FILE: src/components/ui/DotPattern.tsx
  type DotPatternProps (line 5) | interface DotPatternProps {
  function DotPattern (line 16) | function DotPattern({

FILE: src/components/ui/acetenity-tabs.tsx
  type Tab (line 7) | type Tab = {

FILE: src/components/ui/animated-list.tsx
  type AnimatedListProps (line 6) | interface AnimatedListProps {
  function AnimatedListItem (line 46) | function AnimatedListItem({ children }: { children: React.ReactNode }) {

FILE: src/components/ui/animatedBeam.tsx
  type AnimatedBeamProps (line 8) | interface AnimatedBeamProps {

FILE: src/components/ui/bold-copy.tsx
  function BoldCopy (line 9) | function BoldCopy({

FILE: src/components/ui/burnIn.tsx
  type BlurIntProps (line 7) | interface BlurIntProps {

FILE: src/components/ui/chart.tsx
  constant THEMES (line 14) | const THEMES = { light: "", dark: ".dark" } as const
  type ChartConfig (line 16) | type ChartConfig = {
  type ChartContextProps (line 26) | type ChartContextProps = {
  function useChart (line 32) | function useChart() {
  function getPayloadConfigFromPayload (line 325) | function getPayloadConfigFromPayload(

FILE: src/components/ui/fade-text.tsx
  type FadeTextProps (line 6) | type FadeTextProps = {
  function FadeText (line 13) | function FadeText({

FILE: src/components/ui/grid-pattern.tsx
  type GridPatternProps (line 5) | interface GridPatternProps {
  function GridPattern (line 16) | function GridPattern({

FILE: src/components/ui/grid.tsx
  type GridProps (line 1) | interface GridProps {
  function Placeholder (line 18) | function Placeholder({ size = 20 }: Pick<GridProps, "size">) {
  function Grid (line 35) | function Grid({ color = "#cacaca", size = 20, children }: GridProps) {

FILE: src/components/ui/marquee.tsx
  type MarqueeProps (line 3) | interface MarqueeProps {
  function Marquee (line 13) | function Marquee({

FILE: src/components/ui/scroll-based-velocity.tsx
  type VelocityScrollProps (line 16) | interface VelocityScrollProps {
  type ParallaxProps (line 22) | interface ParallaxProps {
  function VelocityScroll (line 33) | function VelocityScroll({

FILE: src/components/ui/shimmer-button.tsx
  type ShimmerButtonProps (line 5) | interface ShimmerButtonProps

FILE: src/components/ui/underline-hover-text.tsx
  type UnderlineHoverTextProps (line 6) | interface UnderlineHoverTextProps {

FILE: src/hooks/useCardStore.tsx
  type Frame (line 5) | type Frame = 'none' | 'macos' | 'windows'
  type SocialPlatform (line 7) | type SocialPlatform = 'instagram' | 'facebook' | 'linkedin' | 'whatsapp'...
  type PostType (line 9) | type PostType = 'landscape' | 'square' | 'portrait' | 'post' | 'story' |...
  type AspectRatio (line 11) | type AspectRatio = '16:9' | '3:2' | '4:3' | '5:4' | '1:1' | '4:5' | '3:4...
  type ImageDimensions (line 14) | interface ImageDimensions {
  type ImageLayoutProps (line 21) | type ImageLayoutProps = {
  type LayoutOption (line 28) | interface LayoutOption {
  type XConfig (line 84) | interface XConfig {
  type CardStore (line 108) | interface CardStore {

FILE: src/hooks/useTemplatesStore.tsx
  type Template (line 8) | type Template = Pick<CardStore, 'colorIndex' | 'backgroundStyles' | 'car...
  type TemplatesStore (line 25) | interface TemplatesStore {

FILE: src/lib/BlurGradientBg.module.js
  function t (line 1) | function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,r=Array(...
  function e (line 1) | function e(t,e,i){return e=a(e),u(t,o()?Reflect.construct(e,i||[],a(t).c...
  function i (line 1) | function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a ...
  function r (line 1) | function r(t,e){for(var i=0;i<e.length;i++){var r=e[i];r.enumerable=r.en...
  function n (line 1) | function n(t,e,i){return e&&r(t.prototype,e),i&&r(t,i),Object.defineProp...
  function s (line 1) | function s(){return s="undefined"!=typeof Reflect&&Reflect.get?Reflect.g...
  function a (line 1) | function a(t){return a=Object.setPrototypeOf?Object.getPrototypeOf.bind(...
  function h (line 1) | function h(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("S...
  function o (line 1) | function o(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct...
  function u (line 1) | function u(t,e){if(e&&("object"==typeof e||"function"==typeof e))return ...
  function l (line 1) | function l(t,e){return l=Object.setPrototypeOf?Object.setPrototypeOf.bin...
  function c (line 1) | function c(e){return function(e){if(Array.isArray(e))return t(e)}(e)||fu...
  function f (line 1) | function f(t){var e=function(t,e){if("object"!=typeof t||!t)return t;var...
  function d (line 1) | function d(t){return d="function"==typeof Symbol&&"symbol"==typeof Symbo...
  function g (line 1) | function g(e,i){if(e){if("string"==typeof e)return t(e,i);var r={}.toStr...
  function v (line 1) | function v(t){var e="function"==typeof Map?new Map:void 0;return v=funct...
  function p (line 1) | function p(t){var e=t[0],i=t[1],r=t[2];return Math.sqrt(e*e+i*i+r*r)}
  function m (line 1) | function m(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t}
  function y (line 1) | function y(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t}
  function _ (line 1) | function _(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t}
  function b (line 1) | function b(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t[2]=e[2]*i,t}
  function x (line 1) | function x(t){var e=t[0],i=t[1],r=t[2];return e*e+i*i+r*r}
  function E (line 1) | function E(t,e){var i=e[0],r=e[1],n=e[2],s=i*i+r*r+n*n;return s>0&&(s=1/...
  function w (line 1) | function w(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}
  function M (line 1) | function M(t,e,i){var r=e[0],n=e[1],s=e[2],a=i[0],h=i[1],o=i[2];return t...
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function U (line 1) | function U(t,e,i,r){r=r.length?function(t){var e=t.length,i=t[0].length;...
  function L (line 1) | function L(t){I>100||I++}
  function G (line 1) | function G(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=i[0],o=i[1],u=i[2],l...
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function Z (line 1) | function Z(t){var e=t[0],i=t[1],r=t[2],n=t[3],s=t[4],a=t[5],h=t[6],o=t[7...
  function Q (line 1) | function Q(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=e[4],o=e[5],u=e[6],l...
  function K (line 1) | function K(t,e){var i=e[0],r=e[1],n=e[2],s=e[4],a=e[5],h=e[6],o=e[8],u=e...
  function tt (line 1) | function tt(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t...
  function et (line 1) | function et(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t...
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function r (line 1) | function r(t){var n,s=arguments.length>1&&void 0!==arguments[1]?argument...
  function lt (line 1) | function lt(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=e[4],o=e[5],u=e[6],...
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function r (line 1) | function r(t){var n,s=arguments.length>1&&void 0!==arguments[1]?argument...
  function vt (line 1) | function vt(t){return!(t&t-1)}
  function bt (line 1) | function bt(t){4===t.length&&(t=t[0]+t[1]+t[1]+t[2]+t[2]+t[3]+t[3]);var ...
  function xt (line 1) | function xt(t){return[((t=parseInt(t))>>16&255)/255,(t>>8&255)/255,(255&...
  function Et (line 1) | function Et(t){return void 0===t?[0,0,0]:3===arguments.length?arguments:...
  function r (line 1) | function r(t){var n;return i(this,r),Array.isArray(t)?u(n,n=e(this,r,c(t...
  function kt (line 1) | function kt(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t}
  function At (line 1) | function At(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t}
  function Tt (line 1) | function Tt(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t}
  function Rt (line 1) | function Rt(t){var e=t[0],i=t[1];return Math.sqrt(e*e+i*i)}
  function Ft (line 1) | function Ft(t,e){return t[0]*e[1]-t[1]*e[0]}
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
  function r (line 1) | function r(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[...
  function l (line 1) | function l(u,l,d){var m=[],y=v(g((l=1==l?{entropy:!0}:l||{}).entropy?[u,...
  function c (line 1) | function c(t){var e,i=t.length,r=this,s=0,a=r.i=r.j=0,h=r.S=[];for(i||(t...
  function f (line 1) | function f(t,e){return e.i=t.i,e.j=t.j,e.S=t.S.slice(),e}
  function g (line 1) | function g(t,e){var i,r=[],n=d(t);if(e&&"object"==n)for(i in t)try{r.pus...
  function v (line 1) | function v(t,e){for(var i,r=t+"",n=0;n<r.length;)e[u&n]=u&(i^=19*e[u&n])...
  function p (line 1) | function p(t){return String.fromCharCode.apply(0,t)}
  function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...

FILE: tailwind.config.ts
  function addVariablesForColors (line 158) | function addVariablesForColors({ addBase, theme }: any) {
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (515K chars).
[
  {
    "path": ".github/workflows/submit.yml",
    "chars": 932,
    "preview": "name: \"Submit to Web Store\"\non:\n  workflow_dispatch:\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses:"
  },
  {
    "path": ".gitignore",
    "chars": 349,
    "preview": "\n# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.p"
  },
  {
    "path": ".prettierrc.mjs",
    "chars": 558,
    "preview": "/**\n * @type {import('prettier').Options}\n */\nexport default {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  semi"
  },
  {
    "path": "LICENSE",
    "chars": 16724,
    "preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
  },
  {
    "path": "README.md",
    "chars": 5724,
    "preview": "<a name=\"readme-top\"></a>\n\n<div align=\"center\">\n<img src=\"assets/icon.png\" width=\"32\" >\n<h1>X Cards</h1>\n\n[English](READ"
  },
  {
    "path": "README_ZH.md",
    "chars": 3856,
    "preview": "<a name=\"readme-top\"></a>\n\n<div align=\"center\">\n<img src=\"assets/icon.png\" width=\"32\" >\n<h1>X Cards</h1>\n\n[English](READ"
  },
  {
    "path": "components/ui/EditableButton.tsx",
    "chars": 1387,
    "preview": "import React, { useState, useRef } from 'react';\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from "
  },
  {
    "path": "components/ui/accordion.tsx",
    "chars": 2118,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
  },
  {
    "path": "components/ui/alert-dialog.tsx",
    "chars": 4434,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimpor"
  },
  {
    "path": "components/ui/badge.tsx",
    "chars": 1128,
    "preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "components/ui/button.tsx",
    "chars": 1835,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "components/ui/color-picker.tsx",
    "chars": 2625,
    "preview": "'use client';\n\nimport { forwardRef, useEffect, useMemo, useRef, useState } from 'react';\nimport { HexColorPicker } from "
  },
  {
    "path": "components/ui/dropdown-menu.tsx",
    "chars": 7309,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
  },
  {
    "path": "components/ui/input.tsx",
    "chars": 824,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n  extends React.InputHTMLA"
  },
  {
    "path": "components/ui/popover.tsx",
    "chars": 1244,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } "
  },
  {
    "path": "components/ui/radio-group.tsx",
    "chars": 1481,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport {"
  },
  {
    "path": "components/ui/select-position.tsx",
    "chars": 1719,
    "preview": "\"use client\"\nimport { cn } from \"@lib/utils\"\nimport { useState } from \"react\"\n\nexport const SelectBackgroundPosition = ("
  },
  {
    "path": "components/ui/select.tsx",
    "chars": 5629,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, C"
  },
  {
    "path": "components/ui/slider.tsx",
    "chars": 1113,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } fr"
  },
  {
    "path": "components/ui/sonner.tsx",
    "chars": 894,
    "preview": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = Rea"
  },
  {
    "path": "components/ui/switch.tsx",
    "chars": 1164,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
  },
  {
    "path": "components/ui/tabs.tsx",
    "chars": 1897,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
  },
  {
    "path": "components/ui/textarea.tsx",
    "chars": 772,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n  extends React.Textare"
  },
  {
    "path": "components/ui/toast.tsx",
    "chars": 4859,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type"
  },
  {
    "path": "components/ui/toaster.tsx",
    "chars": 794,
    "preview": "\"use client\"\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport,\n} from"
  },
  {
    "path": "components/ui/use-toast.ts",
    "chars": 3948,
    "preview": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n  ToastActionElement,"
  },
  {
    "path": "components.json",
    "chars": 343,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n"
  },
  {
    "path": "lib/utils.ts",
    "chars": 166,
    "preview": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "next.config.js",
    "chars": 1594,
    "preview": "// const isProd = process.env.NODE_ENV === 'production'\nconst isProd = false;\nconst bundleAnalyzer = require('@next/bund"
  },
  {
    "path": "package.json",
    "chars": 3583,
    "preview": "{\n  \"name\": \"X Cards Native Tweet Card service for X\",\n  \"displayName\": \"Seamlessly integrate card services directly on "
  },
  {
    "path": "popup.tsx",
    "chars": 116,
    "preview": "// export const Popup = () => {\n//     return (\n//         <div className=\"w-4 h-4\">\n//         </div>\n//     )\n// }"
  },
  {
    "path": "postcss.config.js",
    "chars": 203,
    "preview": "/**\n * @type {import('postcss').ProcessOptions}\n */\nmodule.exports = {\n    plugins: {\n        \"postcss-import\": {},\n    "
  },
  {
    "path": "src/app/(app)/components/FrequentlyAskedQuestions.tsx",
    "chars": 2527,
    "preview": "\"use client\"\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\"\nimp"
  },
  {
    "path": "src/app/(app)/components/GoogleFontSelector.tsx",
    "chars": 3272,
    "preview": "import React, { useState, useEffect } from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectVal"
  },
  {
    "path": "src/app/(app)/components/ImageLayout.tsx",
    "chars": 4160,
    "preview": "import { useCardStore } from '@src/hooks/useCardStore';\nimport React, { useEffect, useRef, useState } from 'react';\n\ncon"
  },
  {
    "path": "src/app/(app)/components/LazyLoadAnimatedSection.tsx",
    "chars": 1021,
    "preview": "import { useInView } from 'react-intersection-observer';\nimport { motion} from \"framer-motion\"\nconst LazyLoadAnimatedSec"
  },
  {
    "path": "src/app/(app)/components/ResultIcon.tsx",
    "chars": 5481,
    "preview": "import React, { useId } from \"react\";\n\n// import noisePicture from \"../assets/noise.inline.png\";\n\n// import { SettingsTy"
  },
  {
    "path": "src/app/(app)/components/card-generator/color.tsx",
    "chars": 6437,
    "preview": "import { cn } from \"@lib/utils\";\nimport { ColorChangeHandler, SketchPicker } from \"react-color\";\nimport ResultIcon from "
  },
  {
    "path": "src/app/(app)/components/card-generator/controller/background-controller.tsx",
    "chars": 13481,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { ColorPicker } fro"
  },
  {
    "path": "src/app/(app)/components/card-generator/controller/card-controller.tsx",
    "chars": 7887,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { ColorPicker } fro"
  },
  {
    "path": "src/app/(app)/components/card-generator/controller/font-controller.tsx",
    "chars": 2220,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { Slider } from \"@c"
  },
  {
    "path": "src/app/(app)/components/card-generator/controller/iframe-controller.tsx",
    "chars": 10209,
    "preview": " {/* <AccordionItem value={'frames'}>\n                        <AccordionTrigger>Frames</AccordionTrigger>\n              "
  },
  {
    "path": "src/app/(app)/components/card-generator/controller/input-controller.tsx",
    "chars": 4433,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { Input } from \"@co"
  },
  {
    "path": "src/app/(app)/components/card-generator/display.tsx",
    "chars": 3212,
    "preview": "import { CommonLayouts, useCardStore } from \"@src/hooks/useCardStore\"\nimport { useMemo } from \"react\"\n\n\nimport { presets"
  },
  {
    "path": "src/app/(app)/components/card-generator/export-tab.tsx",
    "chars": 1126,
    "preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\n\nimport { Button } from \"@"
  },
  {
    "path": "src/app/(app)/components/card-generator/index.tsx",
    "chars": 1545,
    "preview": "import { Accordion, } from \"@components/ui/accordion\";\nimport { Display } from \"./display\";\nimport { useCardStore } from"
  },
  {
    "path": "src/app/(app)/components/card-generator/twitter-card.tsx",
    "chars": 9427,
    "preview": "import React, { useMemo, useState } from 'react';\nimport { cn } from '@/lib/utils'; // Assuming you have a utility for c"
  },
  {
    "path": "src/app/(app)/components/card-generator/wechat-card.tsx",
    "chars": 6597,
    "preview": "import type { CardStore, XConfig } from \"@src/hooks/useCardStore\"\n\ninterface WechatCardProps {\n    xConfig: XConfig,\n   "
  },
  {
    "path": "src/app/(app)/components/dynamic-style-tippy.tsx",
    "chars": 4329,
    "preview": "import Tippy from \"@tippyjs/react\";\nimport { useEffect, useRef } from \"react\";\n\nexport const DynamicStyleTippyComponent "
  },
  {
    "path": "src/app/(app)/components/hero.tsx",
    "chars": 2796,
    "preview": "\nimport chromeSvg from '@assets/chrome.svg';\nimport Image from \"next/image\";\n\nconst Hero = () => {\n    return (\n        "
  },
  {
    "path": "src/app/(app)/components/save-as-template-button.tsx",
    "chars": 1964,
    "preview": "import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFoote"
  },
  {
    "path": "src/app/(app)/components/sections/FeaturesGridSection.tsx",
    "chars": 4647,
    "preview": "// import yellowWaveDown from \"@/public/images/banner-wave.png\";\n// import Image from \"next/image\";\nimport { BookCopy, L"
  },
  {
    "path": "src/app/(app)/components/sections/features2.tsx",
    "chars": 3569,
    "preview": "// import { Icon } from \"@/components/ui/icon\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@src/component"
  },
  {
    "path": "src/app/(app)/components/sections/footer.tsx",
    "chars": 2622,
    "preview": "import Link from \"next/link\";\nimport Logo from \"@assets/icon.png\";\n\nexport const FooterSection = () => {\n    return (\n  "
  },
  {
    "path": "src/app/(app)/components/sections/video.tsx",
    "chars": 222,
    "preview": "\"use client\"\nimport ReactPlayer from 'react-player'\n\n\nconst VideoSection = () => {\n    return (<div className='mx-auto'>"
  },
  {
    "path": "src/app/(app)/components/template-list.tsx",
    "chars": 1949,
    "preview": "import { Button } from \"@components/ui/button\";\nimport { useCardStore } from \"@src/hooks/useCardStore\";\nimport { useTemp"
  },
  {
    "path": "src/app/(app)/components/x-form.tsx",
    "chars": 1083,
    "preview": "import { Button } from \"@components/ui/button\";\nimport { Input } from \"@components/ui/input\";\nimport { useCardStore, typ"
  },
  {
    "path": "src/app/(app)/layout.tsx",
    "chars": 16619,
    "preview": "// \"use client\"\nimport Image from 'next/image';\nimport Logo from '@assets/icon.png';\n\nconst AppLayout = ({ children }) ="
  },
  {
    "path": "src/app/(app)/page.tsx",
    "chars": 2700,
    "preview": "import * as _ from 'lodash-es';\nimport Hero from \"./components/hero\";\nimport { cn } from \"@lib/utils\";\nimport gradientBo"
  },
  {
    "path": "src/app/(app)/request.tsx",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/app/(extension)/independent/components/index.tsx",
    "chars": 4019,
    "preview": "\"use client\"\nimport { useEffect, useRef } from \"react\";\nimport { useCardStore, type XConfig } from \"@src/hooks/useCardSt"
  },
  {
    "path": "src/app/(extension)/independent/page.tsx",
    "chars": 462,
    "preview": "// import Index from \"./components\";\n\nexport async function generateStaticParams() {\n    return [{ slug: 'independent' }"
  },
  {
    "path": "src/app/(extension)/welcome/page.tsx",
    "chars": 11592,
    "preview": "import { cn } from \"@lib/utils\"\nimport gradientBottomSvg from '@assets/gradient-bottom.svg';\nimport introPng from '@asse"
  },
  {
    "path": "src/app/api/license/route.ts",
    "chars": 1827,
    "preview": "import { NextResponse, type NextRequest } from \"next/server\";\nimport { createClient } from '@supabase/supabase-js'\n\nexpo"
  },
  {
    "path": "src/app/api/x/route.ts",
    "chars": 721,
    "preview": "// import type { NextApiRequest } from \"next\";\n// import { NextResponse, type NextRequest } from \"next/server\";\n\n// // e"
  },
  {
    "path": "src/app/globals.css",
    "chars": 2445,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --f"
  },
  {
    "path": "src/app/layout.tsx",
    "chars": 2005,
    "preview": "import { Toaster } from \"@/components/ui/sonner\"\nimport Logo from '@assets/icon.png'\nimport './globals.css'\nimport { sit"
  },
  {
    "path": "src/app/utils/IFrameMessageSystem.ts",
    "chars": 1807,
    "preview": "type MessageCallback = (value: any) => void;\n\ninterface MessageEvent {\n  data: {\n    action: string;\n    value: any;\n  }"
  },
  {
    "path": "src/app/utils/element.ts",
    "chars": 1610,
    "preview": "export function traverseAndCheck(element, condition) {\n    if (condition(element)) return true;\n    return Array.from(el"
  },
  {
    "path": "src/app/utils/export.ts",
    "chars": 4359,
    "preview": "import type {XConfig} from \"@src/hooks/useCardStore\";\n\nimport JSZip from 'jszip';\nimport {saveAs} from 'file-saver';\n\n\ne"
  },
  {
    "path": "src/app/utils/format.ts",
    "chars": 670,
    "preview": "export function formatTimestamp(timestamp: number): string {\n    const date = new Date(timestamp * 1000); // Convert sec"
  },
  {
    "path": "src/app/utils/image.ts",
    "chars": 5116,
    "preview": "import { domToPng, domToJpeg, domToSvg, type Options, createContext, destroyContext } from 'modern-screenshot'\n\n\n\n// con"
  },
  {
    "path": "src/app/utils/index.ts",
    "chars": 8498,
    "preview": "import * as _ from 'lodash-es';\n\nimport { domToPng, domToJpeg, domToSvg, type Options, createContext, destroyContext } f"
  },
  {
    "path": "src/background/index.ts",
    "chars": 612,
    "preview": "import { sendToContentScript } from \"@plasmohq/messaging\"\n\n\nchrome.runtime.onMessage.addListener((request, sender, sendR"
  },
  {
    "path": "src/background/messages/code.ts",
    "chars": 2269,
    "preview": "import type { PlasmoMessaging } from \"@plasmohq/messaging\"\nimport * as _ from 'lodash-es'\nimport Browser from \"webextens"
  },
  {
    "path": "src/background/messages/tweet.ts",
    "chars": 825,
    "preview": "import type { PlasmoMessaging } from \"@plasmohq/messaging\"\nimport * as _ from 'lodash-es'\n\n\nconst handler: PlasmoMessagi"
  },
  {
    "path": "src/components/extension/card-button.tsx",
    "chars": 7664,
    "preview": "import { DynamicStyleTippyComponent } from \"@src/app/(app)/components/dynamic-style-tippy\";\nimport { useMemo, useRef, us"
  },
  {
    "path": "src/components/extension/input-code.tsx",
    "chars": 6587,
    "preview": "import React, { useEffect, useState } from 'react';\nimport { X } from 'lucide-react';\nimport { motion } from 'framer-mot"
  },
  {
    "path": "src/components/extension/label-with-icon.tsx",
    "chars": 657,
    "preview": "import React from 'react';\nimport { LockOpen, Lock } from 'lucide-react';\n\ninterface LabelWithIconProps {\n    label: str"
  },
  {
    "path": "src/components/extension/layout-options.tsx",
    "chars": 2510,
    "preview": "import { CommonLayouts } from '@src/hooks/useCardStore';\nimport React from 'react';\nimport styled from 'styled-component"
  },
  {
    "path": "src/components/extension/preset-color-list.tsx",
    "chars": 1658,
    "preview": "import ResultIcon from \"@src/app/(app)/components/ResultIcon\"\nimport { presets } from \"@src/app/(app)/components/card-ge"
  },
  {
    "path": "src/components/extension/tabs.tsx",
    "chars": 1711,
    "preview": "import React, { useEffect, useState } from 'react';\nimport { Lock } from 'lucide-react';\nimport { useTweetsStore } from "
  },
  {
    "path": "src/components/extension/tweet-manager.tsx",
    "chars": 3829,
    "preview": "import type { XConfig } from '@src/hooks/useCardStore';\nimport { Trash } from 'lucide-react';\nimport React, { useEffect,"
  },
  {
    "path": "src/components/extension/use-tweet-collection.ts",
    "chars": 3561,
    "preview": "import type { CardStore, XConfig } from \"@src/hooks/useCardStore\";\nimport { create } from \"zustand\";\nimport { fontSizeMa"
  },
  {
    "path": "src/components/extension/x-cards-toast/font-control.tsx",
    "chars": 4172,
    "preview": "import { Button } from \"@components/ui/button\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useTwee"
  },
  {
    "path": "src/components/extension/x-cards-toast/image-preview.tsx",
    "chars": 5193,
    "preview": "import React, { useState, useCallback, useRef, useEffect } from 'react';\nimport { ZoomIn, ZoomOut, Move, RotateCcw } fro"
  },
  {
    "path": "src/components/extension/x-cards-toast/index.module.css",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/components/extension/x-cards-toast/index.tsx",
    "chars": 16835,
    "preview": "import * as _ from 'lodash-es';\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { PresetColo"
  },
  {
    "path": "src/components/extension/x-cards-toast/padding-control.tsx",
    "chars": 1559,
    "preview": "import { Button } from \"@components/ui/button\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useTwee"
  },
  {
    "path": "src/components/extension/x-cards-toast/scale-control.tsx",
    "chars": 1311,
    "preview": "import React from 'react';\nimport { Tabs, TabsList, TabsTrigger } from '@components/ui/tabs'; // Assuming you're using R"
  },
  {
    "path": "src/components/extension/x-cards-toast/tweet-control.tsx",
    "chars": 2738,
    "preview": "import React, { useState } from 'react';\nimport { Activity, Clock, Heart, Twitter, User } from 'lucide-react';\nimport { "
  },
  {
    "path": "src/components/sortableList.tsx",
    "chars": 5641,
    "preview": "\"use client\"\n\n// npx shadcn-ui@latest add checkbox\n// npm  i react-use-measure\nimport { type Dispatch, type ReactNode, t"
  },
  {
    "path": "src/components/ui/BentoGrid.tsx",
    "chars": 2301,
    "preview": "import type { ReactNode } from \"react\";\nimport { ArrowRightIcon } from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@/l"
  },
  {
    "path": "src/components/ui/DotPattern.tsx",
    "chars": 1053,
    "preview": "import { useId } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface DotPatternProps {\n  width?: any;\n  height?:"
  },
  {
    "path": "src/components/ui/acetenity-tabs.tsx",
    "chars": 3895,
    "preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { cn } from \"@lib/utils\""
  },
  {
    "path": "src/components/ui/animated-list.tsx",
    "chars": 1845,
    "preview": "\"use client\";\n\nimport React, { type ReactElement, useEffect, useMemo, useState } from \"react\";\nimport { AnimatePresence,"
  },
  {
    "path": "src/components/ui/animatedBeam.tsx",
    "chars": 6019,
    "preview": "\"use client\";\n\nimport { type RefObject, useEffect, useId, useState } from \"react\";\nimport { motion } from \"framer-motion"
  },
  {
    "path": "src/components/ui/api-key-panel.tsx",
    "chars": 7819,
    "preview": "// import { Tabs } from \"@src/components/ui/acetenity-tabs\";\nimport { requestTestConnection } from \"@src/app/(app)/reque"
  },
  {
    "path": "src/components/ui/bold-copy.tsx",
    "chars": 1306,
    "preview": "import { Tourney } from \"next/font/google\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst tourney = Tourney({\n    subsets: ["
  },
  {
    "path": "src/components/ui/burnIn.tsx",
    "chars": 921,
    "preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface BlurIntProps {\n  wo"
  },
  {
    "path": "src/components/ui/card.tsx",
    "chars": 1877,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  Rea"
  },
  {
    "path": "src/components/ui/chart.tsx",
    "chars": 10586,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\nimport type {\n  NameType,\n  P"
  },
  {
    "path": "src/components/ui/fade-text.tsx",
    "chars": 1611,
    "preview": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { motion, type Variants } from \"framer-motion\";\n\ntype FadeTextPro"
  },
  {
    "path": "src/components/ui/grid-pattern.tsx",
    "chars": 1923,
    "preview": "import { useId } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface GridPatternProps {\n    width?: any;\n    hei"
  },
  {
    "path": "src/components/ui/grid.tsx",
    "chars": 1290,
    "preview": "interface GridProps {\n    /**\n     * Color of the grid\n     */\n    color?: string;\n\n    /**\n     * Size of the grid in p"
  },
  {
    "path": "src/components/ui/icon.tsx",
    "chars": 348,
    "preview": "import { icons } from \"lucide-react\";\n\nexport const Icon = ({\n    name,\n    color,\n    size,\n    className,\n}: {\n    nam"
  },
  {
    "path": "src/components/ui/loading-spinner.tsx",
    "chars": 597,
    "preview": "import { cn } from \"@lib/utils\"\n\nexport const LoadingSpinner = ({ className, isLoading, text }) => {\n    {\n        retur"
  },
  {
    "path": "src/components/ui/marquee.tsx",
    "chars": 1444,
    "preview": "import { cn } from \"@/lib/utils\";\n\ninterface MarqueeProps {\n    className?: string;\n    reverse?: boolean;\n    pauseOnHo"
  },
  {
    "path": "src/components/ui/scroll-based-velocity.tsx",
    "chars": 3165,
    "preview": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport {\n  motion,\n  useAnimationFrame,\n  use"
  },
  {
    "path": "src/components/ui/shimmer-button.tsx",
    "chars": 3516,
    "preview": "import React, { type CSSProperties } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ShimmerButtonPro"
  },
  {
    "path": "src/components/ui/text-generate-effect.tsx",
    "chars": 1411,
    "preview": "\"use client\";\nimport { useEffect } from \"react\";\nimport { motion, stagger, useAnimate } from \"framer-motion\";\nimport { c"
  },
  {
    "path": "src/components/ui/underline-hover-text.tsx",
    "chars": 1197,
    "preview": "\"use client\";\nimport React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface UnderlineHoverTextProps {\n"
  },
  {
    "path": "src/config/site.ts",
    "chars": 1757,
    "preview": "const baseSiteConfig = {\n    name: \"X Cards\",\n    description:\n        \"Share X anywhere, any format. A Chrome extension"
  },
  {
    "path": "src/contents/plasmo-overlay.css",
    "chars": 60,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n"
  },
  {
    "path": "src/contents/plasmo-overlay.tsx",
    "chars": 5527,
    "preview": "import React, { useEffect, useRef } from 'react'\nimport type { PlasmoCSConfig, PlasmoCSUIProps, PlasmoRender } from \"pla"
  },
  {
    "path": "src/contents/x-home.tsx",
    "chars": 2276,
    "preview": "import cssText from \"data-text:@src/contents/x.css\"\n\nimport type { PlasmoCSConfig, PlasmoCSUIProps, PlasmoGetInlineAncho"
  },
  {
    "path": "src/contents/x.css",
    "chars": 1817,
    "preview": "#plasmo-shadow-container {\n  z-index: 10 !important;\n  /* position: fixed !important; */\n  height: 100%;\n}\n\n#plasmo-inli"
  },
  {
    "path": "src/hooks/useCardStore.tsx",
    "chars": 9314,
    "preview": "import type { TweetControlState } from '@src/components/extension/use-tweet-collection';\nimport { create } from 'zustand"
  },
  {
    "path": "src/hooks/useTemplatesStore.tsx",
    "chars": 1619,
    "preview": "import { create } from 'zustand'\nimport { persist, createJSONStorage, type StateStorage } from 'zustand/middleware'\nimpo"
  },
  {
    "path": "src/lib/BlurGradientBg.module.js",
    "chars": 84236,
    "preview": "function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,r=Array(e);i<e;i++)r[i]=t[i];return r}function e(t,e,i){"
  },
  {
    "path": "src/sandbox.tsx",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tailwind.config.ts",
    "chars": 5521,
    "preview": "import type { Config } from \"tailwindcss\";\nconst svgToDataUri = require(\"mini-svg-data-uri\");\nconst colors = require(\"ta"
  },
  {
    "path": "tsconfig.json",
    "chars": 406,
    "preview": "{\n  \"extends\": \"plasmo/templates/tsconfig.base\",\n  \"exclude\": [\"node_modules\"],\n  \"include\": [\n    \".plasmo/index.d.ts\","
  },
  {
    "path": "vercel.json",
    "chars": 94,
    "preview": "{\n    \"functions\": {\n        \"app/api/**/*\": {\n            \"maxDuration\": 60\n        }\n    }\n}"
  }
]

About this extraction

This page contains the full source code of the hzeyuan/x-cards GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (476.8 KB), approximately 133.8k tokens, and a symbol index with 164 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!