├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature.yml ├── actions │ └── setup │ │ └── action.yml ├── pull_request_template.md └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── eslint.config.js ├── license ├── package.json ├── pnpm-lock.yaml ├── readme.md ├── src ├── index.ts └── lib │ ├── directus-frame.ts │ ├── editable-element.ts │ ├── editable-store.ts │ ├── overlay-element.ts │ ├── overlay-manager.ts │ ├── page-manager.ts │ └── types │ ├── directus.ts │ └── index.ts ├── test-website ├── readme.md ├── simple-cms │ └── nuxt │ │ ├── .editorconfig │ │ ├── .env.example │ │ ├── .eslintignore │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── .prettierrc.json │ │ ├── app │ │ ├── app.config.ts │ │ ├── app.vue │ │ ├── assets │ │ │ ├── css │ │ │ │ └── tailwind.css │ │ │ └── images │ │ │ │ └── directus-logo-white.svg │ │ ├── components │ │ │ ├── Footer.vue │ │ │ ├── NavigationBar.vue │ │ │ ├── PageBuilder.vue │ │ │ ├── base │ │ │ │ ├── BaseBlock.vue │ │ │ │ ├── BaseButton.vue │ │ │ │ ├── ButtonGroup.vue │ │ │ │ ├── Container.vue │ │ │ │ ├── Headline.vue │ │ │ │ ├── SearchModel.vue │ │ │ │ ├── ShareDialog.vue │ │ │ │ ├── Tagline.vue │ │ │ │ ├── Text.vue │ │ │ │ └── ThemeToggle.vue │ │ │ ├── block │ │ │ │ ├── FormBlock.vue │ │ │ │ ├── Gallery.vue │ │ │ │ ├── Hero.vue │ │ │ │ ├── Posts.vue │ │ │ │ ├── Pricing.vue │ │ │ │ ├── PricingCard.vue │ │ │ │ └── RichText.vue │ │ │ ├── forms │ │ │ │ ├── BaseFormField.vue │ │ │ │ ├── DynamicForm.vue │ │ │ │ ├── FormBuilder.vue │ │ │ │ └── fields │ │ │ │ │ ├── CheckboxField.vue │ │ │ │ │ ├── CheckboxGroupField.vue │ │ │ │ │ ├── FileUploadField.vue │ │ │ │ │ ├── RadioGroupField.vue │ │ │ │ │ └── SelectField.vue │ │ │ ├── shared │ │ │ │ ├── AdminBar.vue │ │ │ │ └── DirectusImage.vue │ │ │ └── ui │ │ │ │ ├── badge │ │ │ │ ├── Badge.vue │ │ │ │ └── index.ts │ │ │ │ ├── button │ │ │ │ ├── Button.vue │ │ │ │ └── index.ts │ │ │ │ ├── checkbox │ │ │ │ ├── Checkbox.vue │ │ │ │ └── index.ts │ │ │ │ ├── collapsible │ │ │ │ ├── Collapsible.vue │ │ │ │ ├── CollapsibleContent.vue │ │ │ │ ├── CollapsibleTrigger.vue │ │ │ │ └── index.ts │ │ │ │ ├── command │ │ │ │ ├── Command.vue │ │ │ │ ├── CommandDialog.vue │ │ │ │ ├── CommandEmpty.vue │ │ │ │ ├── CommandGroup.vue │ │ │ │ ├── CommandInput.vue │ │ │ │ ├── CommandItem.vue │ │ │ │ ├── CommandList.vue │ │ │ │ ├── CommandSeparator.vue │ │ │ │ ├── CommandShortcut.vue │ │ │ │ └── index.ts │ │ │ │ ├── dialog │ │ │ │ ├── Dialog.vue │ │ │ │ ├── DialogClose.vue │ │ │ │ ├── DialogContent.vue │ │ │ │ ├── DialogDescription.vue │ │ │ │ ├── DialogFooter.vue │ │ │ │ ├── DialogHeader.vue │ │ │ │ ├── DialogScrollContent.vue │ │ │ │ ├── DialogTitle.vue │ │ │ │ ├── DialogTrigger.vue │ │ │ │ └── index.ts │ │ │ │ ├── dropdown-menu │ │ │ │ ├── DropdownMenu.vue │ │ │ │ ├── DropdownMenuCheckboxItem.vue │ │ │ │ ├── DropdownMenuContent.vue │ │ │ │ ├── DropdownMenuGroup.vue │ │ │ │ ├── DropdownMenuItem.vue │ │ │ │ ├── DropdownMenuLabel.vue │ │ │ │ ├── DropdownMenuRadioGroup.vue │ │ │ │ ├── DropdownMenuRadioItem.vue │ │ │ │ ├── DropdownMenuSeparator.vue │ │ │ │ ├── DropdownMenuShortcut.vue │ │ │ │ ├── DropdownMenuSub.vue │ │ │ │ ├── DropdownMenuSubContent.vue │ │ │ │ ├── DropdownMenuSubTrigger.vue │ │ │ │ ├── DropdownMenuTrigger.vue │ │ │ │ └── index.ts │ │ │ │ ├── form │ │ │ │ ├── FormControl.vue │ │ │ │ ├── FormDescription.vue │ │ │ │ ├── FormItem.vue │ │ │ │ ├── FormLabel.vue │ │ │ │ ├── FormMessage.vue │ │ │ │ ├── index.ts │ │ │ │ ├── injectionKeys.ts │ │ │ │ └── useFormField.ts │ │ │ │ ├── input │ │ │ │ ├── Input.vue │ │ │ │ └── index.ts │ │ │ │ ├── label │ │ │ │ ├── Label.vue │ │ │ │ └── index.ts │ │ │ │ ├── navigation-menu │ │ │ │ ├── NavigationMenu.vue │ │ │ │ ├── NavigationMenuContent.vue │ │ │ │ ├── NavigationMenuIndicator.vue │ │ │ │ ├── NavigationMenuItem.vue │ │ │ │ ├── NavigationMenuLink.vue │ │ │ │ ├── NavigationMenuList.vue │ │ │ │ ├── NavigationMenuTrigger.vue │ │ │ │ ├── NavigationMenuViewport.vue │ │ │ │ └── index.ts │ │ │ │ ├── pagination │ │ │ │ ├── PaginationEllipsis.vue │ │ │ │ ├── PaginationFirst.vue │ │ │ │ ├── PaginationLast.vue │ │ │ │ ├── PaginationNext.vue │ │ │ │ ├── PaginationPrev.vue │ │ │ │ └── index.ts │ │ │ │ ├── popover │ │ │ │ ├── Popover.vue │ │ │ │ ├── PopoverContent.vue │ │ │ │ └── PopoverTrigger.vue │ │ │ │ ├── radio-group │ │ │ │ ├── RadioGroup.vue │ │ │ │ ├── RadioGroupItem.vue │ │ │ │ └── index.ts │ │ │ │ ├── select │ │ │ │ ├── Select.vue │ │ │ │ ├── SelectContent.vue │ │ │ │ ├── SelectGroup.vue │ │ │ │ ├── SelectItem.vue │ │ │ │ ├── SelectItemText.vue │ │ │ │ ├── SelectLabel.vue │ │ │ │ ├── SelectScrollDownButton.vue │ │ │ │ ├── SelectScrollUpButton.vue │ │ │ │ ├── SelectSeparator.vue │ │ │ │ ├── SelectTrigger.vue │ │ │ │ ├── SelectValue.vue │ │ │ │ └── index.ts │ │ │ │ ├── separator │ │ │ │ ├── Separator.vue │ │ │ │ └── index.ts │ │ │ │ ├── textarea │ │ │ │ ├── Textarea.vue │ │ │ │ └── index.ts │ │ │ │ └── tooltip │ │ │ │ ├── Tooltip.vue │ │ │ │ ├── TooltipContent.vue │ │ │ │ ├── TooltipProvider.vue │ │ │ │ ├── TooltipTrigger.vue │ │ │ │ └── index.ts │ │ ├── error.vue │ │ ├── layouts │ │ │ └── default.vue │ │ ├── lib │ │ │ └── zodSchemaBuilder.ts │ │ ├── pages │ │ │ ├── [...permalink].vue │ │ │ └── blog │ │ │ │ └── [slug].vue │ │ └── plugins │ │ │ └── directus.ts │ │ ├── components.json │ │ ├── eslint.config.mjs │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── public │ │ ├── favicon.ico │ │ ├── icons │ │ │ └── social │ │ │ │ ├── discord.svg │ │ │ │ ├── facebook.svg │ │ │ │ ├── github.svg │ │ │ │ ├── instagram.svg │ │ │ │ ├── linkedin.svg │ │ │ │ ├── reddit.svg │ │ │ │ ├── x.svg │ │ │ │ └── youtube.svg │ │ └── images │ │ │ └── logo.svg │ │ ├── scripts │ │ └── generate-types.js │ │ ├── server │ │ ├── api │ │ │ ├── forms │ │ │ │ └── submit.ts │ │ │ ├── page-data.ts │ │ │ ├── posts │ │ │ │ ├── [slug] │ │ │ │ │ ├── index.ts │ │ │ │ │ └── related.ts │ │ │ │ ├── count.ts │ │ │ │ └── index.ts │ │ │ ├── search │ │ │ │ └── index.ts │ │ │ ├── site-data.ts │ │ │ ├── sitemap.ts │ │ │ └── users │ │ │ │ ├── [id].ts │ │ │ │ └── authenticated-user.ts │ │ └── utils │ │ │ ├── directus-server.ts │ │ │ └── directus-utils.ts │ │ ├── shared │ │ ├── types │ │ │ └── schema.ts │ │ └── utils.ts │ │ ├── tailwind.config.ts │ │ └── tsconfig.json └── template │ ├── .gitignore │ ├── package.json │ └── src │ ├── access.json │ ├── assets │ ├── 03a7d1c7-81e2-432f-9561-9df2691189c8.png │ ├── 12e02b82-b4a4-4aaf-8ca4-e73c20a41c26.jpeg │ ├── 1d3d2bd3-ff59-4626-bef5-9d5eef6510b3.png │ ├── 2b4a0ba0-52c7-4e10-b191-c803d8da6a36.png │ ├── 35a67f1b-d401-4300-a503-b8e583186f2a.svg │ ├── 3eff7dc2-445a-47c5-9503-3f600ecdb5c6.jpeg │ ├── 43ddd7b8-9b2f-4aa1-b63c-933b4ae81ca2.svg │ ├── 440df429-4715-42a0-afcd-569f5cdfb145.svg │ ├── 44a4e780-d59b-4fa5-9b26-1c4b447474d2.jpg │ ├── 50570a31-a990-453c-bdfc-0ad7175dd8bf.png │ ├── 535f1284-dbc4-4e4e-9e50-b44a1df130bd.webp │ ├── 5e93050a-6f17-4314-a7e5-f78bda425fea.png │ ├── 6464e61f-455a-4b47-b623-bb12e5251dfe.jpeg │ ├── 68103296-6634-4d66-918a-04b09afe6621.jpeg │ ├── 6964d750-1c00-4b9c-81e4-0c81cfa82bbb.png │ ├── 7775c53a-6c2c-453d-8c22-8b5445121d10.jpeg │ ├── 8a652e52-a275-4dde-9fc5-edf2188afe56.jpg │ ├── 8f748634-d77b-4985-b27e-7e1f3559881a.jpeg │ ├── 9a52e835-e131-4290-81bb-5a512599f93e.png │ ├── a051ea01-07a5-45cb-bcc6-411af9560be5.png │ ├── ac905071-0643-4337-8f53-48ed45b1ccf2.jpg │ ├── ae390ba1-fcff-4b99-a445-5f19257095d1.svg │ ├── b9db00d9-535f-4e24-8a46-5f7e5fc65bf2.jpg │ ├── c2a301bd-74ed-4a50-9b85-3cb1f40f8dee.png │ ├── d4fd6edc-4cc5-48c1-8bc7-e646924bbdca.jpeg │ ├── d5a1290f-8819-4e7c-b292-bffe5b1c8274.jpg │ ├── dc258f02-d1a3-47f4-9f3e-2a71a0010c56.png │ ├── dea64c65-de50-4d86-abea-6dee3d5256b2.webp │ ├── df0745c2-b6e3-4b37-b64d-55a4eb0033ab.avif │ ├── ea743e20-e6e9-4be8-a949-3771cd182810.png │ ├── fd6440c2-dd48-4792-9d08-3124cd99b40f.png │ └── fe7c7e04-5aac-4370-8bbd-6fd578d26ea1.jpg │ ├── collections.json │ ├── content │ ├── block_button.json │ ├── block_button_group.json │ ├── block_form.json │ ├── block_gallery.json │ ├── block_gallery_items.json │ ├── block_hero.json │ ├── block_posts.json │ ├── block_pricing.json │ ├── block_pricing_cards.json │ ├── block_richtext.json │ ├── form_fields.json │ ├── form_submission_values.json │ ├── form_submissions.json │ ├── forms.json │ ├── globals.json │ ├── navigation.json │ ├── navigation_items.json │ ├── page_blocks.json │ ├── pages.json │ └── posts.json │ ├── dashboards.json │ ├── extensions.json │ ├── fields.json │ ├── files.json │ ├── flows.json │ ├── folders.json │ ├── operations.json │ ├── panels.json │ ├── permissions.json │ ├── policies.json │ ├── presets.json │ ├── relations.json │ ├── roles.json │ ├── schema │ └── snapshot.json │ ├── settings.json │ ├── translations.json │ └── users.json ├── tsconfig.json └── tsup.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{mjs,cjs,js,mts,cts,ts,json,vue,html,scss,css,toml,md}] 10 | indent_style = tab 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Report a bug 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Hi, thank you for taking the time to create an issue! Before you get started, please ensure the following are correct: 8 | 9 | - I'm using the [latest version of the Visual Editing library](https://github.com/directus/visual-editing/releases) 10 | - I'm using the [latest version of Directus](https://github.com/directus/directus/releases) 11 | - I've completed all [Troubleshooting Steps](https://docs.directus.io/getting-started/support/#troubleshooting-steps). 12 | - There's [no other issue](https://github.com/directus/visual-editing/issues?q=is%3Aissue) that already describes the problem. 13 | 14 | _For issues specific to Directus Cloud projects, please reach out through support@directus.io._ 15 | - type: textarea 16 | attributes: 17 | label: Describe the Bug 18 | description: A clear and concise description of what the bug is. 19 | validations: 20 | required: true 21 | - type: textarea 22 | attributes: 23 | label: To Reproduce 24 | description: 25 | Steps to reproduce the behavior. Contributors should be able to follow the steps provided in order to reproduce 26 | the bug. 27 | validations: 28 | required: true 29 | - type: input 30 | attributes: 31 | label: Visual Editing Version 32 | placeholder: v1.x.x 33 | validations: 34 | required: true 35 | - type: input 36 | attributes: 37 | label: Directus Version 38 | placeholder: v11.x.x 39 | validations: 40 | required: true 41 | - type: dropdown 42 | id: deployment 43 | attributes: 44 | label: Hosting Strategy 45 | options: 46 | - Self-Hosted (Docker Image) 47 | - Self-Hosted (Custom) 48 | - Directus Cloud 49 | validations: 50 | required: true 51 | - type: input 52 | attributes: 53 | label: Database 54 | description: If applicable, the vendor and version of the database being used. For example, PostgreSQL 16. 55 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question 4 | url: https://directus.chat/ 5 | about: Please ask and answer questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Create a feature request 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Hi, thank you for taking the time to create a feature request! Before you get started, please ensure the following are correct: 8 | 9 | - I'm using the [latest version of the Visual Editing library](https://github.com/directus/visual-editing/releases) 10 | - I'm using the [latest version of Directus](https://github.com/directus/directus/releases) 11 | - There's [no other issue](https://github.com/directus/visual-editing/issues?q=is%3Aissue) that already describes the feature. 12 | 13 | - type: textarea 14 | attributes: 15 | label: Describe the Feature 16 | description: A clear and concise description of what the feature is. 17 | validations: 18 | required: true 19 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/directus/eslint-config/blob/main/.github/actions/setup/action.yml 2 | 3 | name: Setup 4 | description: Configure Node.js + pnpm and install dependencies 5 | 6 | inputs: 7 | registry: 8 | description: NPM registry to set up for auth 9 | required: false 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version-file: package.json 18 | registry-url: ${{ inputs.registry }} 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | 23 | - name: Get pnpm cache dir 24 | id: pnpm-cache-dir 25 | shell: bash 26 | run: echo "pnpm-cache-dir=$(pnpm store path)" >> $GITHUB_OUTPUT 27 | 28 | - name: Setup pnpm cache 29 | uses: actions/cache@v4 30 | with: 31 | path: ${{ steps.pnpm-cache-dir.outputs.pnpm-cache-dir }} 32 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pnpm-store- 35 | 36 | - name: Install dependencies 37 | shell: bash 38 | run: pnpm install 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ## Scope 14 | 15 | What's changed: 16 | 17 | - Lorem ipsum dolor sit amet 18 | - Consectetur adipiscing elit 19 | - Sed do eiusmod tempor incididunt 20 | 21 | ## Potential Risks / Drawbacks 22 | 23 | - Lorem ipsum dolor sit amet 24 | - Consectetur adipiscing elit 25 | 26 | ## Review Notes / Questions 27 | 28 | - I would like to lorem ipsum 29 | - Special attention should be paid to dolor sit amet 30 | 31 | --- 32 | 33 | Fixes #\ 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User preferences 2 | .DS_Store 3 | 4 | # Dependencies 5 | node_modules/ 6 | 7 | # Builds / Caches 8 | *.tsbuildinfo 9 | **/.vitepress/cache/ 10 | **/.vitepress/.temp/ 11 | .eslintcache 12 | 13 | # Dotenv configs 14 | .env 15 | .env.* 16 | 17 | # Logs 18 | npm-debug.log 19 | debug 20 | .clinic 21 | 22 | # IDEs / Editors 23 | .devcontainer/ 24 | .vscode/* 25 | !.vscode/extensions.json 26 | .idea/ 27 | *.code-workspace 28 | *.sublime-settings 29 | .*.swp 30 | 31 | # Environments / Workspaces 32 | .gitpod.yml 33 | .netlify 34 | 35 | # Temporary files 36 | TODO 37 | 38 | # dist folder 39 | dist/ 40 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | shell-emulator=true 3 | save-prefix='' 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | pnpm-lock.yaml 3 | **/.vitepress/cache/ 4 | /.changeset/pre.json 5 | /.changeset/*.md 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "proseWrap": "always", 6 | "htmlWhitespaceSensitivity": "ignore", 7 | "overrides": [ 8 | { 9 | "files": "docs/**/*.md", 10 | "options": { 11 | "embeddedLanguageFormatting": "off" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2025 Monospace, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the “Software”), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@directus/visual-editing", 3 | "version": "1.1.0", 4 | "description": "Visual editing library to enable in-place editing of your website’s frontend from within the Visual Editor in Directus", 5 | "homepage": "https://directus.io/docs/guides/content/visual-editor", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/directus/visual-editing.git" 9 | }, 10 | "funding": "https://github.com/directus/directus?sponsor=1", 11 | "license": "MIT", 12 | "keywords": [ 13 | "visual-editing", 14 | "visual-editor", 15 | "content-editing-experience", 16 | "in-place-editing", 17 | "in-place-editor", 18 | "frontend-editing", 19 | "frontend-editor", 20 | "content-editing", 21 | "content-editor", 22 | "live-editing", 23 | "live-editor", 24 | "directus", 25 | "in-place", 26 | "frontend", 27 | "overlays", 28 | "preview", 29 | "editing", 30 | "editor", 31 | "iframe", 32 | "cms" 33 | ], 34 | "type": "module", 35 | "exports": { 36 | ".": { 37 | "import": "./dist/index.js", 38 | "require": "./dist/index.cjs", 39 | "default": "./dist/visual-editing.js" 40 | }, 41 | "./package.json": "./package.json" 42 | }, 43 | "main": "./dist/index.js", 44 | "files": [ 45 | "dist" 46 | ], 47 | "scripts": { 48 | "dev": "NODE_ENV=development tsup", 49 | "build": "NODE_ENV=production tsup", 50 | "test": "vitest --typecheck --watch=false" 51 | }, 52 | "dependencies": { 53 | "@reach/observe-rect": "1.2.0" 54 | }, 55 | "devDependencies": { 56 | "@directus/tsconfig": "3.0.0", 57 | "@eslint/js": "9.24.0", 58 | "eslint": "9.24.0", 59 | "eslint-config-prettier": "10.1.2", 60 | "eslint-plugin-vue": "10.0.0", 61 | "globals": "16.0.0", 62 | "prettier": "3.5.3", 63 | "tsup": "8.4.0", 64 | "typescript": "5.8.3", 65 | "typescript-eslint": "8.30.1", 66 | "vitest": "3.1.1" 67 | }, 68 | "engines": { 69 | "node": ">=22" 70 | }, 71 | "packageManager": "pnpm@10.8.1", 72 | "pnpm": { 73 | "onlyBuiltDependencies": [ 74 | "esbuild" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Visual Editing Frontend Library by Directus 2 | 3 | This library works together with a Directus instance to enable Visual Editing functionality from within the Visual 4 | Editor module in Directus Studio. 5 | 6 | Web developers must make use of this library to ensure communication between their website’s HTML elements and their 7 | Directus instance. This is done through data attributes and helper functions built into the library and imported into 8 | your website. 9 | 10 | ## Documentation 11 | 12 | Documentation covering the setup and functionality for both the frontend library and the studio module can be found in 13 | the [Directus documentation](https://directus.io/docs/guides/content/visual-editor). 14 | 15 | ## Test Website 16 | 17 | A test website pre-configured to work with the Directus Visual Editor module is included in the library’s repository. 18 | This can be used for example purposes and to make it easy to set up an environment for testing or contributing. 19 | 20 | For detailed instructions on setting up the test environment see the 21 | [test website's readme.md](https://github.com/directus/visual-editing/blob/main/test-website/readme.md). 22 | 23 | ## License 24 | 25 | This package is licensed under the MIT License. See the LICENSE file for more information. 26 | 27 | ## Additional Resources 28 | 29 | - [Directus Website](https://directus.io) 30 | - [Directus GitHub Repository](https://github.com/directus/directus) 31 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectusFrame } from './lib/directus-frame.ts'; 2 | import { EditableElement } from './lib/editable-element.ts'; 3 | import { EditableStore } from './lib/editable-store.ts'; 4 | import { OverlayManager } from './lib/overlay-manager.ts'; 5 | import { PageManager } from './lib/page-manager.ts'; 6 | import type { EditConfig, EditableElementOptions } from './lib/types/index.ts'; 7 | 8 | export async function apply({ 9 | directusUrl, 10 | elements = undefined, 11 | customClass = undefined, 12 | onSaved = undefined, 13 | }: { 14 | directusUrl: string; 15 | elements?: HTMLElement | HTMLElement[] | null; 16 | } & EditableElementOptions) { 17 | const directusFrame = new DirectusFrame(); 18 | const connected = directusFrame.connect(directusUrl); 19 | if (!connected) return; 20 | 21 | const confirmed = await directusFrame.receiveConfirm(); 22 | if (!confirmed) return; 23 | 24 | PageManager.onNavigation((data) => directusFrame.send('navigation', data)); 25 | OverlayManager.addStyles(); 26 | 27 | const editableElements = EditableElement.query(elements); 28 | const scopedItems: EditableElement[] = []; 29 | 30 | editableElements.forEach((element) => { 31 | const existingItem = EditableStore.getItem(element); 32 | const item = existingItem ?? new EditableElement(element); 33 | 34 | item.applyOptions({ customClass, onSaved }, !!elements); 35 | 36 | scopedItems.push(item); 37 | if (!existingItem) EditableStore.addItem(item); 38 | }); 39 | 40 | return { 41 | remove() { 42 | EditableStore.removeItems(scopedItems); 43 | }, 44 | enable() { 45 | EditableStore.enableItems(scopedItems); 46 | }, 47 | disable() { 48 | EditableStore.disableItems(scopedItems); 49 | }, 50 | }; 51 | } 52 | 53 | export function remove() { 54 | EditableStore.removeItems(); 55 | } 56 | 57 | export function disable() { 58 | const items = EditableStore.disableItems(); 59 | return { 60 | enable() { 61 | EditableStore.enableItems(items); 62 | }, 63 | }; 64 | } 65 | 66 | export function setAttr(editConfig: EditConfig) { 67 | return EditableElement.objectToEditAttr(editConfig); 68 | } 69 | -------------------------------------------------------------------------------- /src/lib/directus-frame.ts: -------------------------------------------------------------------------------- 1 | import { EditableStore } from './editable-store.ts'; 2 | import type { SendAction, ReceiveData, SavedData } from './types/index.ts'; 3 | 4 | /** 5 | * *Singleton* class to handle communication with Directus in parent frame. 6 | */ 7 | export class DirectusFrame { 8 | private static SINGLETON?: DirectusFrame; 9 | private static readonly ERROR_PARENT_NOT_FOUND = 'Error sending message to Directus in parent frame:'; 10 | 11 | private origin: string | null = null; 12 | private confirmed = false; 13 | 14 | constructor() { 15 | if (DirectusFrame.SINGLETON) return DirectusFrame.SINGLETON; 16 | DirectusFrame.SINGLETON = this; 17 | 18 | window?.addEventListener('message', this.receive.bind(this)); 19 | } 20 | 21 | connect(origin: string) { 22 | this.origin = origin; 23 | return this.send('connect'); 24 | } 25 | 26 | send(action: SendAction, data?: unknown) { 27 | try { 28 | if (!this.origin) throw new Error(); 29 | window.parent.postMessage({ action, data }, this.origin); 30 | return true; 31 | } catch (error) { 32 | // eslint-disable-next-line 33 | console.error(DirectusFrame.ERROR_PARENT_NOT_FOUND, error); 34 | return false; 35 | } 36 | } 37 | 38 | private sameOrigin(origin: string, url: string) { 39 | try { 40 | return origin === new URL(url).origin; 41 | } catch { 42 | return false; 43 | } 44 | } 45 | 46 | receive(event: MessageEvent) { 47 | if (!this.origin || !this.sameOrigin(event.origin, this.origin)) return; 48 | 49 | const { action, data }: ReceiveData = event.data; 50 | 51 | if (action === 'confirm') this.confirmed = true; 52 | if (action === 'showEditableElements') this.receiveShowEditableElements(data); 53 | if (action === 'saved') this.receiveSaved(data); 54 | } 55 | 56 | receiveConfirm() { 57 | let attempts = 0; 58 | const maxAttempts = 10; 59 | const timeout = 100; 60 | 61 | return new Promise((resolve) => { 62 | const checkConfirmed = () => { 63 | if (attempts >= maxAttempts) return resolve(false); 64 | attempts++; 65 | 66 | if (this.confirmed) resolve(true); 67 | else setTimeout(checkConfirmed, timeout); 68 | }; 69 | 70 | checkConfirmed(); 71 | }); 72 | } 73 | 74 | private receiveShowEditableElements(data: unknown) { 75 | const show = !!data; 76 | EditableStore.highlightItems(show); 77 | } 78 | 79 | private receiveSaved(data: unknown) { 80 | const { key = '', collection = '', item = null, payload = {} } = data as SavedData; 81 | 82 | const storeItem = EditableStore.getItemByKey(key); 83 | 84 | if (storeItem && collection && typeof storeItem.onSaved === 'function') { 85 | storeItem.onSaved({ collection, item, payload }); 86 | return; 87 | } 88 | 89 | window.location.reload(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/lib/editable-store.ts: -------------------------------------------------------------------------------- 1 | import { EditableElement } from './editable-element.ts'; 2 | 3 | export class EditableStore { 4 | private static items: EditableElement[] = []; 5 | static highlightOverlayElements = false; 6 | 7 | static getItem(element: Element) { 8 | return EditableStore.items.find((item) => item.element === element); 9 | } 10 | 11 | static getItemByKey(key: EditableElement['key']) { 12 | return EditableStore.items.find((item) => item.key === key); 13 | } 14 | 15 | static getHoveredItems() { 16 | return EditableStore.items.filter((item) => item.hover); 17 | } 18 | 19 | static addItem(item: EditableElement) { 20 | EditableStore.items.push(item); 21 | } 22 | 23 | static enableItems(selectedItems?: EditableElement[]) { 24 | const items = selectedItems ?? EditableStore.items; 25 | 26 | items.forEach((item) => { 27 | item.disabled = false; 28 | item.rectObserver.observe(); 29 | item.overlayElement.enable(); 30 | }); 31 | } 32 | 33 | static disableItems(selectedItems?: EditableElement[]) { 34 | const items = selectedItems ?? EditableStore.items.filter((item) => !item.disabled); 35 | 36 | items.forEach((item) => { 37 | item.disabled = true; 38 | item.hover = false; 39 | item.rectObserver.unobserve(); 40 | item.overlayElement.disable(); 41 | }); 42 | 43 | return [...items]; 44 | } 45 | 46 | static removeItems(selectedItems?: EditableElement[]) { 47 | const items = selectedItems ?? EditableStore.items; 48 | 49 | items.forEach((item) => { 50 | item.rectObserver.unobserve(); 51 | item.overlayElement.remove(); 52 | item.removeHoverListener(); 53 | }); 54 | 55 | EditableStore.items = EditableStore.items.filter((item) => !items.includes(item)); 56 | } 57 | 58 | static highlightItems(show: boolean) { 59 | if (this.highlightOverlayElements === show) return; 60 | 61 | this.highlightOverlayElements = show; 62 | 63 | EditableStore.items.forEach((item) => { 64 | item.overlayElement.toggleHighlight(show); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/page-manager.ts: -------------------------------------------------------------------------------- 1 | export class PageManager { 2 | private static navigationInitialized = false; 3 | 4 | static onNavigation(callback: (data: { url: string; title: string }) => void) { 5 | if (PageManager.navigationInitialized) return; 6 | 7 | let lastUrl = ''; 8 | let lastTitle = ''; 9 | let debounceId: number; 10 | 11 | const debounce = (debounceFunction: () => void) => { 12 | clearTimeout(debounceId); 13 | debounceId = setTimeout(debounceFunction, 100); 14 | }; 15 | 16 | const observeNavigation = () => { 17 | const url = window.location.href; 18 | const title = document.title; 19 | const changesDetected = lastUrl !== url || lastTitle !== title; 20 | 21 | if (changesDetected) { 22 | debounce(() => callback({ url, title })); 23 | lastUrl = url; 24 | lastTitle = title; 25 | } 26 | 27 | setTimeout(observeNavigation, changesDetected ? 50 : 200); 28 | }; 29 | 30 | observeNavigation(); 31 | 32 | PageManager.navigationInitialized = true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/types/directus.ts: -------------------------------------------------------------------------------- 1 | /** These types are shared with Directus */ 2 | /** Keep in sync with Directus */ 3 | 4 | // import { PrimaryKey } from '@directus/types'; 5 | type PrimaryKey = string | number; 6 | 7 | export type EditConfig = { 8 | collection: string; 9 | item: PrimaryKey | null; 10 | fields?: string[]; 11 | mode?: 'drawer' | 'modal' | 'popover'; 12 | }; 13 | 14 | export type SavedData = { 15 | key: string; 16 | collection: EditConfig['collection']; 17 | item: EditConfig['item']; 18 | payload: Record; 19 | }; 20 | 21 | export type ReceiveAction = 'connect' | 'edit' | 'navigation'; 22 | 23 | export type SendAction = 'confirm' | 'showEditableElements' | 'saved'; 24 | -------------------------------------------------------------------------------- /src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | EditConfig as DirectusEditConfig, 3 | ReceiveAction as DirectusReceiveAction, 4 | SendAction as DirectusSendAction, 5 | SavedData as DirectusSavedData, 6 | } from './directus.ts'; 7 | 8 | export type EditConfigStrict = DirectusEditConfig; 9 | 10 | export type EditConfig = Omit & { fields?: EditConfigStrict['fields'] | string }; 11 | 12 | export type SendAction = DirectusReceiveAction; 13 | export type ReceiveAction = DirectusSendAction; 14 | 15 | export type ReceiveData = { action: ReceiveAction | null; data: unknown }; 16 | 17 | export type SavedData = DirectusSavedData; 18 | 19 | export type EditableElementOptions = { 20 | customClass?: string | undefined; 21 | onSaved?: ((data: Omit) => void) | undefined; 22 | }; 23 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | indent_style = tab 8 | trim_trailing_whitespace = true 9 | 10 | [*.{yml,yaml}] 11 | indent_style = space 12 | 13 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.env.example: -------------------------------------------------------------------------------- 1 | DIRECTUS_URL=http://localhost:8080 2 | DIRECTUS_FORM_TOKEN= 3 | DIRECTUS_SERVER_TOKEN= 4 | NUXT_PUBLIC_SITE_URL=http://localhost:3000 5 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | !.env.example 9 | dist 10 | .DS_Store 11 | .vscode 12 | .eslintcache 13 | .netlify 14 | directus/data 15 | directus/uploads 16 | directus/dbs 17 | directus/extensions/.registry 18 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.prettierignore: -------------------------------------------------------------------------------- 1 | .nuxt 2 | *.d.ts 3 | *.json 4 | dist 5 | coverage 6 | node_modules 7 | pnpm-lock.yaml 8 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "htmlWhitespaceSensitivity": "ignore", 3 | "printWidth": 120, 4 | "singleQuote": true, 5 | "proseWrap": "always" 6 | } 7 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({}); 2 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background-color: #ffffff; 7 | --foreground-color: #42566e; 8 | --background-color-muted: color-mix(in srgb, var(--background-color), var(--foreground-color) 10%); 9 | --accent-color: #6644ff; 10 | --background-variant-color: #172940; 11 | --accent-color-dark: color-mix(in srgb, var(--accent-color), black 40%); 12 | --accent-color-soft: color-mix(in srgb, var(--accent-color), white 20%); 13 | --accent-color-light: color-mix(in srgb, var(--accent-color), white 90%); 14 | --input-color: color-mix(in srgb, var(--background-color), var(--foreground-color) 30%); 15 | } 16 | 17 | .dark { 18 | --background-color: #0e1a2b; 19 | --foreground-color: #ffffff; 20 | --background-color-muted: color-mix(in srgb, var(--background-color), var(--foreground-color) 10%); 21 | --background-variant-color: #172940; 22 | } 23 | 24 | @layer base { 25 | html { 26 | @apply bg-[var(--background-color)] text-[var(--foreground-color)]; 27 | } 28 | } 29 | 30 | @layer components { 31 | input:focus, 32 | textarea:focus, 33 | select:focus, 34 | button:focus, 35 | a:focus, 36 | [role='button']:focus { 37 | @apply outline-none ring-2 ring-[var(--accent-color)] ring-offset-2 ring-offset-background; 38 | } 39 | 40 | [data-background='dark'] { 41 | @apply bg-gray dark:bg-[var(--background-variant-color)]; 42 | } 43 | 44 | :root:not(.dark) [data-background='dark'] { 45 | .inline-flex[class*='bg-gray'] { 46 | @apply bg-[#172940] text-white hover:bg-accent; 47 | } 48 | } 49 | } 50 | 51 | @layer utilities { 52 | a { 53 | @apply hover:text-accent transition-colors no-underline; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/PageBuilder.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/BaseBlock.vue: -------------------------------------------------------------------------------- 1 | 32 | 35 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/BaseButton.vue: -------------------------------------------------------------------------------- 1 | 59 | 75 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/ButtonGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 19 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/Container.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/Headline.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/ShareDialog.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 82 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/Tagline.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/Text.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/base/ThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/block/FormBlock.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/block/Pricing.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 57 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/block/PricingCard.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 81 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/block/RichText.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/BaseFormField.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 72 | 73 | 87 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/FormBuilder.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 79 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/fields/CheckboxField.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/fields/CheckboxGroupField.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/fields/FileUploadField.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/fields/RadioGroupField.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/forms/fields/SelectField.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/shared/DirectusImage.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/badge/Badge.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/badge/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | 3 | export { default as Badge } from './Badge.vue'; 4 | 5 | export const badgeVariants = cva( 6 | 'inline-flex items-center rounded border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'border-transparent bg-primary text-accent', 11 | secondary: 'border-transparent bg-secondary text-white hover:bg-secondary/80', 12 | destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 13 | outline: 'text-foreground', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ); 21 | 22 | export type BadgeVariants = VariantProps; 23 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | 3 | export { default as Button } from './Button.vue'; 4 | 5 | export const buttonVariants = cva( 6 | 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', 7 | { 8 | variants: { 9 | variant: { 10 | default: 'bg-gray text-gray-dark hover:bg-gray/90 hover:text-accent', 11 | destructive: 'bg-red-600 text-white hover:bg-red-500', 12 | outline: 'border border-gray-500 hover:text-accent hover:border-accent', 13 | secondary: 'bg-blue text-white hover:bg-blue-800 dark:bg-accent', 14 | ghost: 'bg-transparent text-gray-900 hover:bg-background-muted dark:text-white', 15 | link: 'text-gray-700 underline-offset-4 hover:text-accent dark:text-gray-500', 16 | }, 17 | size: { 18 | default: 'h-10 px-4 py-2', 19 | sm: 'h-9 rounded-md px-3', 20 | lg: 'h-11 rounded-md px-8', 21 | icon: 'size-10 p-0', 22 | }, 23 | block: { 24 | true: 'w-full', 25 | }, 26 | }, 27 | defaultVariants: { 28 | variant: 'default', 29 | size: 'default', 30 | }, 31 | }, 32 | ); 33 | 34 | export type ButtonVariants = VariantProps; 35 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/checkbox/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Checkbox } from './Checkbox.vue' 2 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/collapsible/Collapsible.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/collapsible/CollapsibleContent.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/collapsible/CollapsibleTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/collapsible/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Collapsible } from './Collapsible.vue'; 2 | export { default as CollapsibleContent } from './CollapsibleContent.vue'; 3 | export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'; 4 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/Command.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandEmpty.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandGroup.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandInput.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandItem.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandList.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandSeparator.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/CommandShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/command/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Command } from './Command.vue'; 2 | export { default as CommandDialog } from './CommandDialog.vue'; 3 | export { default as CommandEmpty } from './CommandEmpty.vue'; 4 | export { default as CommandGroup } from './CommandGroup.vue'; 5 | export { default as CommandInput } from './CommandInput.vue'; 6 | export { default as CommandItem } from './CommandItem.vue'; 7 | export { default as CommandList } from './CommandList.vue'; 8 | export { default as CommandSeparator } from './CommandSeparator.vue'; 9 | export { default as CommandShortcut } from './CommandShortcut.vue'; 10 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/Dialog.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogClose.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogContent.vue: -------------------------------------------------------------------------------- 1 | 25 | 50 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogDescription.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogScrollContent.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 60 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogTitle.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/DialogTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dialog/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Dialog } from './Dialog.vue' 2 | export { default as DialogClose } from './DialogClose.vue' 3 | export { default as DialogContent } from './DialogContent.vue' 4 | export { default as DialogDescription } from './DialogDescription.vue' 5 | export { default as DialogFooter } from './DialogFooter.vue' 6 | export { default as DialogHeader } from './DialogHeader.vue' 7 | export { default as DialogScrollContent } from './DialogScrollContent.vue' 8 | export { default as DialogTitle } from './DialogTitle.vue' 9 | export { default as DialogTrigger } from './DialogTrigger.vue' 10 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenu.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuContent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuGroup.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuLabel.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuRadioItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuSeparator.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuShortcut.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuSub.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuSubContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/DropdownMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/dropdown-menu/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DropdownMenu } from './DropdownMenu.vue'; 2 | 3 | export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'; 4 | export { default as DropdownMenuContent } from './DropdownMenuContent.vue'; 5 | export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'; 6 | export { default as DropdownMenuItem } from './DropdownMenuItem.vue'; 7 | export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'; 8 | export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'; 9 | export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'; 10 | export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'; 11 | export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'; 12 | export { default as DropdownMenuSub } from './DropdownMenuSub.vue'; 13 | export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'; 14 | export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'; 15 | export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'; 16 | export { DropdownMenuPortal } from 'radix-vue'; 17 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/FormControl.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/FormDescription.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/FormItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/FormLabel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/FormMessage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/index.ts: -------------------------------------------------------------------------------- 1 | export { default as FormControl } from './FormControl.vue'; 2 | export { default as FormDescription } from './FormDescription.vue'; 3 | export { default as FormItem } from './FormItem.vue'; 4 | export { default as FormLabel } from './FormLabel.vue'; 5 | export { default as FormMessage } from './FormMessage.vue'; 6 | export { FORM_ITEM_INJECTION_KEY } from './injectionKeys'; 7 | export { Field as FormField, Form } from 'vee-validate'; 8 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/injectionKeys.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from 'vue'; 2 | 3 | export const FORM_ITEM_INJECTION_KEY = Symbol() as InjectionKey; 4 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/form/useFormField.ts: -------------------------------------------------------------------------------- 1 | import { FieldContextKey, useFieldError, useIsFieldDirty, useIsFieldTouched, useIsFieldValid } from 'vee-validate'; 2 | import { inject } from 'vue'; 3 | import { FORM_ITEM_INJECTION_KEY } from './injectionKeys'; 4 | 5 | export function useFormField() { 6 | const fieldContext = inject(FieldContextKey); 7 | const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY); 8 | 9 | if (!fieldContext) throw new Error('useFormField should be used within '); 10 | 11 | const { name } = fieldContext; 12 | const id = fieldItemContext; 13 | 14 | const fieldState = { 15 | valid: useIsFieldValid(name), 16 | isDirty: useIsFieldDirty(name), 17 | isTouched: useIsFieldTouched(name), 18 | error: useFieldError(name), 19 | }; 20 | 21 | return { 22 | id, 23 | name, 24 | formItemId: `${id}-form-item`, 25 | formDescriptionId: `${id}-form-item-description`, 26 | formMessageId: `${id}-form-item-message`, 27 | ...fieldState, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/input/Input.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/input/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input.vue' 2 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/label/Label.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/label/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Label } from './Label.vue' 2 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenu.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuContent.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 36 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuIndicator.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuList.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuTrigger.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/NavigationMenuViewport.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/navigation-menu/index.ts: -------------------------------------------------------------------------------- 1 | import { cva } from 'class-variance-authority'; 2 | 3 | export { default as NavigationMenu } from './NavigationMenu.vue'; 4 | export { default as NavigationMenuContent } from './NavigationMenuContent.vue'; 5 | export { default as NavigationMenuItem } from './NavigationMenuItem.vue'; 6 | export { default as NavigationMenuLink } from './NavigationMenuLink.vue'; 7 | export { default as NavigationMenuList } from './NavigationMenuList.vue'; 8 | export { default as NavigationMenuTrigger } from './NavigationMenuTrigger.vue'; 9 | export { default as NavigationMenuViewport } from './NavigationMenuViewport.vue'; 10 | 11 | export const navigationMenuTriggerStyle = cva( 12 | 'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent focus:bg-background focus:text-accent data-[state=open]:bg-background data-[state=open]:text-accent disabled:pointer-events-none disabled:opacity-50', 13 | ); 14 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/PaginationEllipsis.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/PaginationFirst.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/PaginationLast.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/PaginationNext.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/PaginationPrev.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PaginationEllipsis } from './PaginationEllipsis.vue'; 2 | export { default as PaginationFirst } from './PaginationFirst.vue'; 3 | export { default as PaginationLast } from './PaginationLast.vue'; 4 | export { default as PaginationNext } from './PaginationNext.vue'; 5 | export { default as PaginationPrev } from './PaginationPrev.vue'; 6 | export { PaginationList, PaginationListItem, PaginationRoot as Pagination } from 'radix-vue'; 7 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/popover/Popover.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/popover/PopoverContent.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 46 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/popover/PopoverTrigger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/radio-group/RadioGroup.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/radio-group/RadioGroupItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/radio-group/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RadioGroup } from './RadioGroup.vue'; 2 | export { default as RadioGroupItem } from './RadioGroupItem.vue'; 3 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/Select.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectContent.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 60 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectGroup.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectItemText.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectLabel.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectScrollDownButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectScrollUpButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectSeparator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 18 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectTrigger.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/SelectValue.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/select/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select.vue'; 2 | export { default as SelectContent } from './SelectContent.vue'; 3 | export { default as SelectGroup } from './SelectGroup.vue'; 4 | export { default as SelectItem } from './SelectItem.vue'; 5 | export { default as SelectItemText } from './SelectItemText.vue'; 6 | export { default as SelectLabel } from './SelectLabel.vue'; 7 | export { default as SelectScrollDownButton } from './SelectScrollDownButton.vue'; 8 | export { default as SelectScrollUpButton } from './SelectScrollUpButton.vue'; 9 | export { default as SelectSeparator } from './SelectSeparator.vue'; 10 | export { default as SelectTrigger } from './SelectTrigger.vue'; 11 | export { default as SelectValue } from './SelectValue.vue'; 12 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/separator/Separator.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 35 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/separator/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Separator } from './Separator.vue'; 2 | -------------------------------------------------------------------------------- /test-website/simple-cms/nuxt/app/components/ui/textarea/Textarea.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 |