├── .changeset └── config.json ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── 1-bug-report.yml │ ├── 2-feature-request.yml │ ├── 3-testimonial.yml │ ├── 99-new-chore.md │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci-delabeler.yml │ ├── ci-first-contributor.yml │ ├── ci-pr-snapshot.yml │ ├── ci-push-main.yml │ └── co-author.yml ├── .gitignore ├── .node-version ├── .prototools ├── .vscode ├── extensions.json └── settings.json ├── .well-known └── funding-manifest-urls ├── LICENSE ├── README.md ├── assets ├── banner-readme.png ├── discord-emoji-dark.png ├── discord-emoji-light.png ├── logo-adaptive.svg ├── logo-dark.svg ├── logo-discord.png ├── logo-discord.svg ├── logo-light.svg ├── logo-outlined-adaptive.svg ├── logo-outlined-dark.svg ├── logo-outlined-light.svg ├── old │ ├── banner-readme.png │ ├── discord-emoji-dark.png │ ├── discord-emoji-light.png │ ├── logo-dark.svg │ ├── logo-discord.png │ ├── logo-discord.svg │ ├── logo-light.svg │ ├── studioCMS-dark.png │ ├── studioCMS.png │ └── studiocms-new-logos.zip ├── studioCMS.png ├── studiocms-dark.png └── studiocms-new-logos.zip ├── biome.json ├── build-scripts ├── cmd │ └── build.js ├── index.js ├── jsconfig.json └── package.json ├── docs ├── .gitignore ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mts ├── ec.config.mjs ├── hostUtils.ts ├── lunaria.config.ts ├── lunaria │ ├── components.ts │ └── styles.ts ├── package.json ├── public │ ├── .well-known │ │ └── funding-manifest-urls │ ├── favicon-dark.png │ ├── favicon-light.png │ ├── favicon.svg │ ├── og.png │ └── socialproof │ │ ├── adam-matthiesen.jpg │ │ ├── louis-escher.jpg │ │ ├── matthew-justice-image.png │ │ ├── matthew-justice.jpg │ │ └── the-otterlord.png ├── scripts │ ├── changelog.ts │ ├── lib │ │ ├── changelogs.ts │ │ └── utils.ts │ ├── lunaria-report-bot.ts │ └── lunaria.mts ├── src │ ├── assets │ │ ├── astro.svg │ │ ├── avatar.png │ │ ├── bsky.svg │ │ ├── discord.svg │ │ ├── github.svg │ │ ├── logo-adaptive.svg │ │ ├── patreon.svg │ │ ├── swords.svg │ │ └── twitter.svg │ ├── components │ │ ├── ContributorList.astro │ │ ├── DropdownScript.astro │ │ ├── FacePile.astro │ │ ├── Gallery.astro │ │ ├── Integration.astro │ │ ├── ModalScript.astro │ │ ├── Newsletter.astro │ │ ├── PackageCatalog.astro │ │ ├── PreviewCard.astro │ │ ├── ProgressScript.astro │ │ ├── ReadMore.astro │ │ ├── Sponsors.astro │ │ ├── ThemeHelperScript.astro │ │ ├── ToasterScript.astro │ │ ├── TursoCLI.astro │ │ ├── Version.astro │ │ ├── Youtube.astro │ │ ├── fluid-grid.astro │ │ ├── icons │ │ │ └── GitHubIcon.astro │ │ ├── landing │ │ │ ├── DropdownExample.astro │ │ │ ├── EcosystemSection.astro │ │ │ ├── FormExample.astro │ │ │ ├── HeroSection.astro │ │ │ ├── ModalExample.astro │ │ │ ├── PageHeader.astro │ │ │ ├── ShowcaseSection.astro │ │ │ ├── SocialProofCard.astro │ │ │ ├── SocialProofSection.astro │ │ │ └── styles.css │ │ ├── media-card.astro │ │ ├── showcase-card.astro │ │ └── showcase-sites.astro │ ├── content.config.ts │ ├── content │ │ ├── docs │ │ │ └── docs │ │ │ │ ├── changelog.md │ │ │ │ ├── components │ │ │ │ ├── Sidebar.mdx │ │ │ │ ├── accordion.mdx │ │ │ │ ├── badge.mdx │ │ │ │ ├── breadcrumbs.mdx │ │ │ │ ├── button.mdx │ │ │ │ ├── card.mdx │ │ │ │ ├── center.mdx │ │ │ │ ├── checkbox.mdx │ │ │ │ ├── divider.mdx │ │ │ │ ├── dropdown.mdx │ │ │ │ ├── footer.mdx │ │ │ │ ├── group.mdx │ │ │ │ ├── input.mdx │ │ │ │ ├── modal.mdx │ │ │ │ ├── progress.mdx │ │ │ │ ├── radio-group.mdx │ │ │ │ ├── row.mdx │ │ │ │ ├── select-searchable.mdx │ │ │ │ ├── select.mdx │ │ │ │ ├── tabs.mdx │ │ │ │ ├── textarea.mdx │ │ │ │ ├── theme-toggle.mdx │ │ │ │ ├── toast.mdx │ │ │ │ ├── toggle.mdx │ │ │ │ └── user.mdx │ │ │ │ ├── guides │ │ │ │ └── customization.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── showcase.mdx │ │ │ │ ├── upgrade-guides │ │ │ │ ├── 0.1.0-to-0.3.0.mdx │ │ │ │ └── 0.3-to-0.4.mdx │ │ │ │ └── utilities │ │ │ │ └── theme-helper.mdx │ │ ├── i18n │ │ │ └── en.json │ │ ├── showcase.json │ │ └── socialproof │ │ │ ├── adam-matthiesen.json │ │ │ ├── louis-escher.json │ │ │ └── matthew-justice.json │ ├── env.d.ts │ ├── pages │ │ └── index.astro │ ├── plugins │ │ ├── rehype.types.ts │ │ ├── rehypeAutolink.ts │ │ ├── rehypeExternalLinks.ts │ │ └── rehypePluginKit.ts │ ├── share-link.ts │ ├── starlightOverrides │ │ ├── Head.astro │ │ ├── Header.astro │ │ ├── PageTitle.astro │ │ ├── Sidebar.astro │ │ └── SiteTitle.astro │ ├── styles │ │ ├── sponsorcolors.css │ │ └── starlight.css │ ├── util-server.ts │ └── util │ │ ├── SponsorLink.astro │ │ ├── contributors.config.ts │ │ ├── getContributors.ts │ │ └── thum.io.ts ├── starlight-types.ts ├── starlight-virtual.d.ts └── tsconfig.json ├── package.json ├── packages └── studiocms_ui │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── env.d.ts │ ├── package.json │ ├── src │ ├── components │ │ ├── Accordion │ │ │ ├── Accordion.astro │ │ │ ├── Item.astro │ │ │ ├── accordion.css │ │ │ └── accordion.ts │ │ ├── Badge │ │ │ ├── Badge.astro │ │ │ └── badge.css │ │ ├── Breadcrumbs │ │ │ ├── Breadcrumbs.astro │ │ │ └── breadcrumbs.css │ │ ├── Button │ │ │ ├── Button.astro │ │ │ └── button.css │ │ ├── Card │ │ │ ├── Card.astro │ │ │ └── card.css │ │ ├── Center │ │ │ ├── Center.astro │ │ │ └── center.css │ │ ├── Checkbox │ │ │ ├── Checkbox.astro │ │ │ ├── checkbox.css │ │ │ └── checkbox.ts │ │ ├── Divider │ │ │ ├── Divider.astro │ │ │ └── divider.css │ │ ├── Dropdown │ │ │ ├── Dropdown.astro │ │ │ ├── dropdown.css │ │ │ └── dropdown.ts │ │ ├── Footer │ │ │ ├── Footer.astro │ │ │ └── footer.css │ │ ├── Group │ │ │ ├── Group.astro │ │ │ └── group.css │ │ ├── Icon │ │ │ ├── Icon.astro │ │ │ ├── IconBase.astro │ │ │ └── iconType.ts │ │ ├── Input │ │ │ ├── Input.astro │ │ │ └── input.css │ │ ├── Modal │ │ │ ├── Modal.astro │ │ │ ├── modal.css │ │ │ └── modal.ts │ │ ├── Progress │ │ │ ├── Progress.astro │ │ │ ├── helper.ts │ │ │ ├── progress.css │ │ │ └── progress.ts │ │ ├── RadioGroup │ │ │ ├── RadioGroup.astro │ │ │ ├── radiogroup.css │ │ │ └── radiogroup.ts │ │ ├── Row │ │ │ ├── Row.astro │ │ │ └── row.css │ │ ├── SearchSelect │ │ │ ├── SearchSelect.astro │ │ │ ├── searchselect.css │ │ │ └── searchselect.ts │ │ ├── Select │ │ │ ├── Select.astro │ │ │ ├── select.css │ │ │ └── select.ts │ │ ├── Sidebar │ │ │ ├── Double.astro │ │ │ ├── Single.astro │ │ │ └── helpers.ts │ │ ├── Tabs │ │ │ ├── TabItem.astro │ │ │ ├── Tabs.astro │ │ │ ├── tabs.css │ │ │ └── tabs.ts │ │ ├── Textarea │ │ │ ├── Textarea.astro │ │ │ └── textarea.css │ │ ├── ThemeToggle │ │ │ ├── ThemeToggle.astro │ │ │ ├── themetoggle.css │ │ │ └── themetoggle.ts │ │ ├── Toast │ │ │ ├── Toaster.astro │ │ │ ├── toast.ts │ │ │ ├── toaster.css │ │ │ └── toaster.ts │ │ ├── Toggle │ │ │ ├── Toggle.astro │ │ │ ├── toggle.css │ │ │ └── toggle.ts │ │ └── User │ │ │ ├── User.astro │ │ │ └── user.css │ ├── css │ │ ├── colors.css │ │ ├── global.css │ │ ├── radii.css │ │ └── resets.css │ ├── icons │ │ ├── Checkmark.astro │ │ ├── ChevronUpDown.astro │ │ ├── User.astro │ │ └── X-Mark.astro │ ├── index.ts │ ├── toolbar │ │ ├── ColorPicker.ts │ │ ├── icon.ts │ │ └── index.ts │ ├── types │ │ └── index.ts │ └── utils │ │ ├── ThemeHelper.ts │ │ ├── colors.ts │ │ ├── generateID.ts │ │ ├── headers.ts │ │ ├── iconStrings.ts │ │ ├── iconifyUtils.ts │ │ └── integration-utils.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── filter-warnings.cjs └── tsconfig.base.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "withstudiocms/ui" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": ["docs", "build-scripts"] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines with a newline ending every file 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @withstudiocms/exalted @louisescher 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | type: "Bug" 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Tell us what you see! 17 | value: "A bug happened!" 18 | validations: 19 | required: true 20 | - type: dropdown 21 | id: package 22 | attributes: 23 | label: Package 24 | description: Which package is the bug report for? 25 | options: 26 | - '@studiocms/ui' 27 | default: 0 28 | validations: 29 | required: true 30 | - type: input 31 | id: version 32 | attributes: 33 | label: Version 34 | description: What version are you running? 35 | placeholder: 0.1.0 36 | validations: 37 | required: true 38 | - type: dropdown 39 | id: browsers 40 | attributes: 41 | label: What browsers are you seeing the problem on? 42 | multiple: true 43 | options: 44 | - Firefox 45 | - Chrome 46 | - Safari 47 | - Microsoft Edge 48 | - type: textarea 49 | id: logs 50 | attributes: 51 | label: Relevant log output 52 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 53 | render: shell 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea/feature for @studiocms/ui 3 | title: '[Feature Request]: ' 4 | labels: 5 | - feat 6 | type: 'Feature' 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for proposing a new feature for `@studiocms/ui`! 12 | - type: textarea 13 | id: idea 14 | attributes: 15 | label: Describe your feature request 16 | description: Try to give as much detail as possible. 17 | placeholder: Tell us what you want! 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: examples 22 | attributes: 23 | label: Other examples? 24 | description: Has there been something similar made elsewhere? 25 | placeholder: If not just respond no 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: additional-context 30 | attributes: 31 | label: Additional context 32 | description: Add any other context or screenshots about the plugin request here. 33 | validations: 34 | required: false 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-testimonial.yml: -------------------------------------------------------------------------------- 1 | name: Testimonial 2 | description: Share your experience with StudioCMS/UI 3 | title: "[Testimonial]: " 4 | labels: ["testimonial"] 5 | type: "Testimonial" 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this Testimonial submission! 11 | - type: input 12 | id: name 13 | attributes: 14 | label: "Desired Display Name" 15 | description: "Would you like an alternate display name from your GitHub Profile? (Optional)" 16 | placeholder: "John Doe" 17 | validations: 18 | required: false 19 | - type: input 20 | id: handle 21 | attributes: 22 | label: "Desired Display Handle" 23 | description: "Would you like an alternate display handle from your GitHub Profile? (Optional)" 24 | placeholder: "@JohnDoe" 25 | validations: 26 | required: false 27 | - type: textarea 28 | id: message 29 | attributes: 30 | label: "Your Message? (You can include a screenshot or photo)" 31 | description: "Tell us how you feel about @studiocms/ui" 32 | placeholder: "Tell us what you think!" 33 | validations: 34 | required: true 35 | - type: checkboxes 36 | attributes: 37 | label: "Give consent to having your likeness posted on the StudioCMS UI Website" 38 | description: "By checking this box you consent to allow us to use your likeness/name/photo on our https://ui.studiocms.dev website" 39 | options: 40 | - label: "I consent to having my likeness posted on the StudioCMS UI Website" 41 | required: true 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/99-new-chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Chore 3 | about: Used to create a new Chore for a needed task 4 | title: "Chore: [NAME]" 5 | labels: chore 6 | type: "Task" 7 | --- 8 | 9 | **Is your chore related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 💁 Support 4 | url: https://chat.studiocms.dev 5 | about: 'This issue tracker is not for support questions. Join us on Discord for assistance!' 6 | - name: 👾 Chat 7 | url: https://chat.studiocms.dev 8 | about: Our Discord server is active, come join us! -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | <!-- Thank you for opening a PR! We really appreciate you taking the time to help out 🙌 --> 2 | 3 | #### Description 4 | 5 | - Closes # <!-- If applicable add an issue number to this PR so it can be closed otherwise feel free to remove this. --> 6 | - What does this PR change? Give us a brief description. 7 | 8 | <!-- 9 | Here’s what will happen next: 10 | 11 | One of our maintainers will review your pull request as soon as possible. We strive to provide feedback within a day, but please understand that responses may occasionally take longer depending on the circumstance and our availability. If we request any changes, please feel free to ask for clarification or provide additional context. We appreciate your patience and contribution to the project. 12 | --> -------------------------------------------------------------------------------- /.github/workflows/ci-delabeler.yml: -------------------------------------------------------------------------------- 1 | name: Remove labels from merged PRs and issues 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - closed 7 | issues: 8 | types: 9 | - closed 10 | 11 | permissions: 12 | pull-requests: write 13 | issues: write 14 | 15 | jobs: 16 | remove-merged-pr-labels: 17 | name: Remove merged pull request labels 18 | if: github.event.pull_request.merged 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: mondeja/remove-labels-gh-action@b7118e4ba5dca74acf1059b3cb7660378ff9ab1a # v2 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | labels: | 25 | awaiting review(s) 26 | awaiting review 27 | in progress 28 | question 29 | help wanted 30 | 31 | remove-closed-pr-labels: 32 | name: Remove closed pull request labels 33 | if: github.event_name == 'pull_request_target' && (! github.event.pull_request.merged) 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: mondeja/remove-labels-gh-action@b7118e4ba5dca74acf1059b3cb7660378ff9ab1a # v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | labels: | 40 | in discussion 41 | help wanted 42 | 43 | remove-closed-issue-labels: 44 | name: Remove closed issue labels 45 | if: github.event.issue.state == 'closed' 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: mondeja/remove-labels-gh-action@b7118e4ba5dca74acf1059b3cb7660378ff9ab1a # v2 49 | with: 50 | token: ${{ secrets.GITHUB_TOKEN }} 51 | labels: | 52 | in discussion 53 | in progress 54 | help wanted -------------------------------------------------------------------------------- /.github/workflows/ci-first-contributor.yml: -------------------------------------------------------------------------------- 1 | name: CI - First Time Contributor 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | pull_request_target: 7 | types: [opened, closed] 8 | 9 | jobs: 10 | check_for_first_interaction: 11 | name: Check for First Interaction 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 16 | 17 | - name: First Interaction Check 18 | uses: zephyrproject-rtos/action-first-interaction@1d8459ca65b335265f1285568221e229d45a995e 19 | with: 20 | repo-token: ${{ secrets.STUDIOCMS_SERVICE_TOKEN }} 21 | issue-message: > 22 | Hi @${{ github.event.issue.user.login }} 👋 23 | 24 | Welcome to the StudioCMS project! We're excited to have you here. Thanks for opening your first issue! 🎉 25 | 26 | If you are reporting a bug, please make sure to include the following information in your issue: 27 | - A clear description of the issue 28 | - Steps to reproduce the issue 29 | - Expected behavior 30 | - Actual behavior 31 | - Environment details (OS, hardware, etc.) 32 | 33 | If you have any questions or need help, feel free to ask. We're here to help you! 🚀 34 | 35 | If you're interested in contributing to the project, please check out our [Contributing Guide](https://github.com/withstudiocms/.github/blob/main/CONTRIBUTING.md) and join our [Discord community](https://chat.studiocms.dev) to stay in the loop for any future help we may need! 36 | 37 | Thanks again for opening your first issue! 🙌 38 | 39 | - The StudioCMS Team 40 | 41 | pr-message: > 42 | Hello @${{ github.event.pull_request.user.login }}, and thank you for opening your first pull request to StudioCMS! 🎉 43 | 44 | Please make sure to review the project's [Contributing Guide](https://github.com/withstudiocms/.github/blob/main/CONTRIBUTING.md) to ensure your pull request meets our quality standards. 45 | 46 | We're excited to have you here and appreciate your contribution. If you have not already, please make sure to include the following information in your pull request: 47 | - A clear description of the changes 48 | - Steps to reproduce the issue (if applicable) 49 | - Any relevant screenshots or logs 50 | 51 | Our team will review your pull request as soon as possible. If you have any questions or need help, feel free to ask. We're here to help you! 🚀 52 | 53 | In the meantime, you will notice that a few checks will run on your pull request. These checks are automated and help us ensure that your changes meet our quality standards. If you see any errors or warnings, don't worry! Our team will help you address them. 😊 54 | -------------------------------------------------------------------------------- /.github/workflows/ci-pr-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: CI - Snapshot Release on PRs 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'packages/**' 7 | 8 | jobs: 9 | release: 10 | name: Pkg-pr-new Snapshot Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | 16 | - name: Install Tools & Dependencies 17 | uses: withstudiocms/automations/.github/actions/install@main 18 | 19 | - name: Build packages 20 | run: pnpm ci:prepublish 21 | 22 | - name: Publish packages 23 | run: pnpm ci:snapshot 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/ci-push-main.yml: -------------------------------------------------------------------------------- 1 | name: CI - Push to main 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: ${{ github.workflow }}-${{ github.ref }} 10 | 11 | permissions: 12 | issues: write 13 | repository-projects: read 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | id-token: write 18 | 19 | jobs: 20 | format: 21 | runs-on: ubuntu-latest 22 | env: 23 | NODE_OPTIONS: "--max_old_space_size=4096" 24 | steps: 25 | - name: Check out code using Git 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 27 | with: 28 | ref: ${{ github.head_ref }} 29 | token: ${{ secrets.STUDIOCMS_SERVICE_TOKEN }} 30 | 31 | - name: Install Tools & Dependencies 32 | uses: withstudiocms/automations/.github/actions/install@main 33 | 34 | - name: Format code 35 | run: pnpm run lint:fix 36 | 37 | - name: Commit changes 38 | uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5 39 | with: 40 | commit_message: '[ci] lint' 41 | branch: ${{ github.head_ref }} 42 | commit_user_name: studiocms-no-reply 43 | commit_user_email: no-reply@studiocms.dev 44 | commit_author: StudioCMS <no-reply@studiocms.dev> 45 | 46 | changeset-release: 47 | name: Changeset Release 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout Repo 51 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 52 | 53 | - name: Install Tools & Dependencies 54 | uses: withstudiocms/automations/.github/actions/install@main 55 | 56 | - name: Build packages 57 | run: pnpm ci:prepublish 58 | 59 | - name: Create Release Pull Request or Publish to npm 60 | id: changesets 61 | uses: changesets/action@v1 62 | with: 63 | setupGitUser: true 64 | commit: "👷 [ci]: Version Packages" 65 | title: "👷 [ci]: Ready for Release" 66 | version: pnpm ci:version 67 | publish: pnpm ci:publish 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.STUDIOCMS_SERVICE_TOKEN }} 70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 71 | 72 | - name: Add Label to CI PR 73 | if: ${{ steps.changesets.outputs.hasChangesets == 'true' }} 74 | run: gh pr edit "$PR_URL" --add-label "ci" 75 | env: 76 | PR_URL: ${{ steps.changesets.outputs.pull_request_url }} 77 | GITHUB_TOKEN: ${{ secrets.STUDIOCMS_SERVICE_TOKEN }} 78 | 79 | mergebot: 80 | if: ${{ github.repository_owner == 'withstudiocms' && github.event_name == 'push' && github.event.commits[0].message != '[ci] lint' && github.event.commits[0].author.username != 'dependabot[bot]' }} 81 | uses: withstudiocms/automations/.github/workflows/mergebot.yml@main 82 | secrets: 83 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_MERGEBOT }} 84 | -------------------------------------------------------------------------------- /.github/workflows/co-author.yml: -------------------------------------------------------------------------------- 1 | name: Write coauthors to a pull request 2 | 3 | # To use this type '!coauthor' as a comment in a PR 4 | 5 | on: 6 | issue_comment: 7 | types: 8 | - created 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | generate-coauthors: 15 | name: Generate Coauthor List 16 | if: ${{ github.event.issue.pull_request }} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 20 | - uses: kevinzunigacuellar/coauthor-action@292b32bdbffc663431a2a37b3bfa597d83a74905 # v0.1.3 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .npmrc 4 | 5 | dist/ -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.14.0 -------------------------------------------------------------------------------- /.prototools: -------------------------------------------------------------------------------- 1 | biome = "1.9.2" 2 | node = "20.14.0" 3 | pnpm = "9.5.0" 4 | 5 | [plugins] 6 | biome = "source:https://raw.githubusercontent.com/Phault/proto-toml-plugins/main/biome/plugin.toml" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "biomejs.biome", 5 | "streetsidesoftware.code-spell-checker" 6 | ], 7 | "unwantedRecommendations": [] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "editor.codeActionsOnSave": { 4 | "quickfix.biome": "explicit", 5 | "source.organizeImports.biome": "explicit" 6 | }, 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "editor.gotoLocation.multipleDefinitions": "goto", 9 | "cSpell.words": [ 10 | "astrojs", 11 | "checkmark", 12 | "createtoast", 13 | "dismissable", 14 | "Eleventy", 15 | "fira", 16 | "fontsource", 17 | "frontmatter", 18 | "heroicons", 19 | "iconify", 20 | "louisescher", 21 | "Matthiesen", 22 | "onest", 23 | "pathe", 24 | "polyline", 25 | "searchselect", 26 | "socialproof", 27 | "studiocms", 28 | "themetoggle", 29 | "Thum", 30 | "tsconfigs", 31 | "twoslash", 32 | "vite", 33 | "withstudiocms" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://studiocms.dev/funding.json 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 StudioCMS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@studiocms/ui` 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/@studiocms/ui)](https://npm.im/@studiocms/ui) 4 | [![Formatted with Biome](https://img.shields.io/badge/Formatted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev/) 5 | [![Built with Astro](https://astro.badg.es/v2/built-with-astro/tiny.svg)](https://astro.build) 6 | 7 | This is the UI library that we use to build StudioCMS. 8 | 9 | To see how to get started, check out [the documentation](https://ui.studiocms.dev). 10 | 11 | ## Contributing 12 | 13 | We welcome contributions from the community! Whether it's bug reports, feature requests, or code contributions, we appreciate your help in making this project better. 14 | 15 | ### Bug Reports and Feature Requests 16 | 17 | If you encounter any bugs or have ideas for new features, please [open an issue](https://github.com/withstudiocms/ui/issues/new/choose) on our [GitHub repository](https://github.com/withstudiocms/ui/). When creating a new issue, please provide as much detail as possible, including steps to reproduce the issue (for bugs) or a clear description of the proposed feature. 18 | 19 | ### Code Contributions 20 | 21 | If you'd like to contribute code to this project, please follow these steps: 22 | 23 | 1. Fork the repository and create a new branch for your contribution. 24 | 2. Make your changes and ensure that the code follows our coding conventions and style guidelines. 25 | 3. Write tests for your changes, if applicable. 26 | 4. Update the documentation, if necessary. 27 | 5. Commit your changes and push them to your forked repository. 28 | 6. Open a pull request against the main repository, providing a clear description of your changes and their purpose. 29 | 30 | We will review your contribution as soon as possible and provide feedback or merge it into the main codebase if everything looks good. 31 | 32 | Please note that by contributing to this project, you agree to our [Code of Conduct](https://github.com/withstudiocms/.github/blob/main/CODE_OF_CONDUCT.md). 33 | 34 | Thank you for your interest in contributing to this project! 35 | 36 | ## Chat with Us 37 | 38 | We have an active community of developers on the StudioCMS [Discord Server](https://chat.studiocms.dev/). Feel free to join and connect with other contributors, ask questions, or discuss ideas related to this project or other StudioCMS projects. 39 | 40 | ## Our ToolSet 41 | 42 | For an up-to-date list of our main tools check out our [`.prototools`](https://github.com/withstudiocms/ui/blob/main/.prototools) file. 43 | 44 | For more information about Proto checkout [Proto's Website](https://moonrepo.dev/proto). 45 | 46 | ## Licensing 47 | 48 | [MIT Licensed](./LICENSE) - StudioCMS 2024 49 | -------------------------------------------------------------------------------- /assets/banner-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/banner-readme.png -------------------------------------------------------------------------------- /assets/discord-emoji-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/discord-emoji-dark.png -------------------------------------------------------------------------------- /assets/discord-emoji-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/discord-emoji-light.png -------------------------------------------------------------------------------- /assets/logo-adaptive.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="295" width="460" height="466" rx="32" fill="currentColor"/> 3 | <path d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="currentColor"/> 4 | <path d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="currentColor"/> 5 | </svg> -------------------------------------------------------------------------------- /assets/logo-dark.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="295" width="460" height="466" rx="32" fill="black"/> 3 | <path d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="black"/> 4 | <path d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="black"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /assets/logo-discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/logo-discord.png -------------------------------------------------------------------------------- /assets/logo-discord.svg: -------------------------------------------------------------------------------- 1 | <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect width="512" height="512" fill="white"/> 3 | <rect x="220.773" y="85.3281" width="198.72" height="201.312" rx="13.824" fill="black"/> 4 | <path d="M210.841 272.816V157.04H171.097C163.463 157.04 157.273 163.229 157.273 170.864V343.232C157.273 350.867 163.463 357.056 171.097 357.056H342.817C350.452 357.056 356.641 350.867 356.641 343.232V297.008H235.033C221.673 297.008 210.841 286.177 210.841 272.816Z" fill="black"/> 5 | <path d="M146.901 343.232V227.456H107.157C99.5222 227.456 93.333 233.645 93.333 241.28V413.648C93.333 421.283 99.5222 427.472 107.157 427.472H278.877C286.512 427.472 292.701 421.283 292.701 413.648V367.424H171.093C157.732 367.424 146.901 356.593 146.901 343.232Z" fill="black"/> 6 | </svg> 7 | -------------------------------------------------------------------------------- /assets/logo-light.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="295" width="460" height="466" rx="32" fill="white"/> 3 | <path d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="white"/> 4 | <path d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="white"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /assets/logo-outlined-adaptive.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="305" y="10" width="440" height="446" rx="22" stroke="currentColor" stroke-width="20"/> 3 | <path d="M262 176V434C262 470.451 291.549 500 328 500H599.5V597C599.5 609.15 589.65 619 577.5 619H180C167.85 619 158 609.15 158 597V198C158 185.85 167.85 176 180 176H262Z" stroke="currentColor" stroke-width="20"/> 4 | <path d="M114 339V597C114 633.451 143.549 663 180 663H451.5V760C451.5 772.15 441.65 782 429.5 782H32C19.8497 782 10 772.15 10 760V361C10 348.85 19.8497 339 32 339H114Z" stroke="currentColor" stroke-width="20"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /assets/logo-outlined-dark.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="305" y="10" width="440" height="446" rx="22" stroke="black" stroke-width="20"/> 3 | <path d="M262 176V434C262 470.451 291.549 500 328 500H599.5V597C599.5 609.15 589.65 619 577.5 619H180C167.85 619 158 609.15 158 597V198C158 185.85 167.85 176 180 176H262Z" stroke="black" stroke-width="20"/> 4 | <path d="M114 339V597C114 633.451 143.549 663 180 663H451.5V760C451.5 772.15 441.65 782 429.5 782H32C19.8497 782 10 772.15 10 760V361C10 348.85 19.8497 339 32 339H114Z" stroke="black" stroke-width="20"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /assets/logo-outlined-light.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect x="305" y="10" width="440" height="446" rx="22" stroke="white" stroke-width="20"/> 3 | <path d="M262 176V434C262 470.451 291.549 500 328 500H599.5V597C599.5 609.15 589.65 619 577.5 619H180C167.85 619 158 609.15 158 597V198C158 185.85 167.85 176 180 176H262Z" stroke="white" stroke-width="20"/> 4 | <path d="M114 339V597C114 633.451 143.549 663 180 663H451.5V760C451.5 772.15 441.65 782 429.5 782H32C19.8497 782 10 772.15 10 760V361C10 348.85 19.8497 339 32 339H114Z" stroke="white" stroke-width="20"/> 5 | </svg> 6 | -------------------------------------------------------------------------------- /assets/old/banner-readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/banner-readme.png -------------------------------------------------------------------------------- /assets/old/discord-emoji-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/discord-emoji-dark.png -------------------------------------------------------------------------------- /assets/old/discord-emoji-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/discord-emoji-light.png -------------------------------------------------------------------------------- /assets/old/logo-discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/logo-discord.png -------------------------------------------------------------------------------- /assets/old/studioCMS-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/studioCMS-dark.png -------------------------------------------------------------------------------- /assets/old/studioCMS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/studioCMS.png -------------------------------------------------------------------------------- /assets/old/studiocms-new-logos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/old/studiocms-new-logos.zip -------------------------------------------------------------------------------- /assets/studioCMS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/studioCMS.png -------------------------------------------------------------------------------- /assets/studiocms-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/studiocms-dark.png -------------------------------------------------------------------------------- /assets/studiocms-new-logos.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/assets/studiocms-new-logos.zip -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true, 7 | "defaultBranch": "main" 8 | }, 9 | "files": { 10 | "ignoreUnknown": true, 11 | "ignore": ["**/.astro/**", "**/package.json", "**/dist/**"] 12 | }, 13 | "formatter": { 14 | "lineWidth": 100, 15 | "lineEnding": "lf" 16 | }, 17 | "javascript": { 18 | "formatter": { 19 | "quoteStyle": "single", 20 | "trailingCommas": "es5" 21 | } 22 | }, 23 | "json": { 24 | "formatter": { 25 | "indentStyle": "space" 26 | } 27 | }, 28 | "organizeImports": { 29 | "enabled": true 30 | }, 31 | "linter": { 32 | "enabled": true, 33 | "rules": { 34 | "recommended": true, 35 | "suspicious": { 36 | "noExplicitAny": "warn", 37 | "noImplicitAnyLet": "info" 38 | }, 39 | "style": { 40 | "noNonNullAssertion": "off", 41 | "noParameterAssign": "info", 42 | "useSingleVarDeclarator": "info" 43 | } 44 | } 45 | }, 46 | "overrides": [ 47 | { 48 | "include": ["*.astro"], 49 | "linter": { 50 | "rules": { 51 | "style": { 52 | "useConst": "off", 53 | "useImportType": "off" 54 | } 55 | } 56 | } 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /build-scripts/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | export default async function run() { 3 | const [cmd, ...args] = process.argv.slice(2); 4 | switch (cmd) { 5 | case 'dev': 6 | case 'build': { 7 | const { default: build } = await import('./cmd/build.js'); 8 | await build(...args, cmd === 'dev' ? 'IS_DEV' : undefined); 9 | break; 10 | } 11 | } 12 | } 13 | 14 | run(); 15 | -------------------------------------------------------------------------------- /build-scripts/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "strict": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "target": "esnext" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-scripts", 3 | "version": "0.0.14", 4 | "private": true, 5 | "type": "module", 6 | "main": "./index.js", 7 | "bin": { 8 | "build-scripts": "./index.js" 9 | }, 10 | "dependencies": { 11 | "esbuild": "^0.24.2", 12 | "esbuild-plugin-copy": "^2.1.1", 13 | "fast-glob": "^3.3.2", 14 | "kleur": "^4.1.5", 15 | "p-limit": "^6.1.0", 16 | "tinyexec": "^0.3.1", 17 | "tsconfck": "^3.1.4" 18 | } 19 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | 24 | # Changelog documentation 25 | docs/changelog.md 26 | 27 | # Eleventy cache 28 | .cache/ -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # StudioCMS UI Documentation Website 2 | 3 | https://ui.studiocms.dev 4 | -------------------------------------------------------------------------------- /docs/ec.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineEcConfig } from '@astrojs/starlight/expressive-code'; 2 | import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'; 3 | import { transformerColorizedBrackets } from '@shikijs/colorized-brackets'; 4 | import ecTwoSlash from 'expressive-code-twoslash'; 5 | 6 | export default defineEcConfig({ 7 | shiki: { 8 | transformers: [transformerColorizedBrackets()], 9 | }, 10 | themes: ['dark-plus', 'light-plus'], 11 | defaultProps: { 12 | showLineNumbers: false, 13 | }, 14 | plugins: [ 15 | ecTwoSlash({ 16 | twoslashOptions: { 17 | compilerOptions: { 18 | strict: true, 19 | moduleResolution: 100, 20 | target: 99, 21 | exactOptionalPropertyTypes: true, 22 | downlevelIteration: true, 23 | skipLibCheck: true, 24 | lib: ['ES2022', 'DOM', 'DOM.Iterable', 'dom'], 25 | noEmit: true, 26 | }, 27 | }, 28 | }), 29 | pluginLineNumbers(), 30 | ], 31 | styleOverrides: { 32 | frames: { 33 | editorActiveTabIndicatorBottomColor: 'var(--sl-color-accent)', 34 | }, 35 | gutterForeground: 'rgba(166, 166, 166, 0.84)', 36 | twoSlash: { 37 | cursorColor: '#f8f8f2', 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /docs/hostUtils.ts: -------------------------------------------------------------------------------- 1 | function stripHTTPandHTTPS(url: string) { 2 | return url.replace('http://', '').replace('https://', ''); 3 | } 4 | 5 | function stripTrailingSlash(url: string) { 6 | return url.replace(/\/$/, ''); 7 | } 8 | 9 | function setHTTP(url: string) { 10 | return `http://${url}`; 11 | } 12 | 13 | function setHTTPS(url: string) { 14 | return `https://${url}`; 15 | } 16 | 17 | function splitListandSelectFirst(list: string) { 18 | if (list.indexOf(',') === -1) return list; 19 | return list.split(',')[0]; 20 | } 21 | 22 | /** 23 | * Get the Domain of the Coolify URL from the coolify runtime environment 24 | * 25 | * Requires the COOLIFY_FQDN environment variable to be set as per the coolify docs: 26 | * 27 | * @see https://coolify.io/docs/knowledge-base/environment-variables#predefined-variables 28 | */ 29 | export const getCoolifyURL = (returnHttps?: boolean) => { 30 | const urlList = process.env.COOLIFY_FQDN; 31 | if (!urlList) { 32 | return undefined; 33 | } 34 | const url = splitListandSelectFirst(urlList); 35 | const strippedUrl = stripTrailingSlash(stripHTTPandHTTPS(url)); 36 | if (returnHttps) { 37 | return setHTTPS(strippedUrl); 38 | } 39 | return setHTTP(strippedUrl); 40 | }; 41 | 42 | export default getCoolifyURL; 43 | -------------------------------------------------------------------------------- /docs/lunaria.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@lunariajs/core/config'; 2 | 3 | export default defineConfig({ 4 | repository: { 5 | name: 'withstudiocms/ui', 6 | rootDir: 'docs', 7 | hosting: 'github', 8 | branch: 'main', 9 | }, 10 | sourceLocale: { 11 | label: 'English', 12 | lang: 'en', 13 | parameters: { 14 | tag: 'en', 15 | }, 16 | }, 17 | locales: [ 18 | { 19 | label: 'Español', 20 | lang: 'es', 21 | parameters: { 22 | tag: 'es', 23 | }, 24 | }, 25 | { 26 | label: 'Français', 27 | lang: 'fr', 28 | parameters: { 29 | tag: 'fr', 30 | }, 31 | }, 32 | { 33 | label: 'Deutsch', 34 | lang: 'de', 35 | parameters: { 36 | tag: 'de', 37 | }, 38 | }, 39 | // { 40 | // label: '日本語', 41 | // lang: 'ja', 42 | // parameters: { 43 | // tag: 'ja', 44 | // }, 45 | // }, 46 | // { 47 | // label: 'Italiano', 48 | // lang: 'it', 49 | // parameters: { 50 | // tag: 'it', 51 | // }, 52 | // }, 53 | // { 54 | // label: 'Bahasa Indonesia', 55 | // lang: 'id', 56 | // parameters: { 57 | // tag: 'id', 58 | // }, 59 | // }, 60 | // { 61 | // label: '简体中文', 62 | // lang: 'zh-cn', 63 | // parameters: { 64 | // tag: 'zh-CN', 65 | // }, 66 | // }, 67 | // { 68 | // label: 'Português do Brasil', 69 | // lang: 'pt-br', 70 | // parameters: { 71 | // tag: 'pt-BR', 72 | // }, 73 | // }, 74 | // { 75 | // label: 'Português', 76 | // lang: 'pt-pt', 77 | // parameters: { 78 | // tag: 'pt-PT', 79 | // }, 80 | // }, 81 | // { 82 | // label: '한국어', 83 | // lang: 'ko', 84 | // parameters: { 85 | // tag: 'ko', 86 | // }, 87 | // }, 88 | // { 89 | // label: 'Türkçe', 90 | // lang: 'tr', 91 | // parameters: { 92 | // tag: 'tr', 93 | // }, 94 | // }, 95 | // { 96 | // label: 'Русский', 97 | // lang: 'ru', 98 | // parameters: { 99 | // tag: 'ru', 100 | // }, 101 | // }, 102 | // { 103 | // label: 'हिंदी', 104 | // lang: 'hi', 105 | // parameters: { 106 | // tag: 'hi', 107 | // }, 108 | // }, 109 | // { 110 | // label: 'Dansk', 111 | // lang: 'da', 112 | // parameters: { 113 | // tag: 'da', 114 | // }, 115 | // }, 116 | // { 117 | // label: 'Українська', 118 | // lang: 'uk', 119 | // parameters: { 120 | // tag: 'uk', 121 | // }, 122 | // }, 123 | ], 124 | files: [ 125 | { 126 | include: ['src/content/docs/docs/**/*.(md|mdx)'], 127 | pattern: { 128 | source: 'src/content/docs/docs/@path', 129 | locales: 'src/content/docs/docs/@lang/@path', 130 | }, 131 | type: 'universal', 132 | }, 133 | { 134 | include: ['src/content/i18n/*.json'], 135 | pattern: 'src/content/i18n/@lang.json', 136 | type: 'dictionary', 137 | }, 138 | ], 139 | tracking: { 140 | localizableProperty: 'i18nReady', 141 | ignoredKeywords: [ 142 | 'lunaria-ignore', 143 | 'typo', 144 | 'en-only', 145 | 'broken link', 146 | 'i18nReady', 147 | 'i18nIgnore', 148 | ], 149 | }, 150 | }); 151 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "pnpm dev", 8 | "check": "astro check", 9 | "build": "pnpm make-changelog && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "lunaria:build": "tsm ./scripts/lunaria.mts", 13 | "make-changelog": "tsm --require=../scripts/filter-warnings.cjs ./scripts/changelog.ts" 14 | }, 15 | "dependencies": { 16 | "@11ty/eleventy-fetch": "^5.0.1", 17 | "@astrojs/check": "catalog:", 18 | "@astrojs/starlight": "0.30.0", 19 | "@expressive-code/plugin-line-numbers": "^0.38.3", 20 | "@fontsource-variable/fira-code": "^5.1.0", 21 | "@fontsource-variable/onest": "catalog:", 22 | "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@4c8b9b0", 23 | "@shikijs/colorized-brackets": "^1.24.0", 24 | "@studiocms/ui": "workspace:*", 25 | "@types/hast": "^3.0.4", 26 | "@types/html-escaper": "^3.0.2", 27 | "@types/md5": "^2.3.5", 28 | "@types/mdast": "^4.0.4", 29 | "astro": "catalog:", 30 | "astro-embed": "^0.9.0", 31 | "expressive-code-twoslash": "^0.3.1", 32 | "hast": "1.0.0", 33 | "hast-util-select": "^6.0.3", 34 | "hast-util-to-string": "^3.0.1", 35 | "hastscript": "^9.0.0", 36 | "html-escaper": "^3.0.3", 37 | "md5": "2.3.0", 38 | "mdast": "^3.0.0", 39 | "mdast-util-from-markdown": "^2.0.2", 40 | "mdast-util-gfm": "^3.0.0", 41 | "mdast-util-to-hast": "^13.2.0", 42 | "mdast-util-to-markdown": "^2.1.2", 43 | "mdast-util-to-string": "^4.0.0", 44 | "p-retry": "^6.2.1", 45 | "rehype": "^13.0.2", 46 | "rehype-autolink-headings": "^7.1.0", 47 | "rehype-external-links": "^3.0.0", 48 | "rehype-slug": "^6.0.0", 49 | "sharp": "^0.33.5", 50 | "starlight-package-managers": "^0.8.1", 51 | "tsm": "^2.3.0", 52 | "typescript": "catalog:", 53 | "unist-util-visit": "^5.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/public/.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://studiocms.dev/funding.json 2 | -------------------------------------------------------------------------------- /docs/public/favicon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/favicon-dark.png -------------------------------------------------------------------------------- /docs/public/favicon-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/favicon-light.png -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <style> 3 | .studiocms-logo { 4 | fill: black; 5 | } 6 | @media (prefers-color-scheme: dark) { 7 | .studiocms-logo { 8 | fill: white; 9 | } 10 | } 11 | </style> 12 | <rect class="studiocms-logo" x="295" width="460" height="466" rx="32" fill="currentColor"/> 13 | <path class="studiocms-logo" d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="currentColor"/> 14 | <path class="studiocms-logo" d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="currentColor"/> 15 | </svg> -------------------------------------------------------------------------------- /docs/public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/og.png -------------------------------------------------------------------------------- /docs/public/socialproof/adam-matthiesen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/socialproof/adam-matthiesen.jpg -------------------------------------------------------------------------------- /docs/public/socialproof/louis-escher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/socialproof/louis-escher.jpg -------------------------------------------------------------------------------- /docs/public/socialproof/matthew-justice-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/socialproof/matthew-justice-image.png -------------------------------------------------------------------------------- /docs/public/socialproof/matthew-justice.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/socialproof/matthew-justice.jpg -------------------------------------------------------------------------------- /docs/public/socialproof/the-otterlord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/public/socialproof/the-otterlord.png -------------------------------------------------------------------------------- /docs/scripts/changelog.ts: -------------------------------------------------------------------------------- 1 | import type { List, Root } from 'mdast'; 2 | import { toMarkdown } from 'mdast-util-to-markdown'; 3 | import { loadChangelog, semverCategories } from './lib/changelogs'; 4 | import { writeFileLines } from './lib/utils'; 5 | 6 | const changelog = loadChangelog('../packages/studiocms_ui/CHANGELOG.md'); 7 | 8 | // Generate markdown output 9 | const output: string[] = []; 10 | 11 | output.push( 12 | // Add release notes frontmatter to output 13 | '---', 14 | '# Warning: This file is generated automatically. Do not edit!', 15 | 'title: Release Notes', 16 | 'description: Release notes for the @studiocms/ui package.', 17 | 'editUrl: false', 18 | '---', 19 | '', 20 | 'This document contains release notes for the `@studiocms/ui` package.', 21 | 'For more information, see the [CHANGELOG file](https://github.com/withstudiocms/ui/blob/main/packages/studiocms_ui/CHANGELOG.md)', 22 | '' 23 | ); 24 | 25 | const ast: Root = { 26 | type: 'root', 27 | children: [], 28 | }; 29 | 30 | for (const version of changelog.versions) { 31 | const versionChanges: List = { type: 'list', children: [] }; 32 | 33 | for (const semverCategory of semverCategories) { 34 | for (const listItem of version.changes[semverCategory].children) { 35 | versionChanges.children.push(listItem); 36 | } 37 | } 38 | 39 | if (version.includes.size) { 40 | versionChanges.children.push({ 41 | type: 'listItem', 42 | children: [ 43 | { 44 | type: 'paragraph', 45 | children: [{ type: 'text', value: `Includes: ${[...version.includes].join(', ')} ` }], 46 | }, 47 | ], 48 | }); 49 | } 50 | 51 | if (!versionChanges.children.length) continue; 52 | 53 | ast.children.push({ 54 | type: 'heading', 55 | depth: 2, 56 | children: [{ type: 'text', value: version.version }], 57 | }); 58 | 59 | ast.children.push(versionChanges); 60 | } 61 | 62 | output.push(toMarkdown(ast, { bullet: '-' })); 63 | 64 | // Write output to file 65 | writeFileLines('./src/content/docs/docs/changelog.md', output); 66 | -------------------------------------------------------------------------------- /docs/scripts/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { EOL } from 'node:os'; 3 | 4 | export function normalizeLineEndings(contents: string) { 5 | return contents.replace(/\r\n/g, '\n'); 6 | } 7 | 8 | export function splitLines(contents: string) { 9 | return normalizeLineEndings(contents).split(/\n/); 10 | } 11 | 12 | export function readFileLines(filePath: string) { 13 | return splitLines(fs.readFileSync(filePath, 'utf8')); 14 | } 15 | 16 | export function writeFileLines(filePath: string, lines: string | string[]) { 17 | const normalizedLines = splitLines(Array.isArray(lines) ? lines.join('\n') : lines); 18 | fs.writeFileSync(filePath, normalizedLines.join(EOL)); 19 | } 20 | 21 | /** 22 | * String-only alternative to `JSON.stringify` that uses single quotes instead of double quotes. 23 | */ 24 | export function serializeStringWithSingleQuotes(input: string): string { 25 | const escapedString: string = input.replace(/(['\\\n\r\t])/g, (match) => { 26 | const escapeMap: { [char: string]: string } = { 27 | "'": "\\'", 28 | '\\': '\\\\', 29 | '\n': '\\n', 30 | '\r': '\\r', 31 | '\t': '\\t', 32 | }; 33 | 34 | return escapeMap[match] || match; 35 | }); 36 | 37 | return `'${escapedString}'`; 38 | } 39 | -------------------------------------------------------------------------------- /docs/scripts/lunaria.mts: -------------------------------------------------------------------------------- 1 | import { mkdirSync, writeFileSync } from 'node:fs'; 2 | import { createLunaria } from '@lunariajs/core'; 3 | import { Page } from '../lunaria/components.ts'; 4 | 5 | const lunaria = await createLunaria(); 6 | const status = await lunaria.getFullStatus(); 7 | 8 | const html = Page(lunaria.config, status, lunaria); 9 | 10 | mkdirSync('dist/lunaria', { recursive: true }); 11 | writeFileSync('dist/lunaria/index.html', html); 12 | -------------------------------------------------------------------------------- /docs/src/assets/astro.svg: -------------------------------------------------------------------------------- 1 | <svg width="85" height="107" viewBox="0 0 85 107" fill="currentColor" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 2 | <path d="M27.5893 91.1365C22.7555 86.7178 21.3443 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4374 78.2199 52.0358 77.7822 59.2231 74.2459C60.0453 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.3179 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6844 102.089C39.2086 99.8193 38.5065 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5893 91.1365Z"/> 3 | <path d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z"/> 4 | </svg> 5 | -------------------------------------------------------------------------------- /docs/src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withstudiocms/ui/7210cea634a2ea1966c2ef0e336c94c76f2f1e4d/docs/src/assets/avatar.png -------------------------------------------------------------------------------- /docs/src/assets/bsky.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="256" height="226" preserveAspectRatio="xMidYMid" viewBox="0 0 256 226"><path fill="currentColor" d="M55.491 15.172c29.35 22.035 60.917 66.712 72.509 90.686 11.592-23.974 43.159-68.651 72.509-90.686C221.686-.727 256-13.028 256 26.116c0 7.818-4.482 65.674-7.111 75.068-9.138 32.654-42.436 40.983-72.057 35.942 51.775 8.812 64.946 38 36.501 67.187-54.021 55.433-77.644-13.908-83.696-31.676-1.11-3.257-1.63-4.78-1.637-3.485-.008-1.296-.527.228-1.637 3.485-6.052 17.768-29.675 87.11-83.696 31.676-28.445-29.187-15.274-58.375 36.5-67.187-29.62 5.041-62.918-3.288-72.056-35.942C4.482 91.79 0 33.934 0 26.116 0-13.028 34.314-.727 55.491 15.172Z"/></svg> 2 | -------------------------------------------------------------------------------- /docs/src/assets/discord.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 2 | <path fill="currentColor" d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" /> 3 | </svg> 4 | -------------------------------------------------------------------------------- /docs/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 2 | <path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /> 3 | </svg> 4 | -------------------------------------------------------------------------------- /docs/src/assets/logo-adaptive.svg: -------------------------------------------------------------------------------- 1 | <svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 2 | <style> 3 | .studiocms-logo { 4 | fill: black; 5 | } 6 | @media (prefers-color-scheme: dark) { 7 | .studiocms-logo { 8 | fill: white; 9 | } 10 | } 11 | </style> 12 | <rect class="studiocms-logo" x="295" width="460" height="466" rx="32" fill="currentColor"/> 13 | <path class="studiocms-logo" d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="currentColor"/> 14 | <path class="studiocms-logo" d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="currentColor"/> 15 | </svg> -------------------------------------------------------------------------------- /docs/src/assets/patreon.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 40" class="logomark"><path fill="currentColor" d="M35.975 11.392c0 .312.021.627-.004.937-.03.361-.082.722-.149 1.08a22.535 22.535 0 0 1-.331 1.512 8.59 8.59 0 0 1-.359 1.084c-.313.767-.66 1.518-1.117 2.21-.282.427-.57.849-.864 1.266a4.12 4.12 0 0 1-.37.431c-.225.238-.442.483-.686.695a13.5 13.5 0 0 1-1.123.905c-.356.25-.756.431-1.12.674-.404.268-.866.384-1.296.587-.384.18-.795.24-1.186.38-.498.18-1.021.222-1.531.331-.544.117-1.097.203-1.643.315-.449.09-.894.198-1.34.298-.254.056-.51.098-.756.173-.304.093-.6.214-.896.324-.201.072-.412.126-.604.219-.28.14-.544.315-.823.464-.457.242-.838.585-1.184.96-.292.32-.546.681-.8 1.036-.418.587-.706 1.244-.964 1.916-.254.657-.487 1.322-.725 1.986-.221.625-.43 1.252-.655 1.875-.197.543-.407 1.079-.618 1.615a13.447 13.447 0 0 1-1.12 2.215c-.331.531-.685 1.049-1.142 1.478-.366.343-.72.704-1.17.944-.446.24-.906.448-1.4.557a6.636 6.636 0 0 1-1.807.129c-.229-.012-.455-.075-.684-.117-.137-.026-.276-.047-.409-.089-.112-.035-.215-.097-.322-.151-.302-.147-.624-.264-.9-.448a8.802 8.802 0 0 1-.96-.776c-.554-.492-.97-1.103-1.342-1.74a13.04 13.04 0 0 1-.681-1.319c-.192-.436-.336-.893-.492-1.345a24.916 24.916 0 0 1-.34-1.063c-.092-.317-.165-.641-.243-.963-.073-.298-.15-.594-.212-.895-.112-.536-.215-1.073-.32-1.609a35.827 35.827 0 0 1-.133-.68c-.06-.34-.114-.681-.171-1.022-.044-.254-.092-.506-.13-.76-.044-.28-.08-.56-.124-.839-.036-.242-.078-.483-.112-.725-.032-.226-.06-.452-.089-.678a70.143 70.143 0 0 1-.094-.73c-.03-.236-.055-.471-.082-.707a19421 19421 0 0 1-.096-.818c-.011-.098-.023-.193-.03-.291-.034-.401-.068-.804-.1-1.208-.02-.25-.04-.501-.05-.75-.021-.39-.042-.777-.05-1.166C.01 18.46.001 17.819 0 17.18c0-.378.002-.755.027-1.13.026-.392.08-.784.122-1.176.034-.312.064-.622.105-.934.023-.175.064-.348.1-.52.092-.432.176-.863.281-1.292.076-.31.181-.61.266-.916.157-.571.393-1.11.623-1.653.106-.25.236-.49.368-.725.186-.329.366-.66.576-.97.259-.378.533-.744.823-1.098.275-.336.567-.66.873-.965.24-.24.512-.448.77-.665.254-.212.501-.433.77-.624.412-.296.835-.576 1.263-.849.249-.158.514-.294.774-.434.405-.219.81-.44 1.22-.648.26-.13.527-.244.794-.354.683-.277 1.364-.557 2.055-.816.46-.17.932-.303 1.399-.452.24-.077.475-.161.717-.229.2-.056.402-.086.604-.133.22-.05.434-.119.656-.16.299-.059.603-.1.907-.147.34-.052.679-.105 1.02-.152.139-.019.283-.02.425-.03.47-.026.944-.054 1.414-.077.188-.01.382-.051.565-.019.443.08.889.017 1.332.05.428.03.853.076 1.278.127.306.038.608.103.914.15.268.04.535.065.802.107.215.035.43.081.645.128.46.103.919.196 1.374.317.404.11.797.275 1.204.37.469.113.899.332 1.351.479.462.149.86.424 1.3.608.515.217.96.546 1.418.858.347.238.685.492 1 .77.467.41.92.836 1.356 1.28.258.26.478.564.713.85.38.464.658.993.928 1.523.215.424.393.874.537 1.329.12.373.156.774.245 1.156.098.42.096.844.073 1.27l-.012.008Z"></path></svg> 2 | -------------------------------------------------------------------------------- /docs/src/assets/swords.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-swords"><polyline points="14.5 17.5 3 6 3 3 6 3 17.5 14.5"/><line x1="13" x2="19" y1="19" y2="13"/><line x1="16" x2="20" y1="16" y2="20"/><line x1="19" x2="21" y1="21" y2="19"/><polyline points="14.5 6.5 18 3 21 3 21 6 17.5 9.5"/><line x1="5" x2="9" y1="14" y2="18"/><line x1="7" x2="4" y1="17" y2="20"/><line x1="3" x2="5" y1="19" y2="21"/></svg> 2 | -------------------------------------------------------------------------------- /docs/src/assets/twitter.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1227" fill="none" viewBox="0 0 1200 1227"><path fill="currentColor" d="M714.163 519.284 1160.89 0h-105.86L667.137 450.887 357.328 0H0l468.492 681.821L0 1226.37h105.866l409.625-476.152 327.181 476.152H1200L714.137 519.284h.026ZM569.165 687.828l-47.468-67.894-377.686-540.24h162.604l304.797 435.991 47.468 67.894 396.2 566.721H892.476L569.165 687.854v-.026Z"/></svg> 2 | -------------------------------------------------------------------------------- /docs/src/components/ContributorList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getContributorBreakdown } from '../util/getContributors'; 3 | import FacePile from './FacePile.astro'; 4 | 5 | const breakdown = await getContributorBreakdown(Astro); 6 | --- 7 | 8 | <div class="not-content"> 9 | <div class="split-flex"> 10 | 11 | { 12 | breakdown.map(({ name, contributors }) => { 13 | if (contributors.length === 0) return null; 14 | return ( 15 | <div> 16 | <h5>{name}</h5> 17 | <FacePile contributors={contributors} /> 18 | </div> 19 | ) 20 | }) 21 | } 22 | 23 | </div> 24 | </div> 25 | 26 | <style> 27 | .split-flex { 28 | display: grid; 29 | flex-direction: row; 30 | align-items: center; 31 | gap: 2rem; 32 | width: 100%; 33 | margin-left: auto; 34 | margin-right: auto; 35 | margin-bottom: 1.5rem; 36 | } 37 | 38 | .not-content h5 { 39 | font-size: large; 40 | width: 100%; 41 | } 42 | 43 | .not-content { 44 | width: 100%; 45 | margin-left: auto; 46 | margin-right: auto; 47 | padding: 0.5rem; 48 | } 49 | 50 | 51 | @media (min-width: 50rem) { 52 | .split-flex { 53 | grid-template-columns: 1fr 1fr; 54 | } 55 | } 56 | </style> -------------------------------------------------------------------------------- /docs/src/components/DropdownScript.astro: -------------------------------------------------------------------------------- 1 | <script> 2 | import { DropdownHelper } from 'studiocms:ui/components'; 3 | 4 | new DropdownHelper('usage-dropdown'); 5 | new DropdownHelper('align-start-dropdown'); 6 | new DropdownHelper('align-center-dropdown'); 7 | new DropdownHelper('align-end-dropdown'); 8 | new DropdownHelper('right-click-dropdown'); 9 | new DropdownHelper('options-dropdown'); 10 | new DropdownHelper('icons-dropdown'); 11 | </script> 12 | -------------------------------------------------------------------------------- /docs/src/components/FacePile.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | import type { Contributor } from '~/util/getContributors'; 4 | 5 | export interface Props { 6 | contributors: Contributor[]; 7 | } 8 | 9 | const { contributors } = Astro.props; 10 | --- 11 | 12 | <!-- 13 | Thanks to Astro Docs for the FacePile component! https://docs.astro.build 14 | Thanks to @5t3ph for https://smolcss.dev/#smol-avatar-list! 15 | --> 16 | <div> 17 | <ul class="avatar-list"> 18 | { 19 | contributors.map((item) => ( 20 | <li> 21 | <a href={`https://github.com/${item.login}`}> 22 | <Image 23 | alt={item.login} 24 | title={item.login} 25 | width={64} 26 | height={64} 27 | src={`https://avatars.githubusercontent.com/u/${item.id}?s=64`} 28 | loading="eager" 29 | /> 30 | </a> 31 | </li> 32 | )) 33 | } 34 | </ul> 35 | </div> 36 | 37 | <style> 38 | .avatar-list { 39 | --avatar-size: 3rem; 40 | --avatar-overlap: -0.125em; 41 | --avatar-row-spacing: 0.125em; 42 | --avatar-outline-width: 1px; 43 | --avatar-outline-offset: 0.08em; 44 | 45 | display: flex; 46 | flex-wrap: wrap; 47 | list-style: none; 48 | padding: var(--avatar-border); 49 | font-size: var(--avatar-size); 50 | align-items: center; 51 | } 52 | 53 | .avatar-list li { 54 | --avatar-row-margin: calc( 55 | var(--avatar-outline-offset) + var(--avatar-outline-width) + var(--avatar-row-spacing) / 2 56 | ); 57 | margin: var(--avatar-row-margin) var(--avatar-overlap) var(--avatar-row-margin) 0; 58 | align-items: center; 59 | } 60 | 61 | .avatar-list img, 62 | .avatar-list a { 63 | display: block; 64 | border-radius: 50%; 65 | width: var(--avatar-size); 66 | height: var(--avatar-size); 67 | /* Hide alt/title if Avatar image fails to load. */ 68 | text-decoration: none; 69 | color: transparent; 70 | } 71 | 72 | .avatar-list img { 73 | width: 100%; 74 | height: 100%; 75 | object-fit: cover; 76 | background-color: var(--theme-bg); 77 | box-shadow: 78 | 0 0 0 var(--avatar-outline-width) var(--theme-divider), 79 | 0 0 0 var(--avatar-outline-offset) var(--theme-bg), 80 | 0 0 0 calc(var(--avatar-outline-offset) + var(--avatar-outline-width)) var(--theme-divider), 81 | 0 0 calc(var(--theme-glow-blur) + var(--avatar-outline-offset)) var(--theme-glow-diffuse); 82 | /* Indicates the contributor image boundaries for forced colors users, transparent is changed to a solid color */ 83 | outline: 1px solid transparent; 84 | } 85 | 86 | .avatar-list a:focus { 87 | outline: 2px solid var(--theme-accent); 88 | outline-offset: var(--avatar-outline-offset); 89 | } 90 | </style> -------------------------------------------------------------------------------- /docs/src/components/Gallery.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | 4 | export interface Props { 5 | galleryImages: Array<{ 6 | imageMetadata: ImageMetadata; 7 | alt: string; 8 | }>; 9 | } 10 | 11 | const { galleryImages } = Astro.props; 12 | --- 13 | <div class="grid not-content"> 14 | {galleryImages.map((image) => ( 15 | <starlight-image-zoom-zoomable> 16 | <Image 17 | src={image.imageMetadata} 18 | alt={image.alt} 19 | width="1280" 20 | height="1900" 21 | class="image" 22 | /> 23 | </starlight-image-zoom-zoomable> 24 | ))} 25 | </div> 26 | 27 | <style> 28 | .grid { 29 | display: grid; 30 | grid-template-columns: repeat(2, auto); /* only 3 here */ 31 | /* grid-auto-rows: 100px; */ 32 | grid-auto-flow: dense; /* don't forget this */ 33 | gap: 1rem; 34 | justify-content: center; 35 | } 36 | 37 | .image { 38 | display: block; 39 | width: 100%; 40 | height: auto; 41 | cursor: pointer; 42 | } 43 | </style> 44 | <style is:global> 45 | .starlight-image-zoom-control { 46 | transition: all .15s ease; 47 | cursor: pointer; 48 | } 49 | 50 | .starlight-image-zoom-control:hover { 51 | background-color: var(--sl-color-gray-5); 52 | } 53 | </style> 54 | -------------------------------------------------------------------------------- /docs/src/components/ModalScript.astro: -------------------------------------------------------------------------------- 1 | <script> 2 | import { ModalHelper } from 'studiocms:ui/components'; 3 | 4 | new ModalHelper('usage-modal', 'usage-modal-trigger'); 5 | new ModalHelper('sizes-modal-sm', 'sm-modal-trigger'); 6 | new ModalHelper('sizes-modal-md', 'md-modal-trigger'); 7 | new ModalHelper('sizes-modal-lg', 'lg-modal-trigger'); 8 | new ModalHelper('dismissable-modal', 'dismissable-modal-trigger'); 9 | new ModalHelper('buttons-modal', 'buttons-modal-trigger'); 10 | </script> 11 | -------------------------------------------------------------------------------- /docs/src/components/Newsletter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Button, Checkbox, Icon, Input } from 'studiocms:ui/components'; 3 | --- 4 | <form method="post" action="https://newsletter.studiocms.dev/subscription/form" id="newsletter-form"> 5 | <div class="newsletter-container"> 6 | <span>Subscribe to our newsletter</span> 7 | <input type="hidden" name="nonce" /> 8 | <input id="06711" type="checkbox" name="l" hidden checked value="06711dc1-78ff-4993-9d79-df44ba3572fa" /> 9 | 10 | <div class="newsletter-inputs"> 11 | <Input type="email" name="email" isRequired placeholder='E-mail' aria-label='E-Mail' class='newsletter-email-input' /> 12 | <Button type="submit" color='primary' style="width: fit-content;"> 13 | Subscribe 14 | </Button> 15 | </div> 16 | </div> 17 | </form> 18 | 19 | <script> 20 | import { toast } from "studiocms:ui/components"; 21 | 22 | const form = document.getElementById('newsletter-form') as HTMLFormElement; 23 | 24 | form.addEventListener('submit', async (event) => { 25 | event.preventDefault(); 26 | 27 | const formData = new FormData(form); 28 | 29 | const response = await fetch(form.action, { 30 | method: form.method, 31 | body: formData, 32 | }); 33 | 34 | if (response.ok) { 35 | form.reset(); 36 | toast({ 37 | title: 'Success', 38 | type: 'success', 39 | description: 'You have successfully subscribed to the StudioCMS Newsletter.', 40 | }) 41 | } else { 42 | toast({ 43 | title: 'Error', 44 | type: 'danger', 45 | description: 'An error occurred while subscribing to the StudioCMS Newsletter.', 46 | }) 47 | } 48 | }); 49 | </script> 50 | 51 | <style> 52 | .newsletter-container { 53 | display: flex; 54 | flex-direction: column; 55 | justify-content: center; 56 | gap: .5rem; 57 | 58 | text-wrap: wrap; 59 | } 60 | 61 | .newsletter-inputs { 62 | display: flex; 63 | flex-direction: row; 64 | gap: .5rem; 65 | align-items: center; 66 | height: fit-content; 67 | max-width: 400px; 68 | } 69 | 70 | .newsletter-button { 71 | display: flex; 72 | flex-direction: row; 73 | justify-content: end; 74 | align-items: center; 75 | gap: .5rem; 76 | } 77 | 78 | .newsletter-email-input { 79 | height: 40px; 80 | } 81 | </style> -------------------------------------------------------------------------------- /docs/src/components/PackageCatalog.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import type { CollectionEntry } from 'astro:content'; 4 | import Integration from './Integration.astro'; 5 | import ReadMore from './ReadMore.astro'; 6 | 7 | interface Props { 8 | catalog: CollectionEntry<'package-catalog'>['data']['catalog']; 9 | } 10 | 11 | const { catalog } = Astro.props; 12 | 13 | const packages = (await getCollection('package-catalog')) 14 | .filter(({ data }) => data.catalog === catalog) 15 | .sort(({ data: { name: a } }, { data: { name: b } }) => { 16 | const isScopedA = a.startsWith('@'); 17 | const isScopedB = b.startsWith('@'); 18 | 19 | // Non-scoped packages come first 20 | if (!isScopedA && isScopedB) return -1; 21 | if (isScopedA && !isScopedB) return 1; 22 | 23 | // If both are scoped or non-scoped, sort alphabetically 24 | return a.localeCompare(b); 25 | }) 26 | .map(({ data }) => data); 27 | --- 28 | 29 | <div> 30 | 31 | { 32 | packages.map(({ 33 | name: title, 34 | githubURL, 35 | released, 36 | isPlugin, 37 | description, 38 | docsLink: href, 39 | publiclyUsable 40 | }, index) => ( 41 | <Integration {title} {githubURL} {released} {isPlugin} {publiclyUsable} /> 42 | 43 | <p>{description}</p> 44 | 45 | <ReadMore>{Astro.locals.t('package-catalog.readmore.start')} <a {href}> {Astro.locals.t('package-catalog.readmore.end')}</a></ReadMore> 46 | 47 | <>{ index < packages.length - 1 && <hr /> }</> 48 | )) 49 | } 50 | 51 | </div> -------------------------------------------------------------------------------- /docs/src/components/PreviewCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | vertical?: boolean; 4 | gapSize?: 'sm' | 'md' | 'lg'; 5 | fullWidth?: boolean; 6 | fullHeight?: boolean; 7 | }; 8 | 9 | const { vertical = false, gapSize = 'md', fullWidth, fullHeight } = Astro.props; 10 | --- 11 | <div 12 | class="preview-card not-content" 13 | class:list={[vertical && "vertical", gapSize]} 14 | > 15 | <slot /> 16 | </div> 17 | <style> 18 | .preview-card { 19 | border: 1px solid var(--sl-color-gray-5); 20 | border-radius: 8px; 21 | padding: 4rem 1rem; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | gap: 1rem; 26 | flex-wrap: wrap; 27 | } 28 | 29 | .preview-card.vertical { 30 | flex-direction: column; 31 | } 32 | 33 | .preview-card.sm { 34 | gap: .5rem; 35 | } 36 | 37 | .preview-card.lg { 38 | gap: 2rem; 39 | } 40 | </style> 41 | -------------------------------------------------------------------------------- /docs/src/components/ProgressScript.astro: -------------------------------------------------------------------------------- 1 | <script> 2 | import { ProgressHelper } from 'studiocms:ui/components'; 3 | 4 | const progress = new ProgressHelper('prog-2'); 5 | 6 | async function changeProgress() { 7 | for (let i = 10; i <= 100; i += 10) { 8 | progress.setValue(i); 9 | await new Promise((resolve) => setTimeout(resolve, 1000)); 10 | } 11 | 12 | progress.setValue(0); 13 | await new Promise((resolve) => setTimeout(resolve, 1000)); 14 | changeProgress(); 15 | } 16 | 17 | changeProgress(); 18 | </script> 19 | -------------------------------------------------------------------------------- /docs/src/components/ReadMore.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from '@astrojs/starlight/components'; 3 | --- 4 | 5 | <div class="read-more"> 6 | <Icon class="icon" name="open-book" /> 7 | <span><slot /></span> 8 | </div> 9 | 10 | <style> 11 | .read-more { 12 | display: flex; 13 | gap: 0.5rem; 14 | align-items: flex-start; 15 | } 16 | .icon { 17 | --icon-size: 1.5rem; 18 | font-size: var(--icon-size); 19 | flex-shrink: 0; 20 | /* Align to the middle of the first line of text. */ 21 | margin-block: calc((var(--sl-line-height) * 1rem - var(--icon-size)) / 2); 22 | color: var(--sl-color-text-accent); 23 | } 24 | </style> -------------------------------------------------------------------------------- /docs/src/components/ThemeHelperScript.astro: -------------------------------------------------------------------------------- 1 | <script> 2 | import { ThemeHelper } from 'studiocms:ui/utils'; 3 | 4 | const themeHelper = new ThemeHelper(); 5 | 6 | const outputSpan = document.querySelector<HTMLSpanElement>('#theme-listener-output')!; 7 | 8 | themeHelper.onThemeChange((newTheme, oldTheme) => { 9 | outputSpan.textContent = `Theme is now: ${newTheme}! (Before: ${oldTheme})`; 10 | }); 11 | </script> 12 | -------------------------------------------------------------------------------- /docs/src/components/ToasterScript.astro: -------------------------------------------------------------------------------- 1 | <script> 2 | import { toast } from 'studiocms:ui/components'; 3 | 4 | document.getElementById('usage-trigger')!.addEventListener('click', () => { 5 | toast({ 6 | title: "This is a toast!", 7 | description: "Hi there!", 8 | type: 'info', 9 | }); 10 | }); 11 | </script> 12 | -------------------------------------------------------------------------------- /docs/src/components/TursoCLI.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Code } from '@astrojs/starlight/components'; 3 | 4 | const tursoCliCommands = ['db', 'auth'] as const; 5 | 6 | const tursoAuthCommands = [ 7 | 'login', 8 | 'logout', 9 | 'signup', 10 | 'token', 11 | 'whoami', 12 | 'api-tokens.mint', 13 | 'api-tokens.list', 14 | 'api-tokens.revoke', 15 | ] as const; 16 | 17 | const tursoDBCommands = [ 18 | 'list', 19 | 'create', 20 | 'show', 21 | 'destroy', 22 | 'inspect', 23 | 'shell', 24 | 'locations', 25 | 'tokens.create', 26 | 'tokens.invalidate', 27 | 'config.attach.allow', 28 | 'config.attach.disallow', 29 | 'config.attach.show', 30 | ] as const; 31 | 32 | const allCommands = [...tursoAuthCommands, ...tursoDBCommands] as const; 33 | 34 | type TursoCLICommands = (typeof tursoCliCommands)[number]; 35 | 36 | type AuthCommands = (typeof tursoAuthCommands)[number]; 37 | 38 | type DBCommands = (typeof tursoDBCommands)[number]; 39 | 40 | type AllCommands = (typeof allCommands)[number]; 41 | 42 | type Commands = { 43 | auth: Record<AuthCommands, string>; 44 | db: Record<DBCommands, string>; 45 | }; 46 | 47 | const tursoCLICommands: Commands = { 48 | auth: { 49 | login: 'turso auth login', 50 | logout: 'turso auth logout', 51 | signup: 'turso auth signup', 52 | token: 'turso auth token', 53 | whoami: 'turso auth whoami', 54 | 'api-tokens.mint': 'turso auth api-tokens mint', 55 | 'api-tokens.list': 'turso auth api-tokens list', 56 | 'api-tokens.revoke': 'turso auth api-tokens revoke', 57 | }, 58 | db: { 59 | list: 'turso db list', 60 | create: 'turso db create', 61 | show: 'turso db show', 62 | destroy: 'turso db destroy', 63 | inspect: 'turso db inspect', 64 | shell: 'turso db shell', 65 | locations: 'turso db locations', 66 | 'tokens.create': 'turso db tokens create', 67 | 'tokens.invalidate': 'turso db tokens invalidate', 68 | 'config.attach.allow': 'turso db config attach allow', 69 | 'config.attach.disallow': 'turso db config attach disallow', 70 | 'config.attach.show': 'turso db config attach show', 71 | }, 72 | }; 73 | 74 | const commandBuilder = (tursoCli: TursoCLICommands, type: AllCommands, arg: string) => { 75 | let command: string; 76 | 77 | switch (tursoCli) { 78 | case 'auth': 79 | command = tursoCLICommands.auth[type as AuthCommands]; 80 | break; 81 | case 'db': 82 | command = tursoCLICommands.db[type as DBCommands]; 83 | break; 84 | default: 85 | throw new Error(`Invalid Turso CLI command: ${tursoCli} ${type}`); 86 | } 87 | 88 | return `${command} ${arg}`; 89 | }; 90 | 91 | interface Props { 92 | tursoCli: TursoCLICommands; 93 | type: AllCommands; 94 | arg: string; 95 | } 96 | 97 | const { tursoCli, type, arg } = Astro.props; 98 | 99 | const code = commandBuilder(tursoCli, type, arg); 100 | --- 101 | <Code {code} lang="sh" /> -------------------------------------------------------------------------------- /docs/src/components/Version.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { cachedFetch } from '../util-server'; 3 | 4 | export interface Props { 5 | pkgName: string; 6 | } 7 | 8 | const { pkgName } = Astro.props as Props; 9 | 10 | const url = `https://registry.npmjs.org/${pkgName}/latest`; 11 | 12 | const response = await cachedFetch(url); 13 | const json = await response.json(); 14 | 15 | if (!response.ok) { 16 | throw new Error( 17 | `npm API call failed: GET "${url}" returned status ${response.status}: ${JSON.stringify(json)}` 18 | ); 19 | } 20 | --- 21 | 22 | <code>v{json.version}</code> 23 | 24 | <style> 25 | code { 26 | background-color: var(--sl-custom-code-color); 27 | border-radius: var(--sl-border-radius-medium); 28 | color: var(--sl-color-gray-800); 29 | font-size: var(--sl-font-size-small); 30 | padding: 0.125rem 0.375rem; 31 | margin-block: -0.125rem; 32 | } 33 | </style> -------------------------------------------------------------------------------- /docs/src/components/fluid-grid.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | minColumnWidth?: string; 4 | } 5 | const { minColumnWidth } = Astro.props; 6 | --- 7 | 8 | <ul class="fluid-grid"><slot /></ul> 9 | 10 | <style define:vars={{ minColumnWidth }}> 11 | .fluid-grid { 12 | display: grid; 13 | grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--minColumnWidth, 11rem)), 1fr)); 14 | gap: 1rem; 15 | list-style: none; 16 | padding: 0; 17 | } 18 | .fluid-grid > :global(*) { 19 | margin-top: 0 !important; 20 | } 21 | </style> -------------------------------------------------------------------------------- /docs/src/components/icons/GitHubIcon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'svg'> { 5 | width?: number; 6 | height?: number; 7 | } 8 | 9 | const { width = 98, height = 96, ...props } = Astro.props; 10 | --- 11 | <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}> 12 | <path fill="currentColor" d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2" /> 13 | </svg> 14 | -------------------------------------------------------------------------------- /docs/src/components/landing/DropdownExample.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Button, Dropdown } from 'studiocms:ui/components'; 3 | --- 4 | 5 | <Dropdown 6 | id='dropdown-preview' 7 | options={[ 8 | { label: 'View Profile', value: 'view-profile', icon: 'user' }, 9 | { label: 'Settings', value: 'settings', icon: 'cog-6-tooth' }, 10 | { label: 'Log out', value: 'log-out', color: 'danger', icon: 'arrow-left-start-on-rectangle' }, 11 | ]} 12 | align='start' 13 | > 14 | <Button color='primary'> 15 | Trigger Dropdown 16 | </Button> 17 | </Dropdown> 18 | <script> 19 | import { DropdownHelper } from "studiocms:ui/components"; 20 | 21 | new DropdownHelper('dropdown-preview'); 22 | </script> 23 | -------------------------------------------------------------------------------- /docs/src/components/landing/FormExample.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Button, Card, Checkbox, Input } from 'studiocms:ui/components'; 3 | --- 4 | 5 | <Card as='form' class='form-card' fullWidth> 6 | <span slot='header' class='heading'>Sign up</span> 7 | <div class='form-inputs'> 8 | <Input label='Username' isRequired /> 9 | <Input label='Password' type='password' isRequired /> 10 | <Input label='Confirm Password' type='password' isRequired /> 11 | <Checkbox 12 | color='primary' 13 | label='Yes, please do sell my data!' 14 | size='sm' 15 | isRequired 16 | defaultChecked 17 | /> 18 | </div> 19 | <Button type='submit' slot='footer' color='primary' fullWidth> 20 | Sign up 21 | </Button> 22 | </Card> 23 | <!-- CODE_SPLIT --> 24 | <style is:global> 25 | .heading { 26 | font-size: 1.5em; 27 | font-weight: bold; 28 | margin-bottom: .75rem; 29 | } 30 | 31 | .form-inputs { 32 | display: flex; 33 | flex-direction: column; 34 | gap: .5rem; 35 | } 36 | 37 | .form-card { 38 | max-width: 480px !important; 39 | } 40 | </style> 41 | <script> 42 | const forms = document.querySelectorAll('form'); 43 | 44 | for (const form of forms) { 45 | form.addEventListener('submit', (e) => e.preventDefault()); 46 | } 47 | </script> 48 | -------------------------------------------------------------------------------- /docs/src/components/landing/ModalExample.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Button, Modal } from 'studiocms:ui/components'; 3 | --- 4 | 5 | <Modal 6 | id='example-modal' 7 | actionButton={{ label: 'Confirm', color: 'danger' }} 8 | > 9 | <h3 slot="header">Delete React?</h3> 10 | <p> 11 | Are you sure you want to remove React from your project? 12 | Just imagine how much harder your life could be with it! 13 | </p> 14 | </Modal> 15 | <Button color='danger' id='imaginary-delete-button'> 16 | Trigger Modal 17 | </Button> 18 | <script> 19 | import { ModalHelper } from "studiocms:ui/components"; 20 | 21 | new ModalHelper('example-modal', 'imaginary-delete-button'); 22 | </script> 23 | <!-- CODE_SPLIT --> 24 | <script> 25 | import { ModalHelper, toast } from "studiocms:ui/components"; 26 | 27 | const modal = new ModalHelper('example-modal', 'imaginary-delete-button'); 28 | 29 | modal.registerConfirmCallback(() => { 30 | toast({ 31 | type: 'success', 32 | title: 'React deleted successfully', 33 | description: 'React has successfully been deleted. Check out how to create these toasts in the <a href="/docs/components/toast/">toasts docs!</a>' 34 | }); 35 | }); 36 | </script> 37 | -------------------------------------------------------------------------------- /docs/src/components/landing/PageHeader.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { type HeroIconName, Icon } from 'studiocms:ui/components'; 3 | 4 | interface Props { 5 | icon: HeroIconName; 6 | title: string; 7 | subtitle?: string; 8 | } 9 | 10 | const { icon, title, subtitle } = Astro.props; 11 | --- 12 | 13 | <header> 14 | <Icon class="page-header-icon" name={icon} width={24} height={24} /> 15 | <div> 16 | <h2>{title}</h2> 17 | {subtitle && (<span class="subtitle">{subtitle}</span>)} 18 | </div> 19 | </header> 20 | <style> 21 | header { 22 | margin-top: 8rem; 23 | margin-bottom: 2rem; 24 | width: 65ch; 25 | max-width: 100%; 26 | } 27 | 28 | .page-header-icon { 29 | transform: scale(2); 30 | transform-origin: top left; 31 | margin-bottom: 1.5rem; 32 | } 33 | 34 | h2 { 35 | font-size: 2em; 36 | margin-bottom: 1rem; 37 | } 38 | </style> 39 | -------------------------------------------------------------------------------- /docs/src/components/landing/SocialProofCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | 4 | interface Props { 5 | avatar: string; 6 | name: string; 7 | handle: string; 8 | message: string; 9 | image?: { 10 | path: string; 11 | width: number; 12 | height: number; 13 | }; 14 | } 15 | 16 | const { avatar, name, handle, message, image } = Astro.props; 17 | --- 18 | <div class="social-proof-card" class:list={[image && "two-spaces"]}> 19 | <div class="social-proof-author"> 20 | <Image class="social-proof-author-avatar" src={avatar} alt={handle} width={40} height={40} /> 21 | <div class="social-proof-author-text"> 22 | <span class="social-proof-author-name">{name}</span> 23 | <span class="social-proof-author-handle">{handle}</span> 24 | </div> 25 | </div> 26 | <p class="social-proof-message">{message}</p> 27 | {image && ( 28 | <Image src={image.path} alt={''} width={image.width} height={image.height} class='social-proof-image' /> 29 | )} 30 | </div> 31 | <style> 32 | .social-proof-card { 33 | display: flex; 34 | flex-direction: column; 35 | width: 100%; 36 | height: fit-content; 37 | border-radius: 1rem; 38 | padding: 2rem; 39 | background-color: hsl(var(--background-step-1)); 40 | } 41 | 42 | .social-proof-author { 43 | display: flex; 44 | flex-direction: row; 45 | gap: 1rem; 46 | } 47 | 48 | .social-proof-author-avatar { 49 | border-radius: 9999px; 50 | overflow: hidden; 51 | } 52 | 53 | .social-proof-author-text { 54 | display: flex; 55 | flex-direction: column; 56 | } 57 | 58 | .social-proof-author-name { 59 | font-weight: 500; 60 | } 61 | 62 | .social-proof-author-handle { 63 | font-weight: 300; 64 | color: hsl(var(--text-muted)); 65 | font-size: .875em; 66 | line-height: 1; 67 | } 68 | 69 | .social-proof-message { 70 | margin: 1rem 0 0 0; 71 | } 72 | 73 | .social-proof-image { 74 | width: 100%; 75 | height: auto; 76 | position: relative; 77 | margin-top: 1rem; 78 | border-radius: .5rem; 79 | } 80 | </style> 81 | -------------------------------------------------------------------------------- /docs/src/components/landing/SocialProofSection.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import { Button } from 'studiocms:ui/components'; 4 | import SocialProofCard from '~/components/landing/SocialProofCard.astro'; 5 | import PageHeader from './PageHeader.astro'; 6 | 7 | const socialProofEntries = await getCollection('socialproof'); 8 | 9 | const chunkSize = socialProofEntries.length / 3; 10 | const chunkedSocialProofEntries = []; 11 | 12 | for (let i = 0; i < socialProofEntries.length; i += chunkSize) { 13 | const chunk = socialProofEntries.slice(i, i + chunkSize); 14 | chunkedSocialProofEntries.push(chunk); 15 | } 16 | --- 17 | <section class="social-proof not-content"> 18 | <PageHeader 19 | icon="hand-thumb-up" 20 | title="Patting ourselves on the back" 21 | subtitle="(Until we have actual opinions)" 22 | /> 23 | <div class="social-proof-grid"> 24 | {chunkedSocialProofEntries.map((entries) => ( 25 | <div class="social-proof-col"> 26 | {entries.map((entry) => ( 27 | <SocialProofCard 28 | {...entry.data} 29 | /> 30 | ))} 31 | </div> 32 | ))} 33 | </div> 34 | <div class="social-proof-cta"> 35 | <Button color='primary' as='a' href={'https://github.com/withstudiocms/ui/issues/new?assignees=&labels=testimonial&projects=&template=3-testimonial.yml&title=%5BTestimonial%5D%3A+<title>'}> 36 | Submit your own opinion! 37 | </Button> 38 | </div> 39 | </section> 40 | <style> 41 | .social-proof-grid { 42 | display: grid; 43 | grid-template-columns: repeat(3, 1fr); 44 | gap: 2rem; 45 | grid-auto-flow: row dense; 46 | } 47 | 48 | .social-proof-col { 49 | display: flex; 50 | flex-direction: column; 51 | gap: 2rem; 52 | } 53 | 54 | .social-proof-cta { 55 | display: flex; 56 | width: 100%; 57 | align-items: center; 58 | justify-content: center; 59 | margin-top: 4rem; 60 | } 61 | 62 | .social-proof { 63 | padding: 0 10vw; 64 | } 65 | 66 | @media screen and (max-width: 1500px) { 67 | .social-proof { 68 | padding: 0 2rem; 69 | } 70 | } 71 | 72 | @media screen and (max-width: 1100px) { 73 | .social-proof-grid { 74 | grid-template-columns: repeat(2, 1fr); 75 | } 76 | 77 | .social-proof-col:last-of-type { 78 | display: none; 79 | } 80 | } 81 | 82 | @media screen and (max-width: 640px) { 83 | .social-proof-grid { 84 | grid-template-columns: repeat(1, 1fr); 85 | } 86 | 87 | .social-proof-col:not(:first-of-type) { 88 | display: none; 89 | } 90 | } 91 | </style> 92 | -------------------------------------------------------------------------------- /docs/src/components/landing/styles.css: -------------------------------------------------------------------------------- 1 | form { 2 | padding: 1.5rem; 3 | background: hsl(var(--background-base)); 4 | } 5 | 6 | form > span { 7 | font-size: 1.5em; 8 | font-weight: 700; 9 | margin-bottom: .75rem; 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/components/media-card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | href?: string | undefined; 4 | } 5 | const { href } = Astro.props; 6 | const El = href ? 'a' : 'span'; 7 | --- 8 | 9 | <li class="not-content sl-flex"> 10 | <El class="media-card sl-flex" {href}> 11 | <div class="media"><slot name="media" /></div> 12 | <slot /> 13 | </El> 14 | </li> 15 | 16 | <style> 17 | .media-card { 18 | flex-direction: column; 19 | border: 1px solid var(--sl-color-gray-5); 20 | border-radius: 0.5rem; 21 | overflow: hidden; 22 | text-decoration: none; 23 | box-shadow: var(--sl-shadow-sm); 24 | width: 100%; 25 | transition: all .15s ease; 26 | } 27 | 28 | a:hover { 29 | border-color: var(--sl-color-gray-2); 30 | background-color: var(--sl-color-gray-7, var(--sl-color-gray-6)); 31 | } 32 | 33 | .media { 34 | border-bottom: 1px solid var(--sl-color-gray-5); 35 | } 36 | 37 | .media :global(img) { 38 | display: block; 39 | max-width: 100%; 40 | height: auto; 41 | aspect-ratio: 16 / 9; 42 | object-fit: cover; 43 | } 44 | 45 | .media-card :global(.title) { 46 | color: var(--sl-color-white); 47 | font-weight: 600; 48 | line-height: var(--sl-line-height-headings); 49 | } 50 | </style> 51 | -------------------------------------------------------------------------------- /docs/src/components/showcase-card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from 'astro:assets'; 3 | import { THUM_SECRET_KEY } from 'astro:env/server'; 4 | import getThumURL from '~/util/thum.io'; 5 | import MediaCard from './media-card.astro'; 6 | 7 | interface Props { 8 | href: string; 9 | title: string; 10 | } 11 | const { href, title } = Astro.props; 12 | 13 | const thum = getThumURL({ 14 | url: Astro.props.href, 15 | width: 800, 16 | crop: 675, 17 | auth: { 18 | type: 'md5', 19 | keyId: '72924', 20 | secret: THUM_SECRET_KEY || '', 21 | }, 22 | }); 23 | --- 24 | 25 | <MediaCard {href}> 26 | <Image slot="media" src={thum} alt="" height={500} width={800} inferSize /> 27 | <p class="title">{title}</p> 28 | </MediaCard> 29 | 30 | <style> 31 | .title { 32 | padding: 0.75rem 1rem; 33 | } 34 | </style> -------------------------------------------------------------------------------- /docs/src/components/showcase-sites.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | import FluidGrid from './fluid-grid.astro'; 4 | import Card from './showcase-card.astro'; 5 | 6 | const sites = (await getCollection('showcase'))[0].data.filter(({ name }) => name !== 'Example'); 7 | --- 8 | 9 | <FluidGrid> 10 | 11 | { 12 | sites.length > 0 13 | ? sites.map(({ link, name }) => ( 14 | <Card title={name} href={link} /> 15 | )) 16 | : <p>No sites on the showcase yet! Start using <code>@studiocms/ui</code> and add your site today!</p> 17 | } 18 | 19 | </FluidGrid> -------------------------------------------------------------------------------- /docs/src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from 'astro:content'; 2 | import { docsLoader, i18nLoader } from '@astrojs/starlight/loaders'; 3 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema'; 4 | import { file, glob } from 'astro/loaders'; 5 | 6 | const baseSchema = z.object({ 7 | type: z.literal('base').optional().default('base'), 8 | i18nReady: z.boolean().optional().default(false), 9 | }); 10 | 11 | const integrationSchema = baseSchema.extend({ 12 | type: z.literal('integration'), 13 | integration: z.object({ 14 | name: z.string(), 15 | githubURL: z.string(), 16 | released: z.boolean().default(true), 17 | }), 18 | }); 19 | 20 | const docs = defineCollection({ 21 | loader: docsLoader(), 22 | schema: docsSchema({ extend: z.union([baseSchema, integrationSchema]) }), 23 | }); 24 | 25 | const i18n = defineCollection({ 26 | loader: i18nLoader(), 27 | schema: i18nSchema({ 28 | extend: z.object({ 29 | 'site-title.labels.docs': z.string().optional(), 30 | 'sponsors.sponsoredby': z.string().optional(), 31 | 'integration-labels.changelog': z.string().optional(), 32 | 'contributors.ui-library': z.string().optional(), 33 | }), 34 | }), 35 | }); 36 | 37 | const socialproof = defineCollection({ 38 | loader: glob({ pattern: '*.json', base: 'src/content/socialproof' }), 39 | schema: z.object({ 40 | avatar: z.string(), 41 | name: z.string(), 42 | handle: z.string(), 43 | message: z.string(), 44 | image: z 45 | .object({ 46 | path: z.string(), 47 | width: z.number(), 48 | height: z.number(), 49 | }) 50 | .optional(), 51 | }), 52 | }); 53 | 54 | const showcase = defineCollection({ 55 | loader: file('src/content/showcase.json'), 56 | schema: z.array( 57 | z.object({ 58 | name: z.string(), 59 | link: z.string(), 60 | }) 61 | ), 62 | }); 63 | 64 | export const collections = { 65 | docs, 66 | i18n, 67 | socialproof, 68 | showcase, 69 | }; 70 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/breadcrumbs.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Breadcrumbs 3 | sidebar: 4 | badge: 5 | text: New! 6 | variant: success 7 | --- 8 | 9 | import { Breadcrumbs } from 'studiocms:ui/components'; 10 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 11 | import PreviewCard from '~/components/PreviewCard.astro'; 12 | 13 | A simple breadcrumb generator component! Used to indicate the current page's location within a navigational hierarchy. 14 | 15 | ## Usage 16 | 17 | <Tabs> 18 | <TabItem label="Preview"> 19 | <PreviewCard> 20 | <Breadcrumbs 21 | segments={[ 22 | { label: "Home", segment: "" }, 23 | { label: "Docs", segment: "docs" }, 24 | { label: "Components", segment: "components" }, 25 | { label: "Breadcrumbs", segment: "breadcrumbs" }, 26 | ]} 27 | /> 28 | </PreviewCard> 29 | </TabItem> 30 | <TabItem label="Code"> 31 | ```astro showLinenumbers title="ButtonExample.astro" 32 | --- 33 | import { Breadcrumbs } from 'studiocms:ui/components'; 34 | --- 35 | 36 | <Breadcrumbs 37 | segments={[ 38 | { label: "Home", segment: "" }, 39 | { label: "Docs", segment: "docs" }, 40 | { label: "Components", segment: "components" }, 41 | { label: "Breadcrumbs", segment: "breadcrumbs" }, 42 | ]} 43 | /> 44 | ``` 45 | </TabItem> 46 | </Tabs> 47 | 48 | ### Changing the separator 49 | 50 | You can change the separator by passing a `separator` prop. 51 | 52 | <Tabs> 53 | <TabItem label="Preview"> 54 | <PreviewCard> 55 | <Breadcrumbs 56 | segments={[ 57 | { label: "Home", segment: "" }, 58 | { label: "Docs", segment: "docs" }, 59 | { label: "Components", segment: "components" }, 60 | { label: "Breadcrumbs", segment: "breadcrumbs" }, 61 | ]} 62 | separator="/" 63 | /> 64 | </PreviewCard> 65 | </TabItem> 66 | <TabItem label="Code"> 67 | ```astro showLinenumbers title="ButtonExample.astro" 68 | --- 69 | import { Breadcrumbs } from 'studiocms:ui/components'; 70 | --- 71 | 72 | <Breadcrumbs 73 | segments={[ 74 | { label: "Home", segment: "" }, 75 | { label: "Docs", segment: "docs" }, 76 | { label: "Components", segment: "components" }, 77 | { label: "Breadcrumbs", segment: "breadcrumbs" }, 78 | ]} 79 | separator="/" 80 | /> 81 | ``` 82 | </TabItem> 83 | </Tabs> 84 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/center.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Center 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { Center } from 'studiocms:ui/components'; 9 | 10 | <small> 11 | This is only here to annoy Jumper. Hi Jumper! 12 | </small> 13 | 14 | Jokes aside. This is a wrapper that saves you some time by abstracting the `flex` or `grid` shenanigans into a component. You can use it like this: 15 | 16 | <Tabs> 17 | <TabItem label="Preview"> 18 | <PreviewCard> 19 | <Center> 20 | Hi Jumper! 21 | </Center> 22 | </PreviewCard> 23 | </TabItem> 24 | <TabItem label="Code"> 25 | ```astro showLinenumbers title="CenterExample.astro" 26 | --- 27 | import { Center } from 'studiocms:ui/components'; 28 | --- 29 | 30 | <Center> 31 | Hi Jumper! 32 | </Center> 33 | ``` 34 | </TabItem> 35 | </Tabs> 36 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/divider.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Divider 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { Divider } from 'studiocms:ui/components'; 9 | 10 | A simple divider. Allows for showing a label in the center, which is helpful for seperating sidebars or other structured content. 11 | 12 | <Tabs> 13 | <TabItem label="Preview"> 14 | <PreviewCard> 15 | <Divider> 16 | Divider 17 | </Divider> 18 | </PreviewCard> 19 | </TabItem> 20 | <TabItem label="Code"> 21 | ```astro showLinenumbers title="DividerExample.astro" 22 | --- 23 | import { Divider } from 'studiocms:ui/components'; 24 | --- 25 | 26 | <Divider> 27 | Divider 28 | </Divider> 29 | ``` 30 | </TabItem> 31 | </Tabs> 32 | 33 | ## Changing the label background color 34 | 35 | There may be cases where your divider does not sit on an element with the default page background color. In such cases, 36 | you can use the `background` prop to change it to match the correct color. 37 | 38 | ```astro "background='background-step-1'" showLinenumbers title="DividerBackgroundExample.astro" 39 | --- 40 | import { Divider } from 'studiocms:ui/components'; 41 | --- 42 | 43 | <Divider background='background-step-1'> 44 | Divider 45 | </Divider> 46 | ``` 47 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/footer.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Footer" 3 | --- 4 | 5 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 6 | import PreviewCard from '~/components/PreviewCard.astro'; 7 | import { Footer } from 'studiocms:ui/components'; 8 | 9 | A footer component with lots of customization options! 10 | 11 | :::note 12 | To see this footer in action, please visit the [landing page](/)! We can't show it in the usual preview 13 | boxes due to size limitations. 14 | ::: 15 | 16 | ## Usage 17 | 18 | The footer component has four different ways of customization: 19 | 20 | 1. The `link` prop for adding links in various categories 21 | 2. The `copyright` prop for displaying a copyright or different license information 22 | 3. The `brand` slot, which can be used for logos or different branding texts 23 | 4. The `socials` slot, which you can use for social icons (or anything else of course) 24 | 25 | ```astro showLinenumbers title="FooterExample.astro" 26 | --- 27 | import { Footer } from 'studiocms:ui/components'; 28 | --- 29 | 30 | <Footer 31 | links={{ 32 | 'Resources': [ 33 | { label: 'Docs', href: '/docs' }, 34 | { label: 'Support', href: '/support' }, 35 | ], 36 | 'Legal': [ 37 | { label: 'Privacy Policy', href: '/privacy' }, 38 | { label: 'Terms of Service', href: '/tos' }, 39 | ], 40 | }} 41 | copyright='MIT License © 2024 - Acme Inc.' 42 | > 43 | <div slot="brand"> 44 | Acme, Inc. 45 | </div> 46 | </Footer> 47 | ``` 48 | 49 | Here's the example of how we implemented this footer on the StudioCMS UI landing page: 50 | 51 | ```astro showLinenumbers title="FooterExample.astro" 52 | --- 53 | import { Footer } from 'studiocms:ui/components'; 54 | // Different imports for icons & logos... 55 | --- 56 | <!-- Other page content --> 57 | <Footer 58 | links={{ 59 | 'StudioCMS Ecosystem': [ 60 | { label: 'StudioCMS', href: 'https://studiocms.dev' }, 61 | { label: 'Apollo', href: 'https://github.com/withstudiocms/apollo' } 62 | ], 63 | 'Resources': [ 64 | { label: 'Docs', href: '/docs' }, 65 | ], 66 | }} 67 | copyright='MIT License © 2024 - StudioCMS' 68 | > 69 | <div slot='brand' class='footer-brand'> 70 | <LogoAdaptive height={48} width={45.76} /> 71 | <span class='studiocms-footer-text'> 72 | <span>StudioCMS</span> 73 | <span>UI</span> 74 | </span> 75 | </div> 76 | <div slot='socials' class='socials'> 77 | <a href='https://github.com/withstudiocms'> 78 | <GitHubLogo width={28} height={28} /> 79 | </a> 80 | <a href='https://chat.studiocms.dev'> 81 | <DiscordLogo width={32} height={32} /> 82 | </a> 83 | <a href='https://patreon.com/studiocms'> 84 | <PatreonLogo width={24} height={24} /> 85 | </a> 86 | <a href='https://bsky.app/profile/studiocms.dev'> 87 | <BlueSkyLogo width={24} height={24} /> 88 | </a> 89 | <a href='https://twitter.com/withstudiocms'> 90 | <TwitterLogo width={22} height={22} /> 91 | </a> 92 | </div> 93 | </Footer> 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/group.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Group 3 | sidebar: 4 | badge: 5 | text: New! 6 | variant: success 7 | --- 8 | 9 | import { Group, Button, Badge } from 'studiocms:ui/components'; 10 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 11 | import PreviewCard from '~/components/PreviewCard.astro'; 12 | 13 | The group component is a container component that groups its children together. 14 | It is useful for grouping buttons, badges, inputs and other components, which 15 | makes it easier to manage their layout. It also changes their styles to make them 16 | look like a bar. 17 | 18 | The following components are supported by the group component: 19 | 20 | - Button 21 | - Badge 22 | 23 | ## Usage 24 | 25 | <Tabs> 26 | <TabItem label="Preview"> 27 | <PreviewCard> 28 | <Group> 29 | <Button color="primary">Button 1</Button> 30 | <Button color="primary">Button 2</Button> 31 | <Button color="primary">Button 3</Button> 32 | </Group> 33 | </PreviewCard> 34 | </TabItem> 35 | <TabItem label="Code"> 36 | ```astro showLinenumbers title="GroupExample.astro" 37 | --- 38 | import { Group, Button } from 'studiocms:ui/components'; 39 | --- 40 | 41 | <Group> 42 | <Button color="primary">Button 1</Button> 43 | <Button color="primary">Button 2</Button> 44 | <Button color="primary">Button 3</Button> 45 | </Group> 46 | ``` 47 | </TabItem> 48 | </Tabs> 49 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/input.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Input 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { Input } from 'studiocms:ui/components'; 9 | 10 | A simple input component with support for easy labels and placeholders. 11 | 12 | ## Usage 13 | 14 | <Tabs> 15 | <TabItem label="Preview"> 16 | <PreviewCard> 17 | <Input label="Label" placeholder="Placeholder" /> 18 | </PreviewCard> 19 | </TabItem> 20 | <TabItem label="Code"> 21 | ```astro showLinenumbers title="InputExample.astro" 22 | --- 23 | import { Input } from 'studiocms:ui/components'; 24 | --- 25 | 26 | <Input label='Label' placeholder='Placeholder' /> 27 | ``` 28 | </TabItem> 29 | </Tabs> 30 | 31 | ### Different types of inputs 32 | 33 | You can set the input's type to like you would normally. Here's a password input to show you how that's done: 34 | 35 | <Tabs> 36 | <TabItem label="Preview"> 37 | <PreviewCard> 38 | <Input label="Label" placeholder="Placeholder" type='password' /> 39 | </PreviewCard> 40 | </TabItem> 41 | <TabItem label="Code"> 42 | ```astro "type='password'" showLinenumbers title="PasswordInputExample.astro" 43 | --- 44 | import { Input } from 'studiocms:ui/components'; 45 | --- 46 | 47 | <Input label='Label' placeholder='Placeholder' type='password' /> 48 | ``` 49 | </TabItem> 50 | </Tabs> 51 | 52 | ### Disabled 53 | 54 | You can disable the input altogether by using the `disabled` prop. 55 | 56 | <Tabs> 57 | <TabItem label="Preview"> 58 | <PreviewCard> 59 | <Input label="Label" placeholder="Placeholder" disabled /> 60 | </PreviewCard> 61 | </TabItem> 62 | <TabItem label="Code"> 63 | ```astro "disabled" showLinenumbers title="DisabledInputExample.astro" 64 | --- 65 | import { Input } from 'studiocms:ui/components'; 66 | --- 67 | 68 | <Input label='Label' placeholder='Placeholder' disabled /> 69 | ``` 70 | </TabItem> 71 | </Tabs> 72 | 73 | ### Form Support 74 | 75 | Inputs have full form support. You can use the `isRequired` and `name` attributes as needed. 76 | 77 | The first one does what it says it does, while the latter sets the `name` attribute so you can access the input in, for example, 78 | the `FormData` returned by a submission. Here's an example showing how to set both of them: 79 | 80 | <Tabs> 81 | <TabItem label="Preview"> 82 | <PreviewCard> 83 | <Input label="Label" placeholder="Placeholder" isRequired name="example-input" /> 84 | </PreviewCard> 85 | </TabItem> 86 | <TabItem label="Code"> 87 | ```astro "isRequired" "name='example-input'" showLinenumbers title="FormInputExample.astro" 88 | --- 89 | import { Input } from 'studiocms:ui/components'; 90 | --- 91 | 92 | <Input label='Label' placeholder='Placeholder' isRequired name='example-input' /> 93 | ``` 94 | </TabItem> 95 | </Tabs> 96 | 97 | :::note 98 | If no name is set, a random one is generated for the component. 99 | ::: 100 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/row.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Row 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { Row } from 'studiocms:ui/components'; 9 | 10 | A small `flex: row` wrapper. Abstracts all the flex weirdness away into a component. 11 | 12 | ## Usage 13 | 14 | <Tabs> 15 | <TabItem label="Preview"> 16 | <PreviewCard> 17 | <Row> 18 | <span>Row Item 1</span> 19 | <span>Row Item 2</span> 20 | <span>Row Item 3</span> 21 | </Row> 22 | </PreviewCard> 23 | </TabItem> 24 | <TabItem label="Code"> 25 | ```astro showLinenumbers title="RowExample.astro" 26 | --- 27 | import { Row } from 'studiocms:ui/components'; 28 | --- 29 | 30 | <Row> 31 | <span>Row Item 1</span> 32 | <span>Row Item 2</span> 33 | <span>Row Item 3</span> 34 | </Row> 35 | ``` 36 | </TabItem> 37 | </Tabs> 38 | 39 | ### `align-items: center` 40 | 41 | You can pass the `alignCenter` prop to add the `align-items: center` CSS property to the row. 42 | 43 | ```astro "alignCenter" showLinenumbers title="RowAlignCenterExample.astro" 44 | --- 45 | import { Row } from 'studiocms:ui/components'; 46 | --- 47 | 48 | <Row alignCenter> 49 | <!-- Content --> 50 | </Row> 51 | ``` 52 | 53 | ### Gap Size 54 | 55 | You can change the gap size of the row via the `gapSize` prop, which can be set to one of `sm`, `md` or `lg`. 56 | 57 | <Tabs> 58 | <TabItem label="Preview"> 59 | <PreviewCard vertical gapSize="lg"> 60 | <Row gapSize="sm"> 61 | <span>Small Row</span> 62 | <span>Small Row</span> 63 | <span>Small Row</span> 64 | </Row> 65 | <Row gapSize="md"> 66 | <span>Medium Row</span> 67 | <span>Medium Row</span> 68 | <span>Medium Row</span> 69 | </Row> 70 | <Row gapSize="lg"> 71 | <span>Large Row</span> 72 | <span>Large Row</span> 73 | <span>Large Row</span> 74 | </Row> 75 | </PreviewCard> 76 | </TabItem> 77 | <TabItem label="Code"> 78 | ```astro "gapSize='sm'" showLinenumbers title="RowGapSizeExample.astro" 79 | --- 80 | import { Row } from 'studiocms:ui/components'; 81 | --- 82 | 83 | <Row gapSize='sm'> 84 | <span>Small Row</span> 85 | <span>Small Row</span> 86 | <span>Small Row</span> 87 | </Row> 88 | <!-- Other examples omitted for simplicity --> 89 | ``` 90 | </TabItem> 91 | </Tabs> 92 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/textarea.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Textarea 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { Textarea } from 'studiocms:ui/components'; 9 | 10 | A simple textarea component with support for easy labels and placeholders. 11 | 12 | ## Usage 13 | 14 | <Tabs> 15 | <TabItem label="Preview"> 16 | <PreviewCard> 17 | <Textarea label="Label" placeholder="Placeholder" /> 18 | </PreviewCard> 19 | </TabItem> 20 | <TabItem label="Code"> 21 | ```astro showLinenumbers title="TextareaExample.astro" 22 | --- 23 | import { Textarea } from 'studiocms:ui/components'; 24 | --- 25 | 26 | <Textarea label='Label' placeholder='Placeholder' /> 27 | ``` 28 | </TabItem> 29 | </Tabs> 30 | 31 | ### Disabled 32 | 33 | You can disable the textarea altogether by using the `disabled` prop. 34 | 35 | <Tabs> 36 | <TabItem label="Preview"> 37 | <PreviewCard> 38 | <Textarea label="Label" placeholder="Placeholder" disabled /> 39 | </PreviewCard> 40 | </TabItem> 41 | <TabItem label="Code"> 42 | ```astro "disabled" showLinenumbers title="DisabledTextareaExample.astro" 43 | --- 44 | import { Textarea } from 'studiocms:ui/components'; 45 | --- 46 | 47 | <Textarea label='Label' placeholder='Placeholder' disabled /> 48 | ``` 49 | </TabItem> 50 | </Tabs> 51 | 52 | ### Form Support 53 | 54 | Textareas have full form support. You can use the `isRequired` and `name` attributes as needed. 55 | 56 | The first one does what it says it does, while the latter sets the `name` attribute so you can access the textarea in, for example, 57 | the `FormData` returned by a submission. Here's an example showing how to set both of them: 58 | 59 | <Tabs> 60 | <TabItem label="Preview"> 61 | <PreviewCard> 62 | <Textarea label="Label" placeholder="Placeholder" isRequired name="example-textarea" /> 63 | </PreviewCard> 64 | </TabItem> 65 | <TabItem label="Code"> 66 | ```astro "isRequired" "name='example-textarea'" showLinenumbers title="FormTextareaExample.astro" 67 | --- 68 | import { Textarea } from 'studiocms:ui/components'; 69 | --- 70 | 71 | <Textarea label='Label' placeholder='Placeholder' isRequired name='example-textarea' /> 72 | ``` 73 | </TabItem> 74 | </Tabs> 75 | 76 | :::note 77 | If no name is set, a random one is generated for the component. 78 | ::: 79 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/theme-toggle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: Theme Toggle 4 | --- 5 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 6 | import PreviewCard from '~/components/PreviewCard.astro'; 7 | import { ThemeToggle, Icon } from 'studiocms:ui/components'; 8 | 9 | A simple but useful helper component that you can use as a theme toggle. 10 | 11 | ## Usage 12 | 13 | <Tabs> 14 | <TabItem label="Preview"> 15 | <PreviewCard> 16 | <ThemeToggle> 17 | <Icon name='moon' width={24} height={24} slot="dark" /> 18 | <Icon name='sun' width={24} height={24} slot="light" /> 19 | </ThemeToggle> 20 | </PreviewCard> 21 | </TabItem> 22 | <TabItem label="Code"> 23 | ```astro showLinenumbers title="ThemeToggleExample.astro" 24 | --- 25 | import { ThemeToggle, Icon } from 'studiocms:ui/components'; 26 | --- 27 | 28 | <ThemeToggle> 29 | <div slot='dark'> 30 | {/* Content in here will be displayed when the theme is dark! */} 31 | <Icon name='moon' width={24} height={24} slot="dark" /> 32 | </div> 33 | <div slot='light'> 34 | {/* Content in here will be displayed when the theme is light! */} 35 | <Icon name='sun' width={24} height={24} slot="light" /> 36 | </div> 37 | </ThemeToggle> 38 | ``` 39 | </TabItem> 40 | </Tabs> 41 | 42 | ### Extending the Toggle 43 | 44 | The `<ThemeToggle />` component is really just a button with two slots. You can pass all props from the 45 | <a href="/customizing/studiocms-ui/components/button/">`<Button />` component</a> and they will apply to the toggle. 46 | 47 | :::danger 48 | The `<ThemeToggle />` element has a unique ID and should only be used once on the entire page, for example in the navbar. 49 | If you need more than one toggle, consider creating your own using the <a href="/customizing/studiocms-ui/components/theme-helper/">`ThemeHelper`</a>. 50 | ::: 51 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/components/user.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: User 4 | --- 5 | 6 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 7 | import PreviewCard from '~/components/PreviewCard.astro'; 8 | import { User } from 'studiocms:ui/components'; 9 | import Avatar from '~/assets/avatar.png' 10 | 11 | A component to display user info. 12 | 13 | ## Usage 14 | 15 | <Tabs> 16 | <TabItem label="Preview"> 17 | <PreviewCard> 18 | <User name="John Doe" description="A cool dude" /> 19 | </PreviewCard> 20 | </TabItem> 21 | <TabItem label="Code"> 22 | ```astro showLinenumbers title="UserExample.astro" 23 | --- 24 | import { User } from 'studiocms:ui/components'; 25 | --- 26 | 27 | <User name='John Doe' description='A cool dude' /> 28 | ``` 29 | </TabItem> 30 | </Tabs> 31 | 32 | ### Avatar 33 | 34 | You can provide an `avatar` using the same rules as the [`Image`](https://docs.astro.build/en/guides/images/#src-required) component. 35 | To change the loading behavior, you can pass the `loading` attribute. It is passed to the `<Image />` component. 36 | 37 | <Tabs> 38 | <TabItem label="Preview"> 39 | <PreviewCard> 40 | <User name="John Doe" loading='eager' description="A cool dude" avatar={Avatar} /> 41 | </PreviewCard> 42 | </TabItem> 43 | <TabItem label="Code"> 44 | ```astro "avatar={ProfileImage}" "loading='eager'" showLinenumbers title="UserAvatarExample.astro" 45 | --- 46 | import { User } from 'studiocms:ui/components'; 47 | import ProfileImage from '~/assets/profile.png' 48 | --- 49 | 50 | <User name='John Doe' loading='eager' description='A cool dude' avatar={ProfileImage} /> 51 | ``` 52 | </TabItem> 53 | </Tabs> 54 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | i18nReady: true 3 | title: "Getting Started" 4 | description: Install @studiocms/ui in your own project and make use of our components! 5 | type: integration 6 | integration: 7 | name: "@studiocms/ui" 8 | githubURL: "https://github.com/withstudiocms/ui/tree/main/packages/studiocms_ui" 9 | --- 10 | 11 | import { PackageManagers } from 'starlight-package-managers' 12 | 13 | :::danger[Accessibility] 14 | `@studiocms/ui` has not yet been audited for accessibility. You can track our progress in our [accessibility issue tracker](https://github.com/withstudiocms/ui/issues/19) on GitHub. 15 | ::: 16 | 17 | `@studiocms/ui` is the UI library that we use to build StudioCMS. The library is agnostic, meaning that you can use it 18 | in your own Astro projects! 19 | 20 | To get started, add the integration to your project. 21 | 22 | <PackageManagers type="exec" pkg="astro add @studiocms/ui" /> 23 | 24 | <details> 25 | <summary>Using `@studiocms/ui` with Astro 4</summary> 26 | 27 | StudioCMS UI can be used in Astro 5 without any additional configuration. As for Astro v4, `@studiocms/ui` requires version `v4.5.0` or higher, with the 28 | `experimental.directRenderScript` flag turned on. 29 | </details> 30 | 31 | ## Manual Installation 32 | 33 | Install the `@studiocms/ui` package with your preferred package manager. 34 | 35 | <PackageManagers type="add" pkg="@studiocms/ui" /> 36 | 37 | Import and use the `@studiocms/ui` integration in your Astro config. 38 | 39 | ```ts showLinenumbers title="astro.config.mjs" {2,6} 40 | import { defineConfig } from 'astro/config'; 41 | import ui from '@studiocms/ui'; 42 | 43 | export default defineConfig({ 44 | integrations: [ 45 | ui() 46 | ] 47 | }); 48 | ``` 49 | 50 | ## Importing Components 51 | 52 | All components in this library are exported from `@studiocms/ui/components`. You can import and use the components like this: 53 | 54 | ```astro showLinenumbers title="ButtonExample.astro" 55 | --- 56 | import { Button } from 'studiocms:ui/components'; 57 | --- 58 | 59 | <Button /> 60 | ``` 61 | 62 | ## Using `@studiocms/ui` with Tailwind 63 | 64 | If you haven't already, add Astro's Tailwind integration to your project. 65 | 66 | <PackageManagers type="exec" pkg="astro add tailwind" /> 67 | 68 | Disable [Tailwind's preflight](https://tailwindcss.com/docs/preflight) styles with `applyBaseStyles: false`. 69 | 70 | ```ts showLinenumbers title="astro.config.mjs" "applyBaseStyles: false" 71 | import { defineConfig } from 'astro/config'; 72 | import ui from '@studiocms/ui'; 73 | 74 | export default defineConfig({ 75 | integrations: [ 76 | ui(), 77 | tailwind({ applyBaseStyles: false }) 78 | ] 79 | }); 80 | ``` 81 | 82 | Create a CSS file for Tailwind's base styles. For example `src/styles/tailwind.css`. 83 | 84 | ```css showLinenumbers title="src/styles/tailwind.css" 85 | @tailwind base; 86 | @tailwind components; 87 | @tailwind utilities; 88 | ``` 89 | 90 | Import this CSS file in any layout or page that uses Tailwind. 91 | 92 | ```astro showLinenumbers title="src/layouts/Base.astro" 93 | --- 94 | import '../styles/tailwind.css'; 95 | 96 | // ... 97 | --- 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/showcase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Site Showcase" 3 | description: "See what others have built with the StudioCMS UI library." 4 | editUrl: false 5 | --- 6 | 7 | :::tip[Add your own!] 8 | Have you built a site with `@studiocms/ui`? 9 | [Open a PR](https://github.com/withstudiocms/ui/blob/main/docs/src/content/showcase.json) to add it to the showcase! 10 | ::: 11 | 12 | ## Sites 13 | 14 | import ShowcaseSites from '~/components/showcase-sites.astro'; 15 | 16 | **`@studiocms/ui`** is already being used in production. These are some of the sites around the web: 17 | 18 | <ShowcaseSites /> 19 | 20 | See all the [public project repos using `@studiocms/ui` on GitHub](https://github.com/withstudiocms/ui/network/dependents). -------------------------------------------------------------------------------- /docs/src/content/docs/docs/upgrade-guides/0.1.0-to-0.3.0.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: From v0.1 to v0.3 3 | --- 4 | 5 | import { Steps } from '@astrojs/starlight/components'; 6 | 7 | Version 0.3.0 of `@studiocms/ui` introduced mostly accessibility improvements and is backwards compatible. 8 | 9 | While there are no breaking changes, **the method of importing the `global.css` stylesheet in the root layout 10 | has been deprecated in favor of using the provided integration.** 11 | 12 | We recommend doing the following to upgrade your project: 13 | 14 | <Steps> 15 | 1. Remove the `global.css` import from your root layout. 16 | ```astro showlinenumbers del={2} title="Layout.astro" 17 | --- 18 | import '@studiocms/ui/css/global.css'; 19 | --- 20 | ``` 21 | 22 | 2. Add the following code to your `astro.config.mjs` file: 23 | ```ts showLinenumbers title="astro.config.mjs" {2,6} 24 | import { defineConfig } from 'astro/config'; 25 | import ui from '@studiocms/ui'; 26 | 27 | export default defineConfig({ 28 | integrations: [ 29 | ui() 30 | ] 31 | }); 32 | ``` 33 | </Steps> 34 | 35 | The integration will automatically add the stylesheet to your page. 36 | -------------------------------------------------------------------------------- /docs/src/content/docs/docs/upgrade-guides/0.3-to-0.4.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: From v0.3 to v0.4 3 | --- 4 | 5 | In v0.3 of StudioCMS UI, we turned the library into an Astro integration. With v0.4, we're building on that foundation to fix some critical issues and further improve the developer experience. Here's what you'll have to do to migrate your existing projects: 6 | 7 | ## New Imports 8 | 9 | In order to remove issues with script and style leakage, we've updated all imports to use virtual modules instead of barrel files. This means that you will have to change all of your imports for all components: 10 | 11 | ```astro del={2} ins={3} 12 | --- 13 | import { Button } from '@studiocms/ui/components'; 14 | import { Button } from 'studiocms:ui/components'; 15 | --- 16 | ``` 17 | 18 | We recommend simply bulk-replacing `@studiocms/ui/` with `studiocms:ui/`. 19 | -------------------------------------------------------------------------------- /docs/src/content/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "site-title.labels.docs": "Docs", 3 | "sponsors.sponsoredby": "Sponsored by", 4 | "integration-labels.changelog": "Changelog", 5 | "contributors.ui-library": "StudioCMS UI Library" 6 | } 7 | -------------------------------------------------------------------------------- /docs/src/content/showcase.json: -------------------------------------------------------------------------------- 1 | { 2 | "sites": [ 3 | { 4 | "name": "Example", 5 | "link": "https://example.com" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /docs/src/content/socialproof/adam-matthiesen.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../.astro/collections/socialproof.schema.json", 3 | "avatar": "/socialproof/adam-matthiesen.jpg", 4 | "name": "Adam Matthiesen", 5 | "handle": "@Adammatthiesen", 6 | "message": "The @studiocms/ui library has completely transformed how I build interfaces in Astro. Its thoughtfully designed components are incredibly easy to use, saving me hours of development time while delivering a sleek, professional look. Whether I'm crafting a simple site or a complex dashboard, this library gives me the tools I need to create something exceptional. I can't imagine working on an Astro project without it!" 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/content/socialproof/louis-escher.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../.astro/collections/socialproof.schema.json", 3 | "avatar": "/socialproof/louis-escher.jpg", 4 | "name": "Louis Escher", 5 | "handle": "@louisescher.dev", 6 | "message": "It's funny how things come to be sometimes. One day, you're browsing GitHub and the next day you're writing a UI library for a project you discovered at 11pm on a Tuesday. Now go install StudioCMS UI." 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/content/socialproof/matthew-justice.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../.astro/collections/socialproof.schema.json", 3 | "avatar": "/socialproof/matthew-justice.jpg", 4 | "name": "Matthew Justice", 5 | "handle": "@justicematthew", 6 | "message": "Look, we've all been there working with Astro. You're building components for your UI, things are getting complex with the interactions and all the accessibility concerns, the struggle is real. You think \"What's the harm installing React? I can use shadcn or NextUI\" and BAM, your project is now bloated with tons of unnecessary JavaScript for basically nothing. \"No more\" I said. Now I install StudioCMS UI." 7 | } 8 | -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference path="../.astro/types.d.ts" /> 2 | /// <reference types="astro/client" /> 3 | -------------------------------------------------------------------------------- /docs/src/plugins/rehype.types.ts: -------------------------------------------------------------------------------- 1 | import type * as hast from 'hast'; 2 | import type * as unified from 'unified'; 3 | 4 | // biome-ignore lint/suspicious/noExplicitAny: any is used to match the generic type 5 | export type RehypePlugin<PluginParameters extends any[] = any[]> = unified.Plugin< 6 | PluginParameters, 7 | hast.Root 8 | >; 9 | 10 | // biome-ignore lint/suspicious/noExplicitAny: any is used to match the generic type 11 | export type RehypePlugins = (string | [string, any] | RehypePlugin | [RehypePlugin, any])[]; 12 | -------------------------------------------------------------------------------- /docs/src/plugins/rehypeAutolink.ts: -------------------------------------------------------------------------------- 1 | import { toString as toStr } from 'hast-util-to-string'; 2 | import { h } from 'hastscript'; 3 | import { escape as esc } from 'html-escaper'; 4 | import rehypeAutoLink from 'rehype-autolink-headings'; 5 | import type { Options as rehypeAutolinkHeadingsOptions } from 'rehype-autolink-headings'; 6 | import type { RehypePlugin } from './rehype.types'; 7 | 8 | const AnchorLinkIcon = h( 9 | 'span', 10 | { ariaHidden: 'true', class: 'anchor-icon' }, 11 | h( 12 | 'svg', 13 | { 14 | width: 16, 15 | height: 16, 16 | viewBox: '0 0 24 24', 17 | fill: 'none', 18 | stroke: 'currentColor', 19 | strokeWidth: 1.5, 20 | }, 21 | h('path', { 22 | strokeLinecap: 'round', 23 | strokeLinejoin: 'round', 24 | d: 'M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244', 25 | }) 26 | ) 27 | ); 28 | 29 | const createSROnlyLabel = (text: string) => { 30 | return h('span', { 'is:raw': true, class: 'sr-only' }, `'Read the “', ${esc(text)}, '” section'`); 31 | }; 32 | 33 | export { AnchorLinkIcon, createSROnlyLabel }; 34 | 35 | // biome-ignore lint/suspicious/noExplicitAny: any is used to match the generic type 36 | export const rehypeAutolinkHeadings: [RehypePlugin, any] = [ 37 | rehypeAutoLink, 38 | { 39 | properties: { 40 | class: 'anchor-link', 41 | }, 42 | behavior: 'after', 43 | group: ({ tagName }) => 44 | h('div', { 45 | tabIndex: -1, 46 | class: `heading-wrapper level-${tagName}`, 47 | }), 48 | content: (heading) => [AnchorLinkIcon, createSROnlyLabel(toStr(heading))], 49 | } as rehypeAutolinkHeadingsOptions, 50 | ]; 51 | 52 | export default rehypeAutolinkHeadings; 53 | -------------------------------------------------------------------------------- /docs/src/plugins/rehypeExternalLinks.ts: -------------------------------------------------------------------------------- 1 | import rehypeExternal from 'rehype-external-links'; 2 | import type { RehypePlugin } from './rehype.types'; 3 | 4 | // biome-ignore lint/suspicious/noExplicitAny: any is used to match the generic type 5 | export const rehypeExternalLinks: [RehypePlugin, any] = [ 6 | rehypeExternal, 7 | { 8 | content: { 9 | type: 'text', 10 | value: ' ⤴', 11 | }, 12 | properties: { 13 | target: '_blank', 14 | }, 15 | rel: ['noopener', 'noreferrer'], 16 | }, 17 | ]; 18 | 19 | export default rehypeExternalLinks; 20 | -------------------------------------------------------------------------------- /docs/src/plugins/rehypePluginKit.ts: -------------------------------------------------------------------------------- 1 | import rehypeSlug from 'rehype-slug'; 2 | import type { RehypePlugins } from './rehype.types'; 3 | import rehypeAutolinkHeadings from './rehypeAutolink'; 4 | import rehypeExternalLinks from './rehypeExternalLinks'; 5 | 6 | export const rehypePluginKit: RehypePlugins = [ 7 | rehypeSlug, 8 | rehypeAutolinkHeadings, 9 | rehypeExternalLinks, 10 | ]; 11 | 12 | export default rehypePluginKit; 13 | -------------------------------------------------------------------------------- /docs/src/share-link.ts: -------------------------------------------------------------------------------- 1 | export { default as SponsorLink } from './util/SponsorLink.astro'; 2 | 3 | // This file is used to define the sponsors and their respective urls. 4 | export const sponsors = { 5 | turso: { 6 | docs: { 7 | sidebarSponsorLink: 'https://tur.so/studiocms', 8 | installCLILink: 'https://docs.turso.tech/cli/installation', 9 | loginsignupLink: 'https://docs.turso.tech/cli/authentication', 10 | }, 11 | }, 12 | }; 13 | 14 | export default sponsors; 15 | -------------------------------------------------------------------------------- /docs/src/starlightOverrides/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Default from '@astrojs/starlight/components/Head.astro'; 3 | import type { Props } from '@astrojs/starlight/props'; 4 | import '@fontsource-variable/onest/index.css'; 5 | import onestWoff2 from '@fontsource-variable/onest/files/onest-latin-wght-normal.woff2?url'; 6 | --- 7 | 8 | <Default {...Astro.props}><slot /></Default> 9 | <link rel="preload" as="font" type="font/woff2" href={onestWoff2} crossorigin="anonymous" /> 10 | -------------------------------------------------------------------------------- /docs/src/starlightOverrides/PageTitle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from '@astrojs/starlight/components'; 3 | import Default from '@astrojs/starlight/components/PageTitle.astro'; 4 | import type { Props } from '@astrojs/starlight/props'; 5 | import Version from '~/components/Version.astro'; 6 | 7 | const { data } = Astro.props.entry; 8 | 9 | function ensureTrailingSlash(url: string) { 10 | return url.endsWith('/') ? url : `${url}/`; 11 | } 12 | --- 13 | { 14 | data.type === 'integration' ? ( 15 | <div class="wrapper"> 16 | <h1 id="_top"> 17 | <span class="scope">@studiocms/</span> 18 | <span>{data.integration.name.replace('@studiocms/', '')}</span> 19 | </h1> 20 | <div class="integration-metadata"> 21 | { data.integration.released ? <Version pkgName={data.integration.name} /> : <code>N/A</code>} 22 | <a href={data.integration.githubURL}> 23 | <Icon size="1rem" name="github" /> GitHub 24 | </a> 25 | <a href={'https://www.npmjs.com/package/' + data.integration.name}> 26 | <svg width="16" height="16" viewBox="0 0 576 512" fill="currentColor" aria-hidden="true"> 27 | <path d="M288 288h-32v-64h32v64zm288-128v192H288v32H160v-32H0V160h576zm-416 32H32v128h64v-96h32v96h32V192zm160 0H192v160h64v-32h64V192zm224 0H352v128h64v-96h32v96h32v-96h32v96h32V192z" /> 28 | </svg> 29 | npm 30 | </a> 31 | <a href={ensureTrailingSlash(data.integration.githubURL) + 'CHANGELOG.md'}> 32 | <Icon size="1rem" name="list-format" /> {Astro.locals.t('integration-labels.changelog')} 33 | </a> 34 | </div> 35 | </div> 36 | ) : data.template !== 'splash' && ( 37 | <Default {...Astro.props}> 38 | <slot /> 39 | </Default> 40 | )} 41 | 42 | <style> 43 | .wrapper { 44 | display: flex; 45 | flex-direction: column; 46 | gap: 0.5rem; 47 | } 48 | h1 { 49 | display: flex; 50 | flex-wrap: wrap; 51 | color: var(--sl-color-white); 52 | font-size: var(--sl-text-h1); 53 | font-weight: 700; 54 | line-height: var(--sl-line-height-headings); 55 | } 56 | .scope { 57 | color: var(--sl-color-text); 58 | font-weight: 500; 59 | } 60 | .integration-metadata { 61 | display: flex; 62 | flex-wrap: wrap; 63 | align-items: center; 64 | gap: 0.5rem 1rem; 65 | font-size: var(--sl-text-xs); 66 | } 67 | .integration-metadata a { 68 | display: flex; 69 | align-items: center; 70 | gap: 0.25rem; 71 | text-decoration: none; 72 | color: var(--sl-color-text-accent); 73 | } 74 | .integration-metadata a:hover { 75 | color: var(--sl-color-white); 76 | } 77 | .integration-metadata svg { 78 | width: 1.5rem; 79 | } 80 | code { 81 | background-color: var(--sl-custom-code-color); 82 | border-radius: var(--sl-border-radius-medium); 83 | color: var(--sl-color-gray-800); 84 | font-size: var(--sl-font-size-small); 85 | padding: 0.125rem 0.375rem; 86 | margin-block: -0.125rem; 87 | } 88 | </style> 89 | -------------------------------------------------------------------------------- /docs/src/starlightOverrides/Sidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // import Default from '@lorenzo_lewis/starlight-utils/components/Sidebar.astro'; 3 | import Default from '@astrojs/starlight/components/Sidebar.astro'; 4 | import type { Props } from '@astrojs/starlight/props'; 5 | import Sponsors from '../components/Sponsors.astro'; 6 | --- 7 | 8 | <div class="sidebar-custom"> 9 | <Default {...Astro.props}><slot /></Default> 10 | 11 | <div class="sl-hidden md:sl-block sidebar-footer"> 12 | <Sponsors /> 13 | </div> 14 | </div> 15 | 16 | <style> 17 | .sidebar-custom { 18 | padding: 0; 19 | display: flex; 20 | flex-direction: column; 21 | height: 98%; 22 | } 23 | .sidebar-footer { 24 | margin-top: auto; 25 | } 26 | </style> 27 | -------------------------------------------------------------------------------- /docs/src/styles/sponsorcolors.css: -------------------------------------------------------------------------------- 1 | /* This is Turso's Branding Colors for the sponsors sidebar section */ 2 | [data-theme="light"] .turso-logo > path { 3 | fill: #183134; 4 | } 5 | -------------------------------------------------------------------------------- /docs/src/util-server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file contains server-side utilities using features that don't work in a browser. 3 | Do not import this file from a hydrated client-side component. 4 | */ 5 | 6 | // @ts-expect-error Package without types we can’t do anything about. 7 | import EleventyFetch from '@11ty/eleventy-fetch'; 8 | import retry from 'p-retry'; 9 | 10 | export type CachedFetchOptions = { 11 | duration?: string; 12 | verbose?: boolean; 13 | }; 14 | 15 | export async function cachedFetch( 16 | url: string, 17 | fetchOptions = {}, 18 | { duration = '5m', verbose = false }: CachedFetchOptions = {} 19 | ) { 20 | let status = 200; 21 | let statusText = 'OK'; 22 | let buffer: Buffer | undefined; 23 | 24 | try { 25 | buffer = await retry(() => 26 | EleventyFetch(url, { 27 | duration, 28 | verbose, 29 | type: 'buffer', 30 | fetchOptions, 31 | }) 32 | ); 33 | } catch (e: unknown) { 34 | const error = e as Error; 35 | const msg: string = error?.message || error.toString(); 36 | const matches = msg.match(/^Bad response for (.*) \(.*?\): (.*)$/); 37 | status = Number.parseInt(matches?.[2] || '') || 404; 38 | statusText = matches?.[3] || msg; 39 | } 40 | 41 | return { 42 | ok: status >= 200 && status <= 299, 43 | status, 44 | statusText, 45 | body: buffer, 46 | json: () => buffer && JSON.parse(buffer.toString()), 47 | text: () => buffer?.toString(), 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /docs/src/util/SponsorLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | text: string; 4 | href: string | URL; 5 | } 6 | const { text, href } = Astro.props; 7 | --- 8 | <a {href} aria-label={text} target="_blank" rel="noopener sponsored">{text} ⤴</a> -------------------------------------------------------------------------------- /docs/src/util/contributors.config.ts: -------------------------------------------------------------------------------- 1 | import type { AstroGlobal } from 'astro'; 2 | 3 | type RepoListItem = { 4 | repo: string; 5 | type: 'all' | 'byPath'; 6 | paths?: string[]; 7 | }; 8 | 9 | type ContributorConfig = { 10 | name: string; 11 | list: RepoListItem[]; 12 | }; 13 | 14 | export const StudioCMSServiceAccounts: string[] = ['studiocms-no-reply']; 15 | 16 | export const contributorConfig = (Astro: AstroGlobal): ContributorConfig[] => [ 17 | { 18 | name: Astro.locals.t('contributors.core-packages'), 19 | list: [ 20 | { 21 | repo: 'withstudiocms/studiocms', 22 | type: 'byPath', 23 | paths: [ 24 | // OLD Paths 25 | 'packages/studioCMS/', 26 | // NEW Paths 27 | 'README.md', 28 | 'packages/studiocms/', 29 | 'packages/studiocms_assets/', 30 | 'packages/studiocms_auth/', 31 | 'packages/studiocms_betaresources/', 32 | 'packages/studiocms_core/', 33 | 'packages/studiocms_dashboard/', 34 | 'packages/studiocms_frontend/', 35 | 'packages/studiocms_imagehandler/', 36 | 'packages/studiocms_renderers/', 37 | 'packages/studiocms_robotstxt/', 38 | ], 39 | }, 40 | ], 41 | }, 42 | { 43 | name: Astro.locals.t('contributors.ui-library'), 44 | list: [ 45 | { 46 | repo: 'withstudiocms/studiocms', 47 | type: 'byPath', 48 | paths: ['packages/studiocms_ui/'], 49 | }, 50 | ], 51 | }, 52 | { 53 | name: Astro.locals.t('contributors.devapps'), 54 | list: [ 55 | { 56 | repo: 'withstudiocms/studiocms', 57 | type: 'byPath', 58 | paths: ['packages/studiocms_devapps/'], 59 | }, 60 | ], 61 | }, 62 | { 63 | name: Astro.locals.t('contributors.plugins'), 64 | list: [ 65 | { 66 | repo: 'withstudiocms/studiocms', 67 | type: 'byPath', 68 | paths: [ 69 | // OLD Paths 70 | 'packages/studioCMSBlog/', 71 | // NEW Paths 72 | 'packages/studiocms_blog/', 73 | ], 74 | }, 75 | ], 76 | }, 77 | { 78 | name: Astro.locals.t('contributors.documentation'), 79 | list: [ 80 | { 81 | repo: 'withstudiocms/studiocms', 82 | type: 'byPath', 83 | paths: ['www/docs/'], 84 | }, 85 | ], 86 | }, 87 | { 88 | name: Astro.locals.t('contributors.website'), 89 | list: [ 90 | { 91 | repo: 'withstudiocms/studiocms', 92 | type: 'byPath', 93 | paths: ['www/web/'], 94 | }, 95 | { 96 | repo: 'withstudiocms/studiocms.dev', 97 | type: 'all', 98 | }, 99 | ], 100 | }, 101 | { 102 | name: Astro.locals.t('contributors.bots'), 103 | list: [ 104 | { 105 | repo: 'withstudiocms/apollo', 106 | type: 'all', 107 | }, 108 | ], 109 | }, 110 | ]; 111 | -------------------------------------------------------------------------------- /docs/src/util/thum.io.ts: -------------------------------------------------------------------------------- 1 | import md5 from 'md5'; 2 | 3 | type ThumURLAuthOptions = 4 | | { 5 | type: 'referer'; 6 | } 7 | | { 8 | type: 'raw' | 'md5'; 9 | keyId: string; 10 | secret: string; 11 | }; 12 | 13 | interface IThumURLOptions { 14 | url: string; 15 | protocol?: string; 16 | useImageAPI?: boolean; 17 | maxAge?: number; 18 | width?: number; 19 | crop?: number; 20 | png?: boolean; 21 | refresh?: boolean; 22 | ogImage?: boolean; 23 | device?: 'ipad' | 'iphone5' | 'iphone6' | 'iphone6plus' | 'galaxys5'; 24 | auth?: string | ThumURLAuthOptions; 25 | } 26 | 27 | export default function getThumURL(options: IThumURLOptions) { 28 | let thumUrl = '//image.thum.io/get'; 29 | 30 | const url = options.url; 31 | if (!url) { 32 | throw new Error('Url must be specified'); 33 | } 34 | 35 | const protocol = options.protocol; 36 | if (protocol) { 37 | thumUrl = `${protocol}:${thumUrl}`; 38 | } 39 | 40 | const useImageAPI = options.useImageAPI; 41 | if (useImageAPI) { 42 | thumUrl += '/image'; 43 | } 44 | 45 | const maxAge = options.maxAge; 46 | if (maxAge) { 47 | thumUrl += `/maxAge/${maxAge}`; 48 | } 49 | 50 | const width = options.width; 51 | if (width) { 52 | thumUrl += `/width/${width}`; 53 | } 54 | 55 | const crop = options.crop; 56 | if (crop) { 57 | thumUrl += `/crop/${crop}`; 58 | } 59 | 60 | const png = options.png; 61 | if (png) { 62 | thumUrl += '/png'; 63 | } 64 | 65 | const refresh = options.refresh; 66 | if (refresh) { 67 | thumUrl += '/refresh'; 68 | } 69 | 70 | const ogImage = options.ogImage; 71 | if (ogImage) { 72 | thumUrl += '/ogImage'; 73 | } 74 | 75 | const device = options.device; 76 | if (device) { 77 | switch (device) { 78 | case 'ipad': 79 | case 'iphone5': 80 | case 'iphone6': 81 | case 'iphone6plus': 82 | case 'galaxys5': 83 | thumUrl += `/${device}`; 84 | break; 85 | 86 | default: 87 | throw new Error('Device is not valid'); 88 | } 89 | } 90 | 91 | const auth = options.auth; 92 | if (auth) { 93 | if (typeof auth === 'string') { 94 | thumUrl += `/auth/${auth}`; 95 | } else { 96 | switch (auth.type) { 97 | case 'raw': 98 | thumUrl += `/auth/${auth.keyId}-${auth.secret}`; 99 | break; 100 | case 'md5': { 101 | // Add 300 seconds to the current time for a 5 minute expiry 102 | const expires = new Date().getTime() + 1000 * 300; 103 | 104 | const hash = md5(auth.secret + expires + url); 105 | thumUrl += `/auth/${auth.keyId}-${expires}-${hash}`; 106 | 107 | break; 108 | } 109 | case 'referer': 110 | // Doesn't need to add to URL 111 | break; 112 | default: 113 | throw new Error("Auth type must be 'raw' or 'md5' or 'referer'"); 114 | } 115 | } 116 | } 117 | 118 | return `${thumUrl}/${url}`; 119 | } 120 | -------------------------------------------------------------------------------- /docs/starlight-types.ts: -------------------------------------------------------------------------------- 1 | export type SidebarGroup = 2 | | SidebarManualGroup 3 | | { 4 | autogenerate: { 5 | collapsed?: boolean; 6 | directory: string; 7 | }; 8 | collapsed?: boolean; 9 | label: string; 10 | }; 11 | 12 | interface SidebarManualGroup { 13 | collapsed?: boolean; 14 | items: (LinkItem | SidebarGroup)[]; 15 | label: string; 16 | badge?: 17 | | string 18 | | { 19 | text: string; 20 | variant: 'note' | 'danger' | 'success' | 'caution' | 'tip' | 'default'; 21 | } 22 | | undefined; 23 | } 24 | 25 | interface LinkItem { 26 | label: string; 27 | link: string; 28 | } 29 | -------------------------------------------------------------------------------- /docs/starlight-virtual.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:starlight/user-images' { 2 | type ImageMetadata = import('astro').ImageMetadata; 3 | export const logos: { 4 | dark?: ImageMetadata; 5 | light?: ImageMetadata; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "exclude": ["dist"], 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "moduleResolution": "bundler", 7 | "baseUrl": ".", 8 | "paths": { 9 | "~/*": ["src/*"] 10 | }, 11 | "outDir": "../../.moon/cache/types/www/docs" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "packageManager": "pnpm@9.5.0", 5 | "engines": { 6 | "node": "20.14.0" 7 | }, 8 | "scripts": { 9 | "build": "pnpm --filter @studiocms/* build", 10 | "dev": "pnpm --stream --filter @studiocms/* --filter docs -r -parallel dev", 11 | 12 | "docs:dev": "pnpm --filter docs dev", 13 | "docs:build": "pnpm build && pnpm --filter docs build", 14 | "docs:preview": "pnpm --filter docs preview", 15 | "docs:sync": "pnpm --filter docs astro sync", 16 | "lint": "biome check .", 17 | "lint:fix": "biome check --write .", 18 | "ci:lint": "biome ci --formatter-enabled=true --organize-imports-enabled=true --reporter=github", 19 | "ci:install": "pnpm install --frozen-lockfile", 20 | "ci:prepublish": "pnpm build", 21 | "ci:version": "pnpm changeset version", 22 | "ci:publish": "pnpm changeset publish", 23 | "ci:snapshot": "pnpx pkg-pr-new publish --pnpm --compact './packages/*' " 24 | }, 25 | "dependencies": { 26 | "@actions/core": "^1.11.1", 27 | "@biomejs/biome": "^1.9.4", 28 | "@changesets/cli": "2.27.9", 29 | "@changesets/config": "3.0.3", 30 | "@changesets/changelog-github": "^0.5.0", 31 | "build-scripts": "workspace:*", 32 | "pkg-pr-new": "^0.0.35", 33 | "typescript": "catalog:" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/studiocms_ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /packages/studiocms_ui/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 StudioCMS - Louis Escher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/studiocms_ui/README.md: -------------------------------------------------------------------------------- 1 | # `@studiocms/ui` 2 | 3 | [![NPM Version](https://img.shields.io/npm/v/@studiocms/ui)](https://npm.im/@studiocms/ui) 4 | [![Formatted with Biome](https://img.shields.io/badge/Formatted_with-Biome-60a5fa?style=flat&logo=biome)](https://biomejs.dev/) 5 | [![Built with Astro](https://astro.badg.es/v2/built-with-astro/tiny.svg)](https://astro.build) 6 | 7 | This is the UI library that we use to build StudioCMS. 8 | 9 | To see how to get started, check out [the documentation](https://ui.studiocms.dev). 10 | 11 | ## Contributing 12 | 13 | We welcome contributions from the community! Whether it's bug reports, feature requests, or code contributions, we appreciate your help in making this project better. 14 | 15 | ### Bug Reports and Feature Requests 16 | 17 | If you encounter any bugs or have ideas for new features, please [open an issue](https://github.com/withstudiocms/ui/issues/new/choose) on our [GitHub repository](https://github.com/withstudiocms/ui/). When creating a new issue, please provide as much detail as possible, including steps to reproduce the issue (for bugs) or a clear description of the proposed feature. 18 | 19 | ### Code Contributions 20 | 21 | If you'd like to contribute code to this project, please follow these steps: 22 | 23 | 1. Fork the repository and create a new branch for your contribution. 24 | 2. Make your changes and ensure that the code follows our coding conventions and style guidelines. 25 | 3. Write tests for your changes, if applicable. 26 | 4. Update the documentation, if necessary. 27 | 5. Commit your changes and push them to your forked repository. 28 | 6. Open a pull request against the main repository, providing a clear description of your changes and their purpose. 29 | 30 | We will review your contribution as soon as possible and provide feedback or merge it into the main codebase if everything looks good. 31 | 32 | Please note that by contributing to this project, you agree to our [Code of Conduct](https://github.com/withstudiocms/.github/blob/main/CODE_OF_CONDUCT.md). 33 | 34 | Thank you for your interest in contributing to this project! 35 | 36 | ## Chat with Us 37 | 38 | We have an active community of developers on the StudioCMS [Discord Server](https://chat.studiocms.dev/). Feel free to join and connect with other contributors, ask questions, or discuss ideas related to this project or other StudioCMS projects. 39 | 40 | ## Our ToolSet 41 | 42 | For an up-to-date list of our main tools check out our [`.prototools`](https://github.com/withstudiocms/ui/blob/main/.prototools) file. 43 | 44 | For more information about Proto checkout [Proto's Website](https://moonrepo.dev/proto). 45 | 46 | ## Licensing 47 | 48 | [MIT Licensed](./LICENSE) - StudioCMS 2024 49 | -------------------------------------------------------------------------------- /packages/studiocms_ui/env.d.ts: -------------------------------------------------------------------------------- 1 | interface CustomEventMap { 2 | createtoast: CustomEvent<import('./src/types/index.ts').ToastProps>; 3 | } 4 | 5 | declare global { 6 | interface Document { 7 | addEventListener<K extends keyof CustomEventMap>( 8 | type: K, 9 | listener: (this: Document, ev: CustomEventMap[K]) => void 10 | ): void; 11 | dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void; 12 | } 13 | } 14 | 15 | declare module 'studiocms:ui/scripts/*' {} 16 | -------------------------------------------------------------------------------- /packages/studiocms_ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@studiocms/ui", 3 | "version": "0.4.17", 4 | "description": "The UI library for StudioCMS. Includes the layouts & components we use to build StudioCMS.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/withstudiocms/ui.git" 8 | }, 9 | "author": { 10 | "name": "Louis Escher", 11 | "url": "https://studiocms.dev" 12 | }, 13 | "contributors": [ 14 | "louisescher", 15 | "TheOtterlord", 16 | "Adammatthiesen", 17 | "JusticeMatthew" 18 | ], 19 | "keywords": [ 20 | "withastro", 21 | "astro-ui-library", 22 | "astro-components", 23 | "studiocms", 24 | "ui-library", 25 | "astro-integration" 26 | ], 27 | "homepage": "https://ui.studiocms.dev", 28 | "publishConfig": { 29 | "access": "public", 30 | "provenance": true 31 | }, 32 | "sideEffects": false, 33 | "files": [ 34 | "dist" 35 | ], 36 | "license": "MIT", 37 | "type": "module", 38 | "scripts": { 39 | "build": "build-scripts build 'src/**/*.{ts,css}' --build-tsconfig", 40 | "dev": "build-scripts dev 'src/**/*.{ts,css}' --build-tsconfig" 41 | }, 42 | "exports": { 43 | ".": { 44 | "types": "./dist/index.d.ts", 45 | "default": "./dist/index.js" 46 | }, 47 | "./toolbar": { 48 | "types": "./dist/toolbar/index.d.ts", 49 | "default": "./dist/toolbar/index.js" 50 | }, 51 | "./components/*": "./dist/components/*", 52 | "./css/*": "./dist/css/*", 53 | "./utils/*": "./dist/utils/*", 54 | "./types": { 55 | "types": "./dist/types/index.d.ts", 56 | "default": "./dist/types/index.js" 57 | } 58 | }, 59 | "dependencies": { 60 | "@iconify-json/heroicons": "catalog:", 61 | "@iconify/types": "^2.0.0", 62 | "astro-transition-event-polyfill": "^1.1.0", 63 | "pathe": "catalog:" 64 | }, 65 | "peerDependencies": { 66 | "astro": "catalog:peers", 67 | "vite": "catalog:peers" 68 | }, 69 | "devDependencies": { 70 | "typescript": "catalog:" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Accordion/Accordion.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './accordion.css'; 3 | 4 | interface Props { 5 | /** 6 | * Whether multiple items in the accordion can be open at once. Default is `true`. 7 | */ 8 | multipleOpen?: boolean; 9 | /** 10 | * The style of the accordion. Default is `outlined` 11 | */ 12 | style?: 'outlined' | 'separated' | 'filled' | 'blank'; 13 | } 14 | 15 | const { style = 'outlined', multipleOpen = true } = Astro.props; 16 | --- 17 | 18 | <div class="sui-accordion" class:list={[style]} data-multiple={multipleOpen}> 19 | <slot /> 20 | </div> 21 | <script> 22 | import 'studiocms:ui/scripts/accordion'; 23 | </script> 24 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Accordion/Item.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Icon from '../Icon/Icon.astro'; 3 | 4 | interface Props { 5 | open?: boolean; 6 | } 7 | 8 | const { open = false } = Astro.props; 9 | --- 10 | 11 | <div class="sui-accordion-item" class:list={{ active: open }} data-open={open}> 12 | <div class="sui-accordion-summary not-content" tabindex="0"> 13 | <Icon name="chevron-right-20-solid" class='sui-summary-chevron' width={20} height={20} /> 14 | <div class="sui-accordion-summary-content"> 15 | <slot name="summary" /> 16 | </div> 17 | </div> 18 | <div class="sui-accordion-details" class:list={{ active: open, open: open, initial: !open }}> 19 | <slot name="default" /> 20 | </div> 21 | </div> 22 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Accordion/accordion.css: -------------------------------------------------------------------------------- 1 | .sui-accordion { 2 | width: 100%; 3 | } 4 | 5 | .sui-accordion-item, 6 | .sui-accordion-details { 7 | margin-top: 0 !important; 8 | } 9 | 10 | .sui-accordion-details { 11 | margin-left: calc(2.25rem); 12 | } 13 | 14 | .sui-accordion-details.initial { 15 | visibility: hidden; 16 | transition: none; 17 | max-height: none; 18 | position: absolute; 19 | top: -200vh; 20 | left: -200vh; 21 | } 22 | 23 | .sui-accordion-details.active { 24 | overflow: hidden; 25 | max-height: 0; 26 | transition: all .3s ease; 27 | position: relative; 28 | bottom: .25rem; 29 | } 30 | 31 | .sui-accordion-details.open { 32 | max-height: none; 33 | } 34 | 35 | .sui-accordion.outlined, 36 | .sui-accordion.filled { 37 | border: 1px solid hsl(var(--border)); 38 | border-radius: var(--radius-sm); 39 | } 40 | 41 | .sui-accordion.filled { 42 | background-color: hsl(var(--background-step-1)); 43 | } 44 | 45 | .sui-accordion.separated .sui-accordion-item, 46 | .sui-accordion.outlined .sui-accordion-item, 47 | .sui-accordion.filled .sui-accordion-item { 48 | border-bottom: 1px solid hsl(var(--border)); 49 | } 50 | 51 | .sui-accordion.separated .sui-accordion-item:first-of-type { 52 | border-top: 1px solid hsl(var(--border)); 53 | } 54 | 55 | .sui-accordion.filled .sui-accordion-item:last-of-type, 56 | .sui-accordion.outlined .sui-accordion-item:last-of-type { 57 | border-bottom: none; 58 | } 59 | 60 | .sui-accordion-summary { 61 | color: hsl(var(--text-normal)); 62 | display: flex; 63 | flex-direction: row; 64 | gap: .5rem; 65 | align-items: center; 66 | padding-left: .5rem; 67 | padding-bottom: .5rem; 68 | padding-top: .5rem; 69 | } 70 | 71 | .sui-summary-chevron { 72 | transition: all .3s ease; 73 | } 74 | 75 | .sui-accordion-item.active .sui-summary-chevron { 76 | transform: rotate(90deg); 77 | } 78 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Badge/Badge.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { StudioCMSColorway } from '../../utils/colors.js'; 3 | import './badge.css'; 4 | import type { HTMLAttributes } from 'astro/types'; 5 | import Icon from '../Icon/Icon.astro'; 6 | import type { HeroIconName } from '../Icon/iconType.js'; 7 | 8 | interface Props extends Omit<HTMLAttributes<'span'>, 'color'> { 9 | /** 10 | * The color of the badge. Defaults to `primary`. 11 | */ 12 | color?: Omit<StudioCMSColorway, 'default'>; 13 | /** 14 | * The icon to display in the badge. 15 | */ 16 | icon?: HeroIconName; 17 | /** 18 | * The icon position. Defaults to `left`. 19 | */ 20 | iconPosition?: 'left' | 'right'; 21 | /** 22 | * The size of the badge. Defaults to `md`. 23 | */ 24 | size?: 'sm' | 'md' | 'lg'; 25 | /** 26 | * The variant of the badge. Defaults to `default`. 27 | */ 28 | variant?: 'default' | 'flat' | 'outline'; 29 | /** 30 | * The rounding of the badge. Full is a half-circle, semi is a quarter-circle. Defaults to `full`. 31 | */ 32 | rounding?: 'full' | 'semi'; 33 | /** 34 | * The label of the badge. 35 | */ 36 | label: string; 37 | } 38 | 39 | const { 40 | color = 'primary', 41 | icon, 42 | size = 'md', 43 | variant = 'default', 44 | rounding = 'full', 45 | iconPosition = 'left', 46 | label, 47 | class: className, 48 | ...props 49 | } = Astro.props; 50 | 51 | let iconSize = 16; 52 | if (size === 'sm') { 53 | iconSize = 8; 54 | } else if (size === 'lg') { 55 | iconSize = 24; 56 | } 57 | --- 58 | 59 | <span class="sui-badge" class:list={[color, size, variant, rounding, className]} {...props}> 60 | {icon && iconPosition === 'left' && <Icon name={icon} width={iconSize} height={iconSize} />} 61 | {label} 62 | {icon && iconPosition === 'right' && <Icon name={icon} width={iconSize} height={iconSize} />} 63 | </span> 64 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Breadcrumbs/Breadcrumbs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './breadcrumbs.css'; 3 | 4 | type Props = { 5 | segments: { 6 | label: string; 7 | segment: string; 8 | }[]; 9 | separator?: string; 10 | }; 11 | 12 | const { segments, separator = '>' } = Astro.props; 13 | 14 | const breadcrumbs = segments.map(({ label }, index) => { 15 | const path = segments 16 | .slice(0, index + 1) 17 | .map(({ segment }) => segment) 18 | .join('/') 19 | .replaceAll('//', '/'); 20 | return { 21 | label, 22 | path, 23 | }; 24 | }); 25 | --- 26 | 27 | <div class="sui-breadcrumbs"> 28 | {breadcrumbs.map(({ label, path }) => ( 29 | <a href={path || '/'}>{label}</a><span>{separator}</span> 30 | ))} 31 | </div> 32 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Breadcrumbs/breadcrumbs.css: -------------------------------------------------------------------------------- 1 | .sui-breadcrumbs { 2 | display: flex; 3 | flex-direction: row; 4 | gap: .25rem; 5 | } 6 | 7 | .sui-breadcrumbs > *:last-child { 8 | display: none; 9 | } 10 | 11 | .sui-breadcrumbs a { 12 | color: hsl(var(--primary-base)); 13 | text-decoration: none; 14 | } 15 | 16 | .sui-breadcrumbs a:hover { 17 | text-decoration: underline; 18 | } 19 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Button/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLTag, Polymorphic } from 'astro/types'; 3 | import type { StudioCMSColorway } from '../../utils/colors.js'; 4 | import './button.css'; 5 | 6 | /** 7 | * Props for the button component. 8 | */ 9 | type Props<As extends HTMLTag = 'button'> = Omit<Polymorphic<{ as: As }>, 'as'> & { 10 | /** 11 | * The polymorphic component to render the button as. Defaults to `button`. 12 | */ 13 | as?: As; 14 | /** 15 | * The size of the button. Defaults to `md`. 16 | */ 17 | size?: 'sm' | 'md' | 'lg'; 18 | /** 19 | * Whether the button should be full width. Defaults to `false`. 20 | */ 21 | fullWidth?: boolean; 22 | /** 23 | * The colorway of the button. Defaults to `default`. 24 | */ 25 | color?: StudioCMSColorway; 26 | /** 27 | * The variant of the button. Defaults to `solid`. 28 | */ 29 | variant?: 'solid' | 'outlined' | 'flat'; 30 | /** 31 | * Whether the button is disabled. Defaults to `false`. 32 | */ 33 | disabled?: boolean; 34 | /** 35 | * An optional href to use for the button. If provided, the button will render as an anchor tag. 36 | */ 37 | href?: string; 38 | }; 39 | 40 | export type { Props }; 41 | 42 | const { 43 | size = 'md', 44 | fullWidth = false, 45 | color = 'default', 46 | variant = 'solid', 47 | disabled = false, 48 | ...props 49 | } = Astro.props; 50 | 51 | let As: HTMLTag = 'button'; 52 | 53 | if ('href' in props) { 54 | As = 'a'; 55 | } else { 56 | As = props.as || 'button'; 57 | } 58 | --- 59 | 60 | <As 61 | class="sui-button" 62 | class:list={[ 63 | fullWidth && "full", 64 | disabled && "disabled", 65 | size, 66 | color, 67 | variant, 68 | ]} 69 | disabled={disabled} 70 | tabindex={0} 71 | {...props}> 72 | <slot name="start-content" /> 73 | <slot /> 74 | <slot name="end-content" /> 75 | </As> 76 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Card/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLTag, Polymorphic } from 'astro/types'; 3 | import './card.css'; 4 | 5 | /** 6 | * Props for the card component. 7 | */ 8 | type Props<As extends HTMLTag = 'div'> = Omit<Polymorphic<{ as: As }>, 'as'> & { 9 | /** 10 | * The polymorphic component to render the card as. Defaults to `div`. 11 | */ 12 | as?: As; 13 | /** 14 | * Whether the card should be full width. Defaults to `false`. 15 | */ 16 | fullWidth?: boolean; 17 | /** 18 | * Whether the card should be full height. Defaults to `false`. 19 | */ 20 | fullHeight?: boolean; 21 | /** 22 | * The variant of the card. Defaults to `default`. 23 | */ 24 | variant?: 'default' | 'filled'; 25 | }; 26 | 27 | const { as: As = 'div', fullWidth, fullHeight, variant = 'default', ...props } = Astro.props; 28 | --- 29 | <As class="sui-card" class:list={[fullWidth && "full-w", fullHeight && "full-h", variant]} {...props}> 30 | <div class="sui-card-header"> 31 | <slot name="header" /> 32 | </div> 33 | <div class="sui-card-body"> 34 | <slot /> 35 | </div> 36 | <div class="sui-card-footer"> 37 | <slot name="footer" /> 38 | </div> 39 | </As> 40 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Card/card.css: -------------------------------------------------------------------------------- 1 | .sui-card { 2 | border: 1px solid hsl(var(--border)); 3 | background-color: hsl(var(--background-base)); 4 | border-radius: var(--radius-md); 5 | width: fit-content; 6 | height: fit-content; 7 | } 8 | 9 | .sui-card.filled { 10 | background-color: hsl(var(--background-step-3)); 11 | border: none; 12 | } 13 | 14 | .sui-card.full-w { 15 | width: 100%; 16 | } 17 | 18 | .sui-card.full-h { 19 | height: 100%; 20 | } 21 | 22 | .sui-card-header:has(*) { 23 | padding: 1rem 1rem 0rem 1rem; 24 | * { 25 | margin: 0; 26 | } 27 | } 28 | 29 | .sui-card-body { 30 | padding: 1rem; 31 | } 32 | 33 | .sui-card-footer:has(*) { 34 | border-top: 1px solid hsl(var(--border)); 35 | padding: 1rem; 36 | } 37 | 38 | .sui-card-footer:not(:has(*)) { 39 | display: none; 40 | } 41 | 42 | .filled .sui-card-footer { 43 | border: none; 44 | } 45 | 46 | @media screen and (max-width: 840px) { 47 | .sui-card { 48 | width: 100%; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Center/Center.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './center.css'; 3 | --- 4 | <!-- This was made for Jumper, to keep him happy <3 --> 5 | <div class="sui-center"> 6 | <slot /> 7 | </div> 8 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Center/center.css: -------------------------------------------------------------------------------- 1 | .sui-center { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | 6 | width: 100%; 7 | height: 100%; 8 | 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Checkbox/Checkbox.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Checkmark from '../../icons/Checkmark.astro'; 3 | import type { StudioCMSColorway } from '../../utils/colors.js'; 4 | import { generateID } from '../../utils/generateID.js'; 5 | import './checkbox.css'; 6 | 7 | /** 8 | * The props for the Checkbox component. 9 | */ 10 | interface Props { 11 | /** 12 | * The label of the checkbox. 13 | */ 14 | label: string; 15 | /** 16 | * The size of the checkbox. Defaults to `md`. 17 | */ 18 | size?: 'sm' | 'md' | 'lg'; 19 | /** 20 | * The color of the checkbox. Defaults to `default`. 21 | */ 22 | color?: StudioCMSColorway; 23 | /** 24 | * Whether the checkbox is checked by default. 25 | */ 26 | defaultChecked?: boolean; 27 | /** 28 | * Whether the checkbox is disabled. 29 | */ 30 | disabled?: boolean; 31 | /** 32 | * The name of the checkbox. 33 | */ 34 | name?: string; 35 | /** 36 | * Whether the checkbox is required. 37 | */ 38 | isRequired?: boolean; 39 | /** 40 | * The value of the checkbox. 41 | */ 42 | value?: string; 43 | } 44 | 45 | const { 46 | size = 'md', 47 | color = 'default', 48 | defaultChecked, 49 | disabled, 50 | name = generateID('checkbox'), 51 | label, 52 | isRequired, 53 | value, 54 | } = Astro.props; 55 | 56 | const iconSizes = { 57 | sm: 14, 58 | md: 16, 59 | lg: 24, 60 | }; 61 | --- 62 | <label 63 | class="sui-checkmark-label" 64 | for={name} 65 | class:list={[ 66 | disabled && "disabled", 67 | color, 68 | size, 69 | ]} 70 | > 71 | <div 72 | class="sui-checkmark-container" 73 | tabindex="0" 74 | role="checkbox" 75 | aria-checked={defaultChecked} 76 | aria-labelledby={`label-${name}`} 77 | > 78 | <Checkmark 79 | class={'sui-checkmark'} 80 | width={iconSizes[size]} 81 | height={iconSizes[size]} 82 | /> 83 | <input 84 | type="checkbox" 85 | name={name} 86 | id={name} 87 | checked={defaultChecked} 88 | disabled={disabled} 89 | required={isRequired} 90 | class="sui-checkbox" 91 | value={value} 92 | hidden 93 | /> 94 | </div> 95 | <span id={`label-${name}`}> 96 | {label} <span class="req-star">{isRequired && "*"}</span> 97 | </span> 98 | </label> 99 | <script> 100 | import "studiocms:ui/scripts/checkbox" 101 | </script> 102 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Checkbox/checkbox.ts: -------------------------------------------------------------------------------- 1 | function loadCheckboxes() { 2 | const allElements = document.querySelectorAll<HTMLDivElement>('.sui-checkmark-container'); 3 | const allCheckbox = document.querySelectorAll<HTMLInputElement>('.sui-checkbox'); 4 | 5 | for (const element of allElements) { 6 | if (element.dataset.initialized) continue; 7 | 8 | element.dataset.initialized = 'true'; 9 | 10 | element.addEventListener('keydown', (e) => { 11 | if (e.key !== 'Enter' && e.key !== ' ') return; 12 | 13 | e.preventDefault(); 14 | 15 | const checkbox = element.querySelector<HTMLInputElement>('.sui-checkbox'); 16 | 17 | if (!checkbox) return; 18 | 19 | checkbox.click(); 20 | }); 21 | } 22 | 23 | for (const box of allCheckbox) { 24 | if (box.dataset.initialized) continue; 25 | 26 | box.dataset.initialized = 'true'; 27 | 28 | box.addEventListener('change', (e) => { 29 | box.parentElement!.ariaChecked = (e.target as HTMLInputElement).checked ? 'true' : 'false'; 30 | }); 31 | } 32 | } 33 | 34 | document.addEventListener('astro:page-load', loadCheckboxes); 35 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Divider/Divider.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './divider.css'; 3 | 4 | /** 5 | * Props for the divider component. 6 | */ 7 | interface Props { 8 | /** 9 | * The background color of the divider, used to "hide" content behind the slot. Defaults to `background-base`. 10 | */ 11 | background?: 'background-base' | 'background-step-1' | 'background-step-2' | 'background-step-3'; 12 | } 13 | 14 | const hasDefaultSlot = Astro.slots.has('default'); 15 | 16 | const { background = 'background-base' } = Astro.props; 17 | --- 18 | <div class="sui-divider-container"> 19 | <hr class="sui-divider-line" /> 20 | <div 21 | class="sui-divider-content" 22 | style={`background-color: hsl(var(--${background})); padding: ${hasDefaultSlot ? '.25rem .5rem;' : '0'}`} 23 | > 24 | <slot /> 25 | </div> 26 | </div> 27 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Divider/divider.css: -------------------------------------------------------------------------------- 1 | .sui-divider-container { 2 | display: flex; 3 | width: 100%; 4 | align-items: center; 5 | justify-content: center; 6 | position: relative; 7 | } 8 | 9 | .sui-divider-line { 10 | position: absolute; 11 | left: 50%; 12 | transform: translate(-50%, 0); 13 | width: 100%; 14 | height: 1px; 15 | background-color: hsl(var(--border)); 16 | z-index: 1; 17 | border: none; 18 | } 19 | 20 | .sui-divider-content { 21 | z-index: 2; 22 | color: hsl(var(--text-muted)); 23 | } 24 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Footer/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | import './footer.css'; 4 | 5 | interface FooterLink { 6 | /** 7 | * The label of the link. 8 | */ 9 | label: string; 10 | /** 11 | * The href of the link. 12 | */ 13 | href: string; 14 | } 15 | 16 | /** 17 | * The props for the footer component. 18 | */ 19 | interface Props extends HTMLAttributes<'footer'> { 20 | /** 21 | * The links to display in the footer. 22 | */ 23 | links: { 24 | [label: string]: FooterLink[]; 25 | }; 26 | /** 27 | * The copyright text to display in the footer. 28 | */ 29 | copyright: string; 30 | } 31 | 32 | const { copyright, class: className, links, ...props } = Astro.props; 33 | --- 34 | 35 | <footer {...props} class={`${className} sui-footer`}> 36 | <div class="upper"> 37 | <div> 38 | <slot name="brand" /> 39 | </div> 40 | <div class="links"> 41 | {Object.keys(links).map((groupLabel) => ( 42 | <ul> 43 | <li class="sui-footer-link-label">{groupLabel}</li> 44 | {links[groupLabel]!.map((item) => ( 45 | <li> 46 | <a href={item.href}>{item.label}</a> 47 | </li> 48 | ))} 49 | </ul> 50 | ))} 51 | </div> 52 | </div> 53 | <hr class="separator" /> 54 | <div class="lower"> 55 | <span class="copyright-span">{copyright}</span> 56 | <slot name="socials" /> 57 | </div> 58 | </footer> 59 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Footer/footer.css: -------------------------------------------------------------------------------- 1 | footer.sui-footer { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 2rem; 5 | background-color: hsl(var(--background-step-1)); 6 | padding: 2rem 10vw; 7 | color: hsl(var(--text-normal)) !important; 8 | } 9 | 10 | .upper, 11 | .lower { 12 | display: flex; 13 | flex-direction: row; 14 | justify-content: space-between; 15 | } 16 | 17 | .links { 18 | display: flex; 19 | justify-content: flex-end; 20 | flex-direction: row; 21 | flex-wrap: wrap; 22 | gap: 2rem; 23 | } 24 | 25 | .links ul { 26 | list-style-type: none; 27 | margin: 0 !important; 28 | padding: 0 !important; 29 | } 30 | 31 | .links ul li, 32 | .links ul li * { 33 | color: hsl(var(--text-normal)) !important; 34 | width: fit-content; 35 | } 36 | 37 | .links ul li:has(a):hover { 38 | text-decoration: underline; 39 | } 40 | 41 | .sui-footer-link-label { 42 | font-size: 1.125em; 43 | font-weight: 700; 44 | } 45 | 46 | .separator { 47 | height: 1px; 48 | width: 100%; 49 | border: none; 50 | background: hsl(var(--border)); 51 | } 52 | 53 | .lower { 54 | align-items: center; 55 | flex-wrap: wrap; 56 | gap: 1rem; 57 | } 58 | 59 | @media screen and (max-width: 1440px) { 60 | footer.sui-footer { 61 | padding: 2rem; 62 | } 63 | } 64 | 65 | @media screen and (max-width: 1280px) { 66 | .upper { 67 | flex-direction: column; 68 | gap: 2rem; 69 | } 70 | 71 | .links { 72 | justify-content: flex-start; 73 | } 74 | } 75 | 76 | @media screen and (max-width: 640px) { 77 | .links ul { 78 | width: calc(50% - 1rem); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Group/Group.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './group.css'; 3 | --- 4 | 5 | <div class="sui-group"> 6 | <slot /> 7 | </div> 8 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Group/group.css: -------------------------------------------------------------------------------- 1 | .sui-group { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | } 6 | 7 | .sui-group .sui-button:not(:first-child):not(:last-child), 8 | .sui-group .sui-badge:not(:first-child):not(:last-child) { 9 | border-radius: 0; 10 | } 11 | 12 | .sui-group .sui-button:first-child, 13 | .sui-group .sui-badge:first-child { 14 | border-top-right-radius: 0; 15 | border-bottom-right-radius: 0; 16 | } 17 | 18 | .sui-group .sui-button:last-child, 19 | .sui-group .sui-badge:last-child { 20 | border-top-left-radius: 0; 21 | border-bottom-left-radius: 0; 22 | } 23 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Icon/Icon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { icons } from '@iconify-json/heroicons'; 3 | import type { HTMLAttributes } from 'astro/types'; 4 | import IconBase from './IconBase.astro'; 5 | import { type HeroIconName } from './iconType.js'; 6 | 7 | interface Props extends HTMLAttributes<'svg'> { 8 | name: HeroIconName; 9 | height?: number; 10 | width?: number; 11 | } 12 | --- 13 | <IconBase iconCollection={icons} {...Astro.props} /> 14 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Icon/IconBase.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { IconifyJSON } from '@iconify/types'; 3 | import { AstroError } from 'astro/errors'; 4 | import type { HTMLAttributes } from 'astro/types'; 5 | import { getIconData, iconToSVG, replaceIDs } from '../../utils/iconifyUtils.js'; 6 | 7 | interface Props extends HTMLAttributes<'svg'> { 8 | /** 9 | * Collection of icons 10 | * 11 | * Must be an `IconifyJSON` object from an Iconify JSON collection 12 | * 13 | * @example 14 | * ```tsx 15 | * --- 16 | * import { icons } from '@iconify-json/heroicons'; 17 | * import type { HTMLAttributes } from 'astro/types'; 18 | * import { IconBase } from 'studiocms:ui/components'; 19 | * 20 | * interface Props extends Omit<HTMLAttributes<'svg'>, 'name'> { 21 | * name: keyof typeof icons; 22 | * height?: number; 23 | * width?: number; 24 | * } 25 | * 26 | * const { name, ...props } = Astro.props; 27 | * --- 28 | * <IconBase iconCollection={icons} {name} {...props} /> 29 | * ``` 30 | */ 31 | iconCollection: IconifyJSON; 32 | /** 33 | * Name of the icon from the collection 34 | */ 35 | name: string; 36 | /** 37 | * Height of the icon in pixels 38 | */ 39 | height?: number; 40 | /** 41 | * Width of the icon in pixels 42 | */ 43 | width?: number; 44 | } 45 | 46 | const { iconCollection, name, ...props } = Astro.props; 47 | 48 | interface SVGAttributes extends HTMLAttributes<'svg'> { 49 | // biome-ignore lint/suspicious/noExplicitAny: Allow any string index 50 | [key: string]: any; 51 | } 52 | 53 | const attributes = props as SVGAttributes; 54 | const iconData = getIconData(iconCollection, name); 55 | 56 | if (!iconData) { 57 | throw new AstroError(`Icon "${name}" is missing in collection`); 58 | } 59 | 60 | const renderData = iconToSVG(iconData, { 61 | height: attributes.height || 24, 62 | width: attributes.width || 24, 63 | }); 64 | 65 | const body = replaceIDs(renderData.body); 66 | 67 | let renderAttribsHTML = 68 | body.indexOf('xlink:') === -1 ? '' : ' xmlns:xlink="http://www.w3.org/1999/xlink"'; 69 | 70 | for (const attr in attributes) { 71 | renderAttribsHTML += ` ${attr}="${attributes[attr]}"`; 72 | } 73 | 74 | const viewBox = renderData.attributes.viewBox; 75 | 76 | const svg = `<svg style="min-width: ${attributes.height || 24}px" xmlns="http://www.w3.org/2000/svg"${renderAttribsHTML}${viewBox && ` viewbox="${viewBox}"`}>${body}</svg>`; 77 | --- 78 | <Fragment set:html={svg} /> -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Icon/iconType.ts: -------------------------------------------------------------------------------- 1 | import type { icons as heroIcons } from '@iconify-json/heroicons/icons.json'; 2 | 3 | export type HeroIconName = keyof typeof heroIcons; 4 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Input/Input.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | import { generateID } from '../../utils/generateID.js'; 4 | import './input.css'; 5 | 6 | /** 7 | * The props for the input component. 8 | */ 9 | interface Props extends HTMLAttributes<'input'> { 10 | /** 11 | * The label of the input. 12 | */ 13 | label?: string; 14 | /** 15 | * The type of the input. Defaults to `text`. 16 | */ 17 | type?: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search'; 18 | /** 19 | * The placeholder of the input. 20 | */ 21 | placeholder?: string; 22 | /** 23 | * Whether the input is required. Defaults to `false`. 24 | */ 25 | isRequired?: boolean; 26 | /** 27 | * The name attribute for the input. Useful for form submission. 28 | */ 29 | name?: string; 30 | /** 31 | * Whether the input is disabled. Defaults to `false`. 32 | */ 33 | disabled?: boolean; 34 | /** 35 | * The default value of the input. 36 | */ 37 | defaultValue?: string; 38 | /** 39 | * Additional classes to apply to the input. 40 | */ 41 | class?: string; 42 | } 43 | 44 | const { 45 | label, 46 | placeholder, 47 | name = generateID('input'), 48 | type = 'text', 49 | defaultValue, 50 | isRequired = false, 51 | disabled = false, 52 | class: className, 53 | ...props 54 | } = Astro.props; 55 | --- 56 | 57 | <label for={name} class="sui-input-label" class:list={[disabled && "disabled"]}> 58 | {label && ( 59 | <span class="label"> 60 | {label} <span class="req-star">{isRequired && "*"}</span> 61 | </span> 62 | )} 63 | <input 64 | placeholder={placeholder} 65 | name={name} 66 | id={name} 67 | type={type} 68 | class="sui-input" 69 | class:list={[className]} 70 | required={isRequired} 71 | disabled={disabled} 72 | value={defaultValue} 73 | {...props} 74 | /> 75 | </label> 76 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Input/input.css: -------------------------------------------------------------------------------- 1 | .sui-input-label { 2 | width: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | gap: .25rem; 6 | } 7 | 8 | .sui-input-label.disabled { 9 | opacity: 0.5; 10 | pointer-events: none; 11 | color: hsl(var(--text-muted)); 12 | } 13 | 14 | .label { 15 | font-size: 14px; 16 | } 17 | 18 | .sui-input { 19 | padding: .5rem 1rem; 20 | border-radius: var(--radius-md); 21 | border: 1px solid hsl(var(--border)); 22 | background: hsl(var(--background-step-2)); 23 | color: hsl(var(--text-normal)); 24 | transition: all .15s ease; 25 | } 26 | 27 | .sui-input:hover { 28 | background: hsl(var(--background-step-3)); 29 | } 30 | 31 | .sui-input:active, 32 | .sui-input:focus { 33 | border: 1px solid hsl(var(--primary-base)); 34 | outline: none; 35 | background: hsl(var(--background-step-2)); 36 | } 37 | 38 | .disabled .sui-input:active { 39 | border: 1px solid hsl(var(--border)); 40 | } 41 | 42 | .req-star { 43 | color: hsl(var(--danger-base)); 44 | font-weight: 700; 45 | } 46 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Modal/modal.css: -------------------------------------------------------------------------------- 1 | .sui-modal { 2 | border: 1px solid hsl(var(--border)); 3 | border-radius: var(--radius-md); 4 | padding: 1.5rem; 5 | box-shadow: 0px 6px 8px hsl(var(--shadow)); 6 | animation: hide .25s ease; 7 | overflow: visible; 8 | margin: auto; 9 | z-index: 50; 10 | max-width: calc(100% - 4rem); 11 | } 12 | 13 | .sui-modal.sm { 14 | width: 384px; 15 | } 16 | 17 | .sui-modal.md { 18 | width: 448px; 19 | } 20 | 21 | .sui-modal.lg { 22 | width: 608px; 23 | } 24 | 25 | .sui-modal[open] { 26 | animation: show .25s ease-in-out; 27 | } 28 | 29 | html:has(.sui-modal[open]), 30 | body:has(.sui-modal[open]) { 31 | overflow: hidden; 32 | } 33 | 34 | .sui-modal[open]::backdrop { 35 | background-color: rgba(0, 0, 0, 0.75); 36 | animation: backdrop .3s ease-in-out forwards; 37 | } 38 | 39 | .sui-modal-header:has(*) { 40 | margin-bottom: 1rem; 41 | 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: space-between; 45 | gap: 1rem; 46 | 47 | * { 48 | margin: 0; 49 | } 50 | } 51 | 52 | .x-mark-container { 53 | cursor: pointer; 54 | height: 1.5rem; 55 | width: 1.5rem; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | transition: background-color .15s ease; 60 | border-radius: var(--radius-sm); 61 | } 62 | 63 | .x-mark-container:hover { 64 | background-color: hsl(var(--default-base)); 65 | } 66 | 67 | .x-mark-container:focus-visible { 68 | outline: 2px solid hsl(var(--text-normal)); 69 | outline-offset: 2px; 70 | } 71 | 72 | .sui-modal-footer { 73 | display: none; 74 | } 75 | 76 | .sui-modal-footer:has(*) { 77 | display: flex; 78 | flex-direction: row; 79 | gap: 1rem; 80 | margin-top: 1rem; 81 | justify-content: end; 82 | } 83 | 84 | @keyframes hide { 85 | 0% { 86 | scale: 1; 87 | opacity: 1; 88 | display: block; 89 | } 90 | 100% { 91 | scale: 0.85; 92 | opacity: 0; 93 | display: none; 94 | } 95 | } 96 | 97 | @keyframes show { 98 | 0% { 99 | scale: 0.85; 100 | opacity: 0; 101 | display: none; 102 | } 103 | 100% { 104 | scale: 1; 105 | opacity: 1; 106 | display: block; 107 | } 108 | } 109 | 110 | @keyframes backdrop { 111 | 0% { 112 | opacity: 0; 113 | } 114 | 100% { 115 | opacity: 1; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Progress/Progress.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { StudioCMSColorway } from '../../utils/colors.js'; 3 | import './progress.css'; 4 | import type { HTMLAttributes } from 'astro/types'; 5 | 6 | type Props = { 7 | id: string; 8 | value?: number; 9 | max?: number; 10 | color?: Omit<StudioCMSColorway, 'default'>; 11 | } & HTMLAttributes<'div'>; 12 | 13 | const { id, value = 0, max = 100, color = 'primary' } = Astro.props; 14 | --- 15 | 16 | <div class="sui-progress" class:list={[color]} role="progressbar" id={id} data-max={max} data-value={value}> 17 | <div class="sui-progress-slider" /> 18 | </div> 19 | <script> 20 | import 'studiocms:ui/scripts/progress'; 21 | </script> 22 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Progress/helper.ts: -------------------------------------------------------------------------------- 1 | class ProgressHelper { 2 | private bar: HTMLDivElement; 3 | private progress: HTMLElement; 4 | 5 | private value: number; 6 | private max: number; 7 | 8 | constructor(id: string) { 9 | this.bar = document.getElementById(id) as HTMLDivElement; 10 | this.progress = this.bar.firstElementChild as HTMLElement; 11 | 12 | this.value = this.getValue(); 13 | this.max = this.getMax(); 14 | } 15 | 16 | getValue() { 17 | return Number.parseInt(this.bar.dataset.value as string, 10); 18 | } 19 | 20 | setValue(value: number) { 21 | const max = Number.parseInt(this.bar.dataset.max as string, 10); 22 | const percent = Math.round((value / max) * 100); 23 | this.progress.style.width = `${percent}%`; 24 | } 25 | 26 | getMax() { 27 | return Number.parseInt(this.bar.dataset.max as string, 10); 28 | } 29 | 30 | setMax(value: number) { 31 | this.bar.dataset.max = value.toString(); 32 | } 33 | 34 | getPercentage() { 35 | return Math.round((this.value / this.max) * 100); 36 | } 37 | } 38 | 39 | export { ProgressHelper }; 40 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Progress/progress.css: -------------------------------------------------------------------------------- 1 | .sui-progress { 2 | width: 100%; 3 | height: 1rem; 4 | border: 1px solid hsl(var(--border)); 5 | border-radius: var(--radius-sm); 6 | overflow: hidden; 7 | } 8 | 9 | .sui-progress-slider { 10 | height: 100%; 11 | width: 0; 12 | transition: all .75s ease; 13 | border-radius: var(--radius-sm); 14 | background-color: hsl(var(--primary-base)); 15 | } 16 | 17 | .sui-progress.success .sui-progress-slider { 18 | background-color: hsl(var(--success-base)); 19 | } 20 | 21 | .sui-progress.warning .sui-progress-slider { 22 | background-color: hsl(var(--warning-base)); 23 | } 24 | 25 | .sui-progress.danger .sui-progress-slider { 26 | background-color: hsl(var(--danger-base)); 27 | } 28 | 29 | .sui-progress.info .sui-progress-slider { 30 | background-color: hsl(var(--info-base)); 31 | } 32 | 33 | .sui-progress.monochrome .sui-progress-slider { 34 | background-color: hsl(var(--mono-base)); 35 | } 36 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Progress/progress.ts: -------------------------------------------------------------------------------- 1 | function loadProgressBars() { 2 | const allBars = document.querySelectorAll<HTMLDivElement>('.sui-progress'); 3 | 4 | for (const bar of allBars) { 5 | const value = bar.dataset.value; 6 | const max = bar.dataset.max; 7 | 8 | const progress = bar.firstElementChild as HTMLElement; 9 | 10 | if (value && max) { 11 | const percent = Math.round((Number.parseInt(value, 10) / Number.parseInt(max, 10)) * 100); 12 | progress.style.width = `${percent}%`; 13 | } 14 | } 15 | } 16 | 17 | document.addEventListener('astro:page-load', loadProgressBars); 18 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/RadioGroup/radiogroup.ts: -------------------------------------------------------------------------------- 1 | function loadRadioGroups() { 2 | const AllRadioGroupContainers = document.querySelectorAll<HTMLDivElement>('.sui-radio-container'); 3 | 4 | for (const element of AllRadioGroupContainers) { 5 | if (element.dataset.initialized) continue; 6 | 7 | element.dataset.initialized = 'true'; 8 | 9 | const radioBoxes = element.querySelectorAll<HTMLDivElement>('.sui-radio-box'); 10 | 11 | let i = 0; 12 | 13 | for (const radioBox of radioBoxes) { 14 | radioBox.addEventListener('keydown', (e) => { 15 | if (e.key === 'Enter' || e.key === ' ') { 16 | e.preventDefault(); 17 | 18 | const input = ( 19 | e.target as HTMLDivElement 20 | ).parentElement!.parentElement!.querySelector<HTMLInputElement>('.sui-radio-toggle')!; 21 | 22 | if (input.disabled) return; 23 | 24 | input.checked = true; 25 | } 26 | 27 | if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { 28 | e.preventDefault(); 29 | 30 | let nextRadioBox: HTMLDivElement | undefined; 31 | 32 | radioBoxes.forEach((box, index) => { 33 | if (box === radioBox) nextRadioBox = radioBoxes[index + 1]; 34 | }); 35 | 36 | if (!nextRadioBox) return; 37 | 38 | radioBox.tabIndex = -1; 39 | nextRadioBox.tabIndex = 0; 40 | nextRadioBox.focus(); 41 | nextRadioBox.click(); 42 | } 43 | 44 | if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { 45 | e.preventDefault(); 46 | 47 | let previousRadioBox: HTMLDivElement | undefined; 48 | 49 | radioBoxes.forEach((box, index) => { 50 | if (box === radioBox) previousRadioBox = radioBoxes[index - 1]; 51 | }); 52 | 53 | if (!previousRadioBox) return; 54 | 55 | radioBox.tabIndex = -1; 56 | previousRadioBox.tabIndex = 0; 57 | previousRadioBox.focus(); 58 | previousRadioBox.click(); 59 | } 60 | }); 61 | 62 | i++; 63 | } 64 | element.addEventListener('keydown', (e) => { 65 | if (e.key !== 'Enter') return; 66 | 67 | const checkbox = element.querySelector<HTMLInputElement>('.sui-checkbox'); 68 | 69 | if (!checkbox) return; 70 | 71 | checkbox.click(); 72 | }); 73 | } 74 | } 75 | 76 | document.addEventListener('astro:page-load', loadRadioGroups); 77 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Row/Row.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | import './row.css'; 4 | 5 | /** 6 | * The props for the row component. 7 | */ 8 | interface Props extends HTMLAttributes<'div'> { 9 | /** 10 | * Whether the row should be aligned to the center. Defaults to `false`. 11 | */ 12 | alignCenter?: boolean; 13 | /** 14 | * The size of the gap between the children. Defaults to `md`. 15 | */ 16 | gapSize?: 'sm' | 'md' | 'lg'; 17 | } 18 | 19 | const { alignCenter, gapSize = 'md', ...props } = Astro.props; 20 | --- 21 | 22 | <div class="sui-row" class:list={[alignCenter && "align", gapSize]} {...props}> 23 | <slot /> 24 | </div> 25 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Row/row.css: -------------------------------------------------------------------------------- 1 | .sui-row { 2 | display: flex; 3 | flex-direction: row; 4 | position: relative; 5 | flex-wrap: wrap; 6 | } 7 | 8 | .sui-row.align { 9 | align-items: center; 10 | } 11 | 12 | .sui-row.sm { 13 | gap: .5rem; 14 | } 15 | 16 | .sui-row.md { 17 | gap: 1rem; 18 | } 19 | 20 | .sui-row.lg { 21 | gap: 2rem; 22 | } 23 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Sidebar/Double.astro: -------------------------------------------------------------------------------- 1 | <div id="sui-sidebars" class="active inner"> 2 | <div id="sui-sidebar-outer"> 3 | <slot name="outer" /> 4 | </div> 5 | <div id="sui-sidebar-inner"> 6 | <slot name="inner" /> 7 | </div> 8 | </div> 9 | <style> 10 | #sui-sidebars { 11 | --sidebars-container-width: calc((280px + 1px) * 2); 12 | display: flex; 13 | align-items: center; 14 | width: var(--sidebars-container-width); 15 | min-width: var(--sidebars-container-width); 16 | overflow: hidden; 17 | transition: all .3s ease; 18 | z-index: 10; 19 | height: 100%; 20 | } 21 | 22 | #sui-sidebars.active { 23 | transform: translateX(0%); 24 | } 25 | 26 | #sui-sidebar-outer { 27 | height: 100%; 28 | min-width: 280px; 29 | width: 280px; 30 | background-color: hsl(var(--background-step-1)); 31 | border-right: 1px solid hsl(var(--border)); 32 | gap: 1rem; 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | justify-content: center; 37 | z-index: 10; 38 | transition: all .3s ease; 39 | padding: 1.5rem; 40 | } 41 | 42 | #sui-sidebar-inner { 43 | min-width: 280px; 44 | width: 280px; 45 | height: 100%; 46 | background-color: hsl(var(--background-step-2)); 47 | border-right: 1px solid hsl(var(--border)); 48 | display: flex; 49 | flex-direction: column; 50 | gap: 1rem; 51 | align-items: center; 52 | justify-content: center; 53 | z-index: 5; 54 | position: relative; 55 | transition: all .3s ease; 56 | } 57 | 58 | @media screen and (max-width: 1200px) { 59 | #sui-sidebars { 60 | --sidebars-container-width: calc(280px + 1px); 61 | } 62 | 63 | #sui-sidebars.inner { 64 | #sui-sidebar-outer, 65 | #sui-sidebar-inner { 66 | transform: translateX(-100%); 67 | } 68 | } 69 | } 70 | 71 | @media screen and (max-width: 840px) { 72 | #sui-sidebars { 73 | transform: translateX(-100%); 74 | position: absolute; 75 | top: 0; 76 | left: 0; 77 | height: 100%; 78 | width: 100%; 79 | } 80 | 81 | #sui-sidebar-outer, 82 | #sui-sidebar-inner { 83 | width: 100%; 84 | flex: 0 0 100%; 85 | } 86 | } 87 | </style> 88 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Sidebar/Single.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends Exclude<HTMLAttributes<'aside'>, 'id'> {} 5 | 6 | const props = Astro.props; 7 | --- 8 | <aside id="sui-sidebar" {...props}> 9 | <slot /> 10 | </aside> 11 | <style> 12 | #sui-sidebar { 13 | height: 100%; 14 | min-width: 280px; 15 | width: 280px; 16 | background-color: hsl(var(--background-step-1)); 17 | border-right: 1px solid hsl(var(--border)); 18 | gap: 1rem; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | padding: 1.5rem; 23 | z-index: 10; 24 | transition: all .3s ease; 25 | } 26 | 27 | #sui-sidebar.active { 28 | transform: translateX(0%); 29 | } 30 | 31 | @media screen and (max-width: 840px) { 32 | #sui-sidebar { 33 | transform: translateX(-100%); 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | height: 100%; 38 | width: 100%; 39 | border-right: none; 40 | } 41 | } 42 | </style> 43 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Tabs/TabItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { StudioCMSColorway } from '../../utils/colors.js'; 3 | import { generateID } from '../../utils/generateID.js'; 4 | import type { HeroIconName } from '../Icon/iconType.js'; 5 | 6 | /** 7 | * The props for the TabItem component. 8 | */ 9 | interface Props { 10 | /** 11 | * The icon to display next to the tab. 12 | */ 13 | icon?: HeroIconName; 14 | /** 15 | * The label of the tab. 16 | */ 17 | label: string; 18 | /** 19 | * The color of the tab. Defaults to `primary`. 20 | */ 21 | color?: Exclude<StudioCMSColorway, 'default'>; 22 | } 23 | 24 | const id = generateID('tab'); 25 | 26 | const { icon, label, color = 'primary' } = Astro.props; 27 | --- 28 | <sui-tab-item 29 | data-icon={icon} 30 | data-label={label} 31 | data-color={color} 32 | data-tab-id={id} 33 | > 34 | <slot /> 35 | </sui-tab-item> 36 | <style> 37 | sui-tab-item { 38 | display: none; 39 | width: 100%; 40 | height: auto; 41 | } 42 | 43 | sui-tab-item.active { 44 | display: block; 45 | } 46 | </style> 47 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Textarea/Textarea.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | import { generateID } from '../../utils/generateID.js'; 4 | import './textarea.css'; 5 | 6 | /** 7 | * Props for the textarea component 8 | */ 9 | interface Props extends HTMLAttributes<'textarea'> { 10 | /** 11 | * The label of the textarea. 12 | */ 13 | label?: string; 14 | /** 15 | * The placeholder of the textarea. 16 | */ 17 | placeholder?: string; 18 | /** 19 | * Whether the textarea is required. Defaults to `false`. 20 | */ 21 | isRequired?: boolean; 22 | /** 23 | * Whether the textarea should take up the full width of its container. Defaults to `false`. 24 | */ 25 | fullWidth?: boolean; 26 | /** 27 | * Whether the textarea should take up the full height of its container. Defaults to `false`. 28 | */ 29 | fullHeight?: boolean; 30 | /** 31 | * Whether the textarea should be resizable. Defaults to `false`. 32 | */ 33 | resize?: boolean; 34 | /** 35 | * The name attribute for the textarea. Useful for form submission. 36 | */ 37 | name?: string; 38 | /** 39 | * Whether the textarea is disabled. Defaults to `false`. 40 | */ 41 | disabled?: boolean; 42 | /** 43 | * The default value of the textarea. 44 | */ 45 | defaultValue?: string; 46 | } 47 | 48 | const { 49 | label, 50 | placeholder, 51 | isRequired, 52 | fullWidth, 53 | fullHeight, 54 | resize, 55 | name = generateID('textarea'), 56 | disabled, 57 | defaultValue, 58 | ...props 59 | } = Astro.props; 60 | --- 61 | <label 62 | for={name} 63 | class="sui-textarea-label" 64 | class:list={[ 65 | fullWidth && "full-width", 66 | fullHeight && "full-height", 67 | resize && "resize", 68 | disabled && "disabled", 69 | ]} 70 | > 71 | <span class="label"> 72 | {label} <span class="req-star">{isRequired && "*"}</span> 73 | </span> 74 | <textarea 75 | placeholder={placeholder} 76 | name={name} 77 | id={name} 78 | class="sui-textarea" 79 | required={isRequired} 80 | disabled={disabled} 81 | {...props} 82 | 83 | >{defaultValue}</textarea> 84 | </label> 85 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Textarea/textarea.css: -------------------------------------------------------------------------------- 1 | .sui-textarea-label { 2 | display: flex; 3 | flex-direction: column; 4 | gap: .25rem; 5 | max-width: 80ch; 6 | } 7 | 8 | .sui-textarea-label.disabled { 9 | opacity: 0.5; 10 | pointer-events: none; 11 | color: hsl(var(--text-muted)); 12 | } 13 | 14 | .sui-textarea-label.full-width { 15 | width: 100%; 16 | max-width: none; 17 | } 18 | 19 | .sui-textarea-label.full-height { 20 | height: 100%; 21 | } 22 | 23 | .label { 24 | font-size: 14px; 25 | } 26 | 27 | .sui-textarea { 28 | padding: .65rem; 29 | border-radius: var(--radius-md); 30 | border: 1px solid hsl(var(--border)); 31 | background: hsl(var(--background-step-2)); 32 | color: hsl(var(--text-normal)); 33 | transition: all .15s ease; 34 | resize: none; 35 | min-height: 12ch; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | 40 | .sui-textarea:hover { 41 | background: hsl(var(--background-step-3)); 42 | } 43 | 44 | .resize .sui-textarea { 45 | resize: both; 46 | } 47 | 48 | .sui-textarea:active, 49 | .sui-textarea:focus { 50 | border: 1px solid hsl(var(--primary-base)); 51 | outline: none; 52 | background: hsl(var(--background-step-2)); 53 | } 54 | 55 | .req-star { 56 | color: hsl(var(--danger-base)); 57 | font-weight: 700; 58 | } 59 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/ThemeToggle/ThemeToggle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Button, { type Props as ButtonProps } from '../Button/Button.astro'; 3 | import './themetoggle.css'; 4 | 5 | type Props = ButtonProps; 6 | 7 | const props = Astro.props; 8 | --- 9 | 10 | <Button id='sui-theme-toggle' {...props}> 11 | <div id="dark-content"> 12 | <slot name="dark" /> 13 | </div> 14 | <div id="light-content"> 15 | <slot name="light" /> 16 | </div> 17 | </Button> 18 | 19 | <script> 20 | import "studiocms:ui/scripts/themetoggle" 21 | </script> 22 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/ThemeToggle/themetoggle.css: -------------------------------------------------------------------------------- 1 | #sui-theme-toggle, 2 | #sui-theme-toggle * { 3 | color: hsl(var(--text-normal)); 4 | } 5 | 6 | #sui-theme-toggle #dark-content, 7 | #sui-theme-toggle #light-content { 8 | display: none; 9 | width: fit-content; 10 | height: fit-content; 11 | max-height: 100%; 12 | } 13 | 14 | [data-theme="dark"] #sui-theme-toggle #dark-content { 15 | display: block; 16 | } 17 | 18 | [data-theme="light"] #sui-theme-toggle #light-content { 19 | display: block; 20 | } 21 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/ThemeToggle/themetoggle.ts: -------------------------------------------------------------------------------- 1 | import { ThemeHelper } from '../../utils/ThemeHelper.js'; 2 | 3 | const themeToggle = document.getElementById('sui-theme-toggle'); 4 | const themeHelper = new ThemeHelper(); 5 | 6 | themeHelper.registerToggle(themeToggle); 7 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Toast/Toaster.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './toaster.css'; 3 | 4 | /** 5 | * Props for the Toast component. 6 | */ 7 | interface Props { 8 | /** 9 | * The position of the toaster. All toasts will originate from this position. 10 | */ 11 | position?: 12 | | 'top-left' 13 | | 'top-right' 14 | | 'top-center' 15 | | 'bottom-left' 16 | | 'bottom-right' 17 | | 'bottom-center'; 18 | /** 19 | * The duration of the toast in milliseconds. Defaults to 4000 (4 seconds). 20 | */ 21 | duration?: number; 22 | /** 23 | * Whether the toast has a close button. Defaults to false. 24 | */ 25 | closeButton?: boolean; 26 | /** 27 | * The offset of the toaster from the edge of the screen in pixels. Defaults to 32. 28 | */ 29 | offset?: number; 30 | /** 31 | * The gap between toasts in pixels. Defaults to 8. 32 | */ 33 | gap?: number; 34 | } 35 | 36 | const { 37 | position = 'top-center', 38 | duration = 4000, 39 | closeButton = false, 40 | offset = 32, 41 | gap = 8, 42 | } = Astro.props; 43 | --- 44 | <div 45 | id="sui-toaster" 46 | class:list={[ 47 | closeButton && "closeable", 48 | position, 49 | ]}, 50 | > 51 | <div 52 | id="sui-toast-drawer" 53 | data-offset={offset} 54 | data-gap={gap} 55 | data-duration={duration} 56 | style={[ 57 | `${position.includes("top-") ? 'top:' : 'bottom:'} ${offset}px;`, 58 | position.includes("-left") && `left: ${offset}px`, 59 | position.includes("-right") && `right: ${offset}px`, 60 | position.includes("-center") && `left: 50%; transform: translateX(-50%);`, 61 | `--gap: ${gap}px;`, 62 | `padding-left: ${offset}px;`, 63 | `padding-right: ${offset}px;`, 64 | ].filter(Boolean).join("")} 65 | /> 66 | </div> 67 | <script> 68 | import "studiocms:ui/scripts/toaster" 69 | </script> 70 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Toast/toast.ts: -------------------------------------------------------------------------------- 1 | import type { ToastProps } from '../../types/index.js'; 2 | 3 | /** 4 | * A function to create toasts with. 5 | 6 | * @param props The props to pass to the toast 7 | */ 8 | function toast(props: ToastProps) { 9 | const createToast = new CustomEvent('createtoast', { 10 | detail: props, 11 | }); 12 | 13 | document.dispatchEvent(createToast); 14 | } 15 | 16 | export { toast }; 17 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Toggle/Toggle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { StudioCMSColorway } from '../../utils/colors.js'; 3 | import { generateID } from '../../utils/generateID.js'; 4 | import './toggle.css'; 5 | 6 | /** 7 | * The props for the toggle component 8 | */ 9 | interface Props { 10 | /** 11 | * The label of the toggle. 12 | */ 13 | label: string; 14 | /** 15 | * The size of the toggle. Defaults to `md`. 16 | */ 17 | size?: 'sm' | 'md' | 'lg'; 18 | /** 19 | * The color of the toggle. Defaults to `default`. 20 | */ 21 | color?: StudioCMSColorway; 22 | /** 23 | * Whether the toggle is checked by default. Defaults to `false`. 24 | */ 25 | defaultChecked?: boolean; 26 | /** 27 | * Whether the toggle is disabled. Defaults to `false`. 28 | */ 29 | disabled?: boolean; 30 | /** 31 | * The name of the toggle. 32 | */ 33 | name?: string; 34 | /** 35 | * Whether the toggle is required. Defaults to `false`. 36 | */ 37 | isRequired?: boolean; 38 | } 39 | 40 | const { 41 | size = 'md', 42 | color = 'default', 43 | defaultChecked, 44 | disabled, 45 | name = generateID('checkbox'), 46 | label, 47 | isRequired, 48 | } = Astro.props; 49 | --- 50 | <label 51 | class="sui-toggle-label" 52 | for={name} 53 | class:list={[ 54 | disabled && "disabled", 55 | color, 56 | size, 57 | ]} 58 | > 59 | <div class="sui-toggle-container"> 60 | <div 61 | class="sui-toggle-switch" 62 | tabindex="0" 63 | role="checkbox" 64 | aria-checked={defaultChecked} 65 | aria-label={label} 66 | /> 67 | <input 68 | type="checkbox" 69 | name={name} 70 | id={name} 71 | checked={defaultChecked} 72 | disabled={disabled} 73 | required={isRequired} 74 | class="sui-toggle-checkbox" 75 | hidden 76 | /> 77 | </div> 78 | <span id={`label-${name}`}> 79 | {label} <span class="req-star">{isRequired && "*"}</span> 80 | </span> 81 | </label> 82 | <script> 83 | import "studiocms:ui/scripts/toggle" 84 | </script> 85 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Toggle/toggle.css: -------------------------------------------------------------------------------- 1 | .sui-toggle-label { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: .5rem; 6 | position: relative; 7 | margin: .25rem 0; 8 | } 9 | 10 | .sui-toggle-label.disabled { 11 | opacity: 0.5; 12 | pointer-events: none; 13 | color: hsl(var(--text-muted)); 14 | } 15 | 16 | .sui-toggle-label:active .sui-toggle-switch { 17 | transform: scale(0.85); 18 | } 19 | 20 | .sui-toggle-container { 21 | --toggle-height: 12px; 22 | --toggle-width: 40px; 23 | display: flex; 24 | align-items: center; 25 | cursor: pointer; 26 | transition: all .15s ease; 27 | background-color: hsl(var(--default-base)); 28 | width: var(--toggle-width); 29 | height: var(--toggle-height); 30 | border-radius: var(--radius-full); 31 | } 32 | 33 | .sui-toggle-switch { 34 | --switch: calc(var(--toggle-height) * 1.75); 35 | height: var(--switch); 36 | width: var(--switch); 37 | background-color: hsl(var(--text-muted)); 38 | border-radius: var(--radius-full); 39 | position: relative; 40 | left: 0; 41 | transition: all .15s ease; 42 | will-change: transform; 43 | } 44 | 45 | .sui-toggle-switch:focus-visible { 46 | outline: 2px solid hsl(var(--text-normal)); 47 | outline-offset: 2px; 48 | } 49 | 50 | .sui-toggle-container:has(.sui-toggle-checkbox:checked) .sui-toggle-switch { 51 | left: calc(100% - var(--switch)); 52 | background-color: hsl(var(--text-normal)); 53 | } 54 | 55 | .sui-toggle-label.sm .sui-toggle-container { 56 | --toggle-height: 10px; 57 | --toggle-width: 32px; 58 | } 59 | 60 | .sui-toggle-label.sm .sui-toggle-switch { 61 | --switch: calc(var(--toggle-height) * 1.65); 62 | } 63 | 64 | .sui-toggle-label.lg .sui-toggle-container { 65 | --toggle-height: 16px; 66 | --toggle-width: 48px; 67 | } 68 | 69 | .sui-toggle-label.lg .sui-toggle-switch { 70 | --switch: calc(var(--toggle-height) * 1.65); 71 | } 72 | 73 | .sui-toggle-label.primary .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 74 | background-color: hsl(var(--primary-base)); 75 | } 76 | 77 | .sui-toggle-label.success .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 78 | background-color: hsl(var(--success-base)); 79 | } 80 | 81 | .sui-toggle-label.warning .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 82 | background-color: hsl(var(--warning-base)); 83 | } 84 | 85 | .sui-toggle-label.danger .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 86 | background-color: hsl(var(--danger-base)); 87 | } 88 | 89 | .sui-toggle-label.info .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 90 | background-color: hsl(var(--info-base)); 91 | } 92 | 93 | .sui-toggle-label.mono .sui-toggle-container:has(.sui-toggle-checkbox:checked) { 94 | background-color: hsl(var(--mono-base)); 95 | } 96 | 97 | .sui-toggle-label.mono .sui-toggle-switch { 98 | border: 1px solid hsl(var(--text-inverted)); 99 | } 100 | 101 | .req-star { 102 | color: hsl(var(--danger-base)); 103 | font-weight: 700; 104 | } 105 | 106 | .sui-toggle-checkbox { 107 | width: 0; 108 | height: 0; 109 | visibility: hidden; 110 | opacity: 0; 111 | margin: 0; 112 | } 113 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/Toggle/toggle.ts: -------------------------------------------------------------------------------- 1 | function loadToggles() { 2 | const allToggleElements = document.querySelectorAll<HTMLDivElement>('.sui-toggle-container'); 3 | const allToggles = document.querySelectorAll<HTMLInputElement>('.sui-toggle-checkbox'); 4 | 5 | for (const element of allToggleElements) { 6 | if (element.dataset.initialized) continue; 7 | 8 | element.dataset.initialized = 'true'; 9 | 10 | element.addEventListener('keydown', (e) => { 11 | if (e.key !== 'Enter' && e.key !== ' ') return; 12 | 13 | e.preventDefault(); 14 | 15 | const checkbox = element.querySelector<HTMLInputElement>('.sui-toggle-checkbox'); 16 | 17 | if (!checkbox) return; 18 | 19 | checkbox.click(); 20 | }); 21 | } 22 | 23 | for (const box of allToggles) { 24 | if (box.dataset.initialized) continue; 25 | 26 | box.dataset.initialized = 'true'; 27 | 28 | box.addEventListener('change', (e) => { 29 | (box.previousSibling as HTMLDivElement).ariaChecked = (e.target as HTMLInputElement).checked 30 | ? 'true' 31 | : 'false'; 32 | }); 33 | } 34 | } 35 | 36 | document.addEventListener('astro:page-load', loadToggles); 37 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/User/User.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // @ts-ignore 3 | import { Image } from 'astro:assets'; 4 | import Icon from '../Icon/Icon.astro'; 5 | import './user.css'; 6 | 7 | /** 8 | * The props for the User component. 9 | */ 10 | interface Props { 11 | /** 12 | * The name of the user. 13 | */ 14 | name: string; 15 | /** 16 | * The description of the user. Could be a role, a handle, etc. 17 | */ 18 | description: string; 19 | /** 20 | * The avatar of the user. Either a URL to an image or an imported image. 21 | */ 22 | avatar?: string; 23 | /** 24 | * Additional classes to apply to the user container. 25 | */ 26 | class?: string; 27 | /** 28 | * The loading strategy for the image. Defaults to `lazy`. 29 | */ 30 | loading?: 'eager' | 'lazy'; 31 | } 32 | 33 | const { name, description, avatar, class: className, loading = 'lazy' } = Astro.props; 34 | --- 35 | <div class="sui-user-container" class:list={[ className ]}> 36 | <div class="sui-avatar-container"> 37 | {avatar ? ( 38 | <Image src={avatar} inferSize loading={loading} alt={name} class="sui-avatar-img" /> 39 | ) : ( 40 | <Icon name='user' width={24} height={24} role='img' aria-label={`Placeholder avatar for ${name}`} /> 41 | )} 42 | </div> 43 | <div class="sui-text-content"> 44 | <span class="sui-name">{name}</span> 45 | <span class="sui-description">{description}</span> 46 | </div> 47 | </div> 48 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/components/User/user.css: -------------------------------------------------------------------------------- 1 | .sui-user-container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 1rem; 6 | } 7 | 8 | .sui-avatar-container { 9 | width: 2.5rem; 10 | height: 2.5rem; 11 | background-color: hsl(var(--default-base)); 12 | border-radius: var(--radius-full); 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | overflow: hidden; 17 | border: 1px solid hsl(var(--border)); 18 | } 19 | 20 | .sui-avatar-img { 21 | width: 100%; 22 | height: auto; 23 | } 24 | 25 | .sui-text-content { 26 | display: flex; 27 | flex-direction: column; 28 | gap: .125rem; 29 | } 30 | 31 | .sui-name { 32 | font-size: 1em; 33 | font-weight: 600; 34 | } 35 | 36 | .sui-description { 37 | font-size: .875em; 38 | font-weight: 400; 39 | color: hsl(var(--text-muted)); 40 | } 41 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/css/global.css: -------------------------------------------------------------------------------- 1 | @import url("./colors.css"); 2 | @import url("./resets.css"); 3 | @import url("./radii.css"); 4 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/css/radii.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --radius-sm: 0.25rem; 3 | --radius-md: 0.5rem; 4 | --radius-lg: 0.75rem; 5 | --radius-full: 999px; 6 | } 7 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/css/resets.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box !important; 5 | } 6 | 7 | html { 8 | color-scheme: dark; 9 | accent-color: hsl(var(--primary-base)); 10 | } 11 | 12 | html, 13 | body { 14 | margin: 0; 15 | } 16 | 17 | html[data-theme="light"] { 18 | color-scheme: light; 19 | } 20 | 21 | body { 22 | -webkit-font-smoothing: antialiased; 23 | color: hsl(var(--text-normal)); 24 | background-color: hsl(var(--background-base)); 25 | } 26 | 27 | input, 28 | button, 29 | textarea, 30 | select { 31 | font: inherit; 32 | } 33 | 34 | p, 35 | h1, 36 | h2, 37 | h3, 38 | h4, 39 | h5, 40 | h6, 41 | code { 42 | overflow-wrap: anywhere; 43 | } 44 | 45 | code { 46 | font-family: monospace; 47 | } 48 | 49 | button { 50 | border: none; 51 | outline: none; 52 | background: none; 53 | padding: 0; 54 | } 55 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/icons/Checkmark.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'svg'> { 5 | height?: number; 6 | width?: number; 7 | } 8 | 9 | const { height = 24, width = 24, ...props } = Astro.props; 10 | --- 11 | <svg role="presentation" viewBox="0 0 17 18" width={width} height={height} {...props}> 12 | <polyline fill="none" points="1 9 7 14 15 4" stroke="currentColor" stroke-dasharray="22" stroke-dashoffset="66" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> 13 | </svg> -------------------------------------------------------------------------------- /packages/studiocms_ui/src/icons/ChevronUpDown.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'svg'> { 5 | height?: number; 6 | width?: number; 7 | } 8 | 9 | const { height = 24, width = 24, ...props } = Astro.props; 10 | --- 11 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" width={width} height={height} {...props} stroke-width="1.5" stroke="currentColor"> 12 | <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" /> 13 | </svg> 14 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/icons/User.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'svg'> { 5 | height?: number; 6 | width?: number; 7 | } 8 | 9 | const { height = 24, width = 24, ...props } = Astro.props; 10 | --- 11 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width={width} height={height} {...props}> 12 | <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /> 13 | </svg> 14 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/icons/X-Mark.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | 4 | interface Props extends HTMLAttributes<'svg'> { 5 | height?: number; 6 | width?: number; 7 | } 8 | 9 | const { height = 24, width = 24, ...props } = Astro.props; 10 | --- 11 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" width={width} height={height} {...props}> 12 | <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> 13 | </svg> -------------------------------------------------------------------------------- /packages/studiocms_ui/src/toolbar/icon.ts: -------------------------------------------------------------------------------- 1 | export const studiocmsLogo: string = `<svg width="755" height="792" viewBox="0 0 755 792" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="295" width="460" height="466" rx="32" fill="white"/><path d="M272 434V166H180C162.327 166 148 180.327 148 198V597C148 614.673 162.327 629 180 629H577.5C595.173 629 609.5 614.673 609.5 597V490H328C297.072 490 272 464.928 272 434Z" fill="white"/><path d="M124 597V329H32C14.3269 329 0 343.327 0 361V760C0 777.673 14.3269 792 32 792H429.5C447.173 792 461.5 777.673 461.5 760V653H180C149.072 653 124 627.928 124 597Z" fill="white"/></svg>`; 2 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ToastProps { 2 | title: string; 3 | /** 4 | * This will get passed to the component as unsanitized HTML. DO NOT PUT USER-GENERATED CONTENT HERE! 5 | */ 6 | description?: string; 7 | type: 'success' | 'warning' | 'danger' | 'info'; 8 | duration?: number; 9 | persistent?: boolean; 10 | closeButton?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | export type StudioCMSColorway = 2 | | 'default' 3 | | 'primary' 4 | | 'success' 5 | | 'warning' 6 | | 'danger' 7 | | 'info' 8 | | 'mono'; 9 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/utils/generateID.ts: -------------------------------------------------------------------------------- 1 | function generateID(prefix: string): string { 2 | return `${prefix}-${Math.random().toString(16).slice(2)}`; 3 | } 4 | 5 | export { generateID }; 6 | -------------------------------------------------------------------------------- /packages/studiocms_ui/src/utils/iconStrings.ts: -------------------------------------------------------------------------------- 1 | type ValidIconString = 2 | | 'check-circle' 3 | | 'exclamation-triangle' 4 | | 'exclamation-circle' 5 | | 'information-circle' 6 | | 'x-mark'; 7 | 8 | const iconStrings: Record<ValidIconString, string> = { 9 | 'check-circle': 10 | '<svg xmlns="http://www.w3.org/2000/svg" class="%class%" width="%width%" height="%height%" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>', 11 | 'exclamation-circle': 12 | '<svg xmlns="http://www.w3.org/2000/svg" class="%class%" width="%width%" height="%height%" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" /></svg>', 13 | 'information-circle': 14 | '<svg xmlns="http://www.w3.org/2000/svg" class="%class%" width="%width%" height="%height%" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg>', 15 | 'x-mark': 16 | '<svg xmlns="http://www.w3.org/2000/svg" class="%class%" width="%width%" height="%height%" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>', 17 | 'exclamation-triangle': 18 | '<svg xmlns="http://www.w3.org/2000/svg" class="%class%" width="%width%" height="%height%" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>', 19 | }; 20 | 21 | function getIconString(icon: ValidIconString, classes: string, width: number, height: number) { 22 | return iconStrings[icon] 23 | .replace('%class%', classes) 24 | .replace('%width%', width.toString()) 25 | .replace('%height%', height.toString()); 26 | } 27 | 28 | export { getIconString }; 29 | export type { ValidIconString }; 30 | -------------------------------------------------------------------------------- /packages/studiocms_ui/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "emitDeclarationOnly": true, 7 | "resolveJsonModule": true, 8 | "rootDir": "./src", 9 | "jsx": "preserve" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/studiocms_ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ESNext" 6 | }, 7 | "exclude": ["dist"], 8 | "include": ["src/**/*", "env.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "build-scripts" 4 | - "docs" 5 | 6 | catalog: 7 | "@astrojs/check": ^0.9.4 8 | "@astrojs/node": ^9.0.0 9 | "@fontsource-variable/onest": 5.1.0 10 | "@iconify-json/heroicons": ^1.2.1 11 | "@iconify/utils": ^2.1.33 12 | "@sentry/astro": ^8.42.0 13 | "@types/node": ^20.14.11 14 | pathe: ^1.1.2 15 | astro: ^5.1.1 16 | typescript: ^5.7.2 17 | 18 | catalogs: 19 | peers: 20 | astro: ^4.5 || ^5.0.0-beta.0 21 | vite: ^5.0.0 || ^6.0.0 22 | -------------------------------------------------------------------------------- /scripts/filter-warnings.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * When using custom loaders like `tsm` to add TypeScript & module support to scripts, 3 | * Node.js outputs a known set of warnings, which distract from the actual script output. 4 | * 5 | * By adding `--require=./scripts/lib/filter-warnings.cjs` to the `node` or `tsm` args, 6 | * this script filters those known warnings (but not any others) from the output. 7 | */ 8 | 9 | // Remove Node's built-in `warning` listener which outputs all warnings to stderr 10 | process.removeAllListeners('warning'); 11 | 12 | // Add our own version that skips known warnings 13 | process.on('warning', (warning) => { 14 | const { name, message } = warning; 15 | if ( 16 | name === 'ExperimentalWarning' && 17 | (message.indexOf('--experimental-loader') > -1 || message.indexOf('Custom ESM Loaders') > -1) 18 | ) 19 | return; 20 | if (name === 'DeprecationWarning' && message.indexOf('Obsolete loader hook') > -1) return; 21 | 22 | console.warn(warning); 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "strict": true, 7 | "moduleResolution": "bundler", 8 | "target": "ESNext", 9 | "module": "ESNext", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "verbatimModuleSyntax": true, 13 | "stripInternal": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------