├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── code-block.yml │ ├── palette.yml │ └── picker-starter.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── annotated-image ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── MapNode.vue │ ├── Plugin.vue │ └── main.js └── vue.config.js ├── code-block ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── README.md ├── assets │ ├── code-block-icon.svg │ └── screenshot.png ├── index.html ├── jest.config.js ├── package.json ├── src │ ├── Options │ │ ├── Options.test.ts │ │ ├── Options.ts │ │ ├── defaultHighlightStateOptionValue.ts │ │ └── index.ts │ ├── components │ │ ├── App.tsx │ │ ├── CodeEditor │ │ │ ├── CodeEditor.tsx │ │ │ ├── CodeEditorContent.tsx │ │ │ ├── CodeEditorHeader.tsx │ │ │ ├── colorFromHighlightState.test.ts │ │ │ ├── colorFromHighlightState.tsx │ │ │ ├── colorFromLineState.test.ts │ │ │ ├── colorFromLineState.tsx │ │ │ ├── index.ts │ │ │ ├── mix.tsx │ │ │ ├── nextHighlightState.test.ts │ │ │ ├── nextHighlightState.tsx │ │ │ ├── onChangeSetAction.test.ts │ │ │ ├── onChangeSetAction.tsx │ │ │ ├── onLineClickSetAction.test.ts │ │ │ ├── onLineClickSetAction.tsx │ │ │ ├── toggleLine.test.ts │ │ │ └── toggleLine.ts │ │ ├── CodeMirror │ │ │ ├── CodeMirror.tsx │ │ │ ├── index.ts │ │ │ ├── theme.ts │ │ │ └── useSyncedFunction.tsx │ │ └── ErrorAlert.tsx │ ├── createRootElement.ts │ ├── main.tsx │ ├── storyblok-design │ │ ├── design-tokens.ts │ │ ├── index.ts │ │ └── transition.ts │ ├── style.css │ ├── useFieldPlugin.ts │ ├── utils │ │ ├── index.ts │ │ ├── integerFromString │ │ │ ├── index.ts │ │ │ ├── integerFromString.test.ts │ │ │ └── integerFromString.ts │ │ ├── unique │ │ │ ├── index.ts │ │ │ ├── unique.test.ts │ │ │ └── unique.ts │ │ ├── withLength │ │ │ ├── index.ts │ │ │ ├── withLength.test.ts │ │ │ └── withLength.ts │ │ └── zeros │ │ │ ├── index.ts │ │ │ ├── zeros.test.ts │ │ │ └── zeros.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── community-examples.json ├── folder-selection ├── .env.local.example ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── docs │ ├── demo.gif │ └── screenshot.png ├── index.html ├── package.json ├── src │ ├── App.tsx │ ├── components │ │ └── FieldPlugin.jsx │ ├── createRootElement.ts │ ├── main.tsx │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── generate-readme.js ├── hosted-plugin ├── .env.local.example ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── index.html ├── package.json ├── src │ ├── App.tsx │ ├── components │ │ └── FieldPlugin.tsx │ ├── createRootElement.ts │ ├── main.tsx │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── material-icon-selector ├── Fieldtype.js └── README.md ├── package.json ├── palette ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── README.md ├── assets │ ├── README.md │ ├── icon.svg │ └── screenshot.png ├── babel.config.cjs ├── docs │ └── demo.gif ├── jest.config.cjs ├── package.json ├── public │ └── index.html ├── src │ ├── components │ │ ├── Checkmark.vue │ │ ├── Palette.vue │ │ ├── PalettePlugin.vue │ │ └── Swatch.vue │ ├── entries │ │ └── main.ts │ ├── lib │ │ ├── README.md │ │ ├── components │ │ │ └── FieldModal │ │ │ │ ├── FieldModal.vue │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── loadPlugin.ts │ │ ├── makeStoryblokPluginComponent.ts │ │ ├── pluginPropsDef.ts │ │ └── types │ │ │ ├── PluginComponentProps.ts │ │ │ ├── RootComponentData.ts │ │ │ ├── RootPluginComponentComputed.ts │ │ │ ├── RootPluginComponentMethods.ts │ │ │ ├── RootPluginProps.ts │ │ │ ├── StoryblokFieldType.ts │ │ │ ├── WrapperPluginProps.ts │ │ │ └── index.ts │ └── utils │ │ ├── SRGB.ts │ │ ├── contrastRatio │ │ ├── contrastRatio.test.ts │ │ └── contrastRatio.ts │ │ ├── hexToSRGB │ │ ├── hexToSRGB.test.ts │ │ ├── hexToSRGB.ts │ │ └── index.ts │ │ ├── index.ts │ │ ├── numberFromString │ │ ├── index.ts │ │ ├── numberFromString.test.ts │ │ └── numberFromString.ts │ │ └── relativeLuminance │ │ ├── relativeLuminance.test.ts │ │ └── relativeLuminance.ts ├── tsconfig.json ├── vue.config.cjs └── yarn.lock ├── picker-starter ├── .env.local.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── docs │ ├── get-options-function.png │ ├── loaded-sandbox.png │ ├── open-sandbox-url.png │ ├── passing-options.png │ ├── picker.config-1.png │ ├── picker.config-2.png │ └── screenshot.png ├── field-plugin.config.json ├── index.d.ts ├── index.html ├── package.json ├── src │ ├── App.vue │ ├── components │ │ ├── AddAssetButton │ │ │ ├── AddAssetButton.vue │ │ │ └── index.ts │ │ ├── Avatar │ │ │ ├── Avatar.vue │ │ │ └── index.ts │ │ ├── Card │ │ │ ├── Card.vue │ │ │ ├── CardContent.vue │ │ │ ├── CardFooter.vue │ │ │ └── index.ts │ │ ├── CartList │ │ │ ├── CartList.vue │ │ │ ├── CartListItem.vue │ │ │ ├── CartListItemImage.vue │ │ │ ├── CartListItemImageContainer.vue │ │ │ ├── index.ts │ │ │ └── move │ │ │ │ ├── index.ts │ │ │ │ ├── move.test.ts │ │ │ │ └── move.ts │ │ ├── EmptyScreen │ │ │ ├── EmptyScreen.vue │ │ │ └── index.ts │ │ ├── ErrorNotification │ │ │ ├── ErrorNotification.vue │ │ │ └── index.ts │ │ ├── FieldPlugin.vue │ │ ├── Filter │ │ │ ├── SearchField.vue │ │ │ ├── SelectFilter.vue │ │ │ └── index.ts │ │ ├── Grid │ │ │ ├── Grid.vue │ │ │ ├── GridItem.vue │ │ │ └── index.ts │ │ ├── Icons │ │ │ ├── StoryblokIcon.vue │ │ │ └── index.ts │ │ ├── ItemCard │ │ │ ├── ItemCard.vue │ │ │ └── index.ts │ │ ├── ItemGrid │ │ │ ├── ItemGrid.vue │ │ │ └── index.ts │ │ ├── ItemImage │ │ │ ├── ItemImage.vue │ │ │ └── index.ts │ │ ├── ItemList │ │ │ ├── ItemList.vue │ │ │ ├── ItemListItem.vue │ │ │ └── index.ts │ │ ├── ItemPicker │ │ │ ├── ItemPicker.vue │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ ├── mixin │ │ │ │ ├── disableLoadMore.ts │ │ │ │ ├── isLoadingItems.ts │ │ │ │ ├── isPageEmpty.ts │ │ │ │ ├── mixin.ts │ │ │ │ ├── showCursorPagination.ts │ │ │ │ └── showPagePagination.ts │ │ │ │ └── state │ │ │ │ ├── dispatch.test.ts │ │ │ │ ├── dispatch.ts │ │ │ │ └── types │ │ │ │ ├── Action.ts │ │ │ │ └── State.ts │ │ ├── List │ │ │ ├── List.vue │ │ │ ├── ListItem.vue │ │ │ └── index.ts │ │ ├── ModalPage │ │ │ ├── ModalContainer.vue │ │ │ ├── ModalHeader.vue │ │ │ ├── ModalPage.vue │ │ │ └── index.ts │ │ ├── NonModalPage │ │ │ ├── NonModalPage.vue │ │ │ ├── add-items-label │ │ │ │ ├── addItemsLabel.test.ts │ │ │ │ ├── addItemsLabel.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── NotificationProvider │ │ │ ├── NotificationProvider.vue │ │ │ └── index.ts │ │ ├── Picker │ │ │ ├── Picker.vue │ │ │ ├── index.ts │ │ │ └── pluginPropsDef.ts │ │ ├── Skeleton │ │ │ ├── Skeleton.vue │ │ │ ├── SkeletonCard.vue │ │ │ ├── SkeletonListItem.vue │ │ │ └── index.ts │ │ ├── ValidationError │ │ │ ├── ValidationError.vue │ │ │ └── index.ts │ │ ├── ViewCartButton │ │ │ ├── ViewCartButton.vue │ │ │ ├── added-items-count-label │ │ │ │ ├── addedItemsCountLabel.test.ts │ │ │ │ ├── addedItemsCountLabel.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── ViewModeSwitch │ │ │ ├── ViewModeSwitch.vue │ │ │ └── index.ts │ │ ├── index.ts │ │ └── styles.scss │ ├── composables │ │ ├── index.ts │ │ └── useErrorNotification.ts │ ├── core │ │ ├── basket │ │ │ ├── BasketItem.ts │ │ │ ├── basket.test.ts │ │ │ ├── basket.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── matchCategories │ │ │ ├── index.ts │ │ │ └── matchCategories.ts │ │ ├── matchSearchTerm │ │ │ ├── index.ts │ │ │ ├── matchSearchTerm.test.ts │ │ │ └── matchSearchTerm.ts │ │ ├── service.ts │ │ ├── setup.ts │ │ └── types.ts │ ├── data │ │ ├── categories.ts │ │ ├── index.ts │ │ └── items.ts │ ├── main.ts │ ├── picker.config.ts │ ├── settings.ts │ ├── setupTests.ts │ ├── style.css │ ├── utils │ │ ├── capitalizeWord │ │ │ ├── capitalizeWord.test.ts │ │ │ ├── capitalizeWord.ts │ │ │ └── index.ts │ │ ├── compareName │ │ │ ├── compareName.ts │ │ │ └── index.ts │ │ ├── getPage │ │ │ ├── getPage.test.ts │ │ │ ├── getPage.ts │ │ │ └── index.ts │ │ ├── hasKey │ │ │ ├── hasKey.test.ts │ │ │ ├── hasKey.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── initials │ │ │ ├── index.ts │ │ │ ├── initials.test.ts │ │ │ └── initials.ts │ │ ├── numberFromString │ │ │ ├── index.ts │ │ │ ├── numberFromString.test.ts │ │ │ └── numberFromString.ts │ │ ├── pseudoRandom │ │ │ ├── index.ts │ │ │ ├── pseudoRandom.test.ts │ │ │ └── pseudoRandom.ts │ │ ├── pseudoRandomColor │ │ │ ├── index.ts │ │ │ └── pseudoRandomColor.ts │ │ └── unsafeHash │ │ │ ├── index.ts │ │ │ ├── unsafeHash.test.ts │ │ │ └── unsafeHash.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── slider ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── assets │ ├── README.md │ ├── icon.svg │ └── screenshot.png ├── babel.config.cjs ├── docs │ ├── demo.gif │ ├── gigabit-per-second.gif │ ├── percent.gif │ └── rotation.gif ├── jest.config.cjs ├── package.json ├── public │ ├── icon.svg │ └── index.html ├── src │ ├── components │ │ ├── HorizontalSlider │ │ │ ├── HorizontalSlider.vue │ │ │ ├── index.ts │ │ │ ├── valueFromCoordinate.ts │ │ │ └── xCoordinateFromValue.ts │ │ ├── SliderPlugin.vue │ │ ├── Thumb │ │ │ ├── Thumb.vue │ │ │ └── index.ts │ │ └── Tooltip │ │ │ ├── Tooltip.vue │ │ │ └── index.ts │ ├── entries │ │ ├── preview.ts │ │ └── production.ts │ ├── lib │ │ ├── Plugin.ts │ │ ├── README.md │ │ ├── components │ │ │ └── FieldModal │ │ │ │ ├── FieldModal.vue │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── loadPlugin.ts │ │ ├── makeStoryblokPluginComponent.ts │ │ ├── pluginPropsDef.ts │ │ └── types │ │ │ ├── PluginComponentProps.ts │ │ │ ├── RootComponentData.ts │ │ │ ├── RootPluginComponentComputed.ts │ │ │ ├── RootPluginComponentMethods.ts │ │ │ ├── RootPluginProps.ts │ │ │ ├── StoryblokFieldType.ts │ │ │ ├── WrapperPluginProps.ts │ │ │ └── index.ts │ ├── styles.scss │ └── utils │ │ ├── index.ts │ │ ├── numberFromString │ │ ├── index.ts │ │ ├── numberFromString.test.ts │ │ └── numberFromString.ts │ │ └── roundToNearest │ │ ├── index.ts │ │ ├── roundToNearest.test.ts │ │ └── roundToNearest.ts ├── tsconfig.json ├── vue.config.cjs └── yarn.lock ├── star-rating ├── .env.local.example ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── assets │ └── icon.svg ├── docs │ ├── demo.gif │ ├── screenshot.png │ ├── setup-1.png │ ├── setup-2.png │ ├── setup-3.png │ ├── setup-4.png │ ├── setup-5.png │ ├── setup-6.png │ ├── star-rating-demo.gif │ └── star-rating-screenshot.png ├── index.html ├── package.json ├── src │ ├── App.vue │ ├── components │ │ ├── AmountInvalidAlert.vue │ │ ├── FieldPluginProvider.vue │ │ ├── StarIcon.vue │ │ └── StarRatingField.vue │ ├── main.ts │ ├── style.css │ ├── useFieldPlugin.ts │ ├── utils │ │ ├── convertToRaw.ts │ │ └── index.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── tags ├── .env.local.example ├── .eslintrc.cjs ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── assets │ ├── README.md │ └── icon.svg ├── docs │ ├── demo.gif │ └── screenshot.png ├── index.html ├── package.json ├── src │ ├── App.tsx │ ├── FieldPluginProvider.tsx │ ├── components │ │ └── Tag.tsx │ ├── createRootElement.ts │ ├── main.tsx │ ├── style.css │ ├── useFieldPlugin.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "jest": true, 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:vue/essential" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "vue" 17 | ], 18 | "rules": { 19 | "strict": "off", 20 | "no-console": "off", 21 | "no-unused-vars": "off", 22 | "no-undef": "off", 23 | "import/no-unresolved": "off" 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @storyblok/plugins 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Issue: 2 | 3 | ## What? 4 | 5 | 6 | ## Why? 7 | 8 | 13 | ## How to test? (optional) 14 | -------------------------------------------------------------------------------- /.github/workflows/code-block.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'code-block/**' 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | cache: 'yarn' 18 | - name: Install 19 | run: yarn install 20 | - name: Prettier 21 | run: yarn workspace code-block prettier 22 | - name: Lint 23 | run: yarn workspace code-block lint 24 | - name: Check types 25 | run: yarn workspace code-block check:types 26 | - name: Test 27 | run: yarn workspace code-block test 28 | - name: Build 29 | run: yarn workspace code-block build 30 | deploy: 31 | name: Deploy 32 | runs-on: ubuntu-latest 33 | needs: build 34 | if: github.ref == 'refs/heads/main' 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/setup-node@v3 38 | with: 39 | node-version: 18 40 | cache: 'yarn' 41 | - name: Install 42 | run: yarn install 43 | - name: Build 44 | run: yarn workspace code-block build 45 | - name: Deploy 46 | env: 47 | STORYBLOK_PERSONAL_ACCESS_TOKEN: ${{secrets.STORYBLOK_PERSONAL_ACCESS_TOKEN}} 48 | run: yarn workspace code-block deploy --skipPrompts --name storyblok-code-block --scope partner-portal 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/palette.yml: -------------------------------------------------------------------------------- 1 | name: Palette CI 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'palette/**' 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./palette 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | cache: 'yarn' 21 | - name: Install 22 | run: yarn install 23 | - name: Lint 24 | run: yarn lint 25 | - name: Test 26 | run: yarn test 27 | - name: Build 28 | run: yarn build 29 | -------------------------------------------------------------------------------- /.github/workflows/picker-starter.yml: -------------------------------------------------------------------------------- 1 | name: Picker Starter CI/CD 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'picker-starter/**' 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version-file: 'picker-starter/.nvmrc' 17 | cache: 'yarn' 18 | - name: Install 19 | run: yarn install 20 | - name: Prettier 21 | run: yarn workspace picker-starter prettier 22 | - name: Lint 23 | run: yarn workspace picker-starter lint 24 | - name: Check types 25 | run: yarn workspace picker-starter check:types 26 | - name: Test 27 | run: yarn workspace picker-starter test 28 | - name: Build 29 | run: yarn workspace picker-starter build 30 | deploy: 31 | name: Deploy 32 | runs-on: ubuntu-latest 33 | needs: build 34 | if: github.ref == 'refs/heads/main' 35 | steps: 36 | - uses: actions/checkout@v3 37 | - uses: actions/setup-node@v3 38 | with: 39 | node-version-file: 'picker-starter/.nvmrc' 40 | cache: 'yarn' 41 | - name: Install 42 | run: yarn install 43 | - name: Build 44 | run: yarn workspace picker-starter build 45 | - name: Deploy 46 | env: 47 | STORYBLOK_PERSONAL_ACCESS_TOKEN: ${{secrets.STORYBLOK_PERSONAL_ACCESS_TOKEN}} 48 | run: yarn workspace picker-starter deploy --skipPrompts --name sb-picker-starter --scope partner-portal 49 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "singleAttributePerLine": true 8 | } 9 | -------------------------------------------------------------------------------- /annotated-image/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /annotated-image/README.md: -------------------------------------------------------------------------------- 1 | # Annotated Image / Image Map 2 | 3 | This field-type allows users to add numbers on images for tutorials, so such numbers can be used in the frontend to refer to points of the image from the text content. 4 | 5 | Image Map Preview 6 | 7 | Name | Description | Author 8 | ------------ | ------------- | ------------- 9 | Annotated Image | Simple Image Maps for tutorials | [Christian Zoppi](https://github.com/christianzoppi) 10 | -------------------------------------------------------------------------------- /annotated-image/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /annotated-image/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | storyblok-fieldtype 9 | 10 | 11 | 12 |
13 | 14 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /annotated-image/src/main.js: -------------------------------------------------------------------------------- 1 | import Plugin from './Plugin.vue' 2 | 3 | if (process.env.NODE_ENV == 'development') { 4 | 5 | window.Fieldtype = Plugin 6 | let customComp = window.Storyblok.vue.extend(window.Fieldtype); 7 | window.Storyblok.vue.component('custom-plugin', customComp); 8 | window.StoryblokPluginRegistered = true; 9 | 10 | } else { 11 | 12 | let init = Plugin.methods.initWith() 13 | window.storyblok.field_types[init.plugin] = Plugin 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /annotated-image/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configureWebpack: { 3 | output: { 4 | filename: 'export.js' 5 | }, 6 | optimization: { 7 | splitChunks: false 8 | } 9 | }, 10 | filenameHashing: false, 11 | runtimeCompiler: true, 12 | productionSourceMap: false, 13 | css: { 14 | extract: false, 15 | loaderOptions: { 16 | sass: { 17 | additionalData: ` 18 | @import "~storyblok-design-system/src/assets/styles/variables.scss"; 19 | @import "~storyblok-design-system/src/assets/styles/global.scss"; 20 | @import "~storyblok-design-system/src/assets/styles/resets.scss"; 21 | @import "~storyblok-design-system/src/assets/styles/mixins.scss";` 22 | }, 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /code-block/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /code-block/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | React: true, 5 | JSX: true, 6 | }, 7 | env: { 8 | browser: true, 9 | es2021: true, 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect', 14 | }, 15 | }, 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:react/recommended', 19 | 'plugin:@typescript-eslint/recommended', 20 | ], 21 | overrides: [], 22 | parser: '@typescript-eslint/parser', 23 | 24 | parserOptions: { 25 | tsconfigRootDir: __dirname, 26 | ecmaVersion: 'latest', 27 | sourceType: 'module', 28 | project: ['./tsconfig.json', './tsconfig.node.json'], 29 | }, 30 | plugins: ['react', '@typescript-eslint'], 31 | rules: {}, 32 | } 33 | -------------------------------------------------------------------------------- /code-block/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Output 27 | stats.html -------------------------------------------------------------------------------- /code-block/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | stats.html -------------------------------------------------------------------------------- /code-block/assets/code-block-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /code-block/assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/code-block/assets/screenshot.png -------------------------------------------------------------------------------- /code-block/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Storyblok Field Plugin 10 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /code-block/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | } 5 | -------------------------------------------------------------------------------- /code-block/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-block", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "check:types": "tsc --noEmit", 9 | "lint": "eslint .", 10 | "prettier": "prettier --check .", 11 | "build": "tsc && vite build", 12 | "test": "jest", 13 | "preview": "vite preview", 14 | "deploy": "field-plugin deploy" 15 | }, 16 | "dependencies": { 17 | "@emotion/react": "^11.11.0", 18 | "@storyblok/field-plugin": "0.0.1-beta.1", 19 | "codemirror": "^6.0.1", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "zod": "^3.21.4" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "29.0.3", 26 | "@types/react": "^18.0.28", 27 | "@types/react-dom": "^18.0.11", 28 | "@typescript-eslint/eslint-plugin": "latest", 29 | "@typescript-eslint/parser": "5.55.0", 30 | "@vitejs/plugin-react": "^3.1.0", 31 | "@storyblok/field-plugin-cli": "^0.0.1-beta.1", 32 | "eslint": "latest", 33 | "eslint-plugin-react": "7.30.0", 34 | "jest": "29.3.1", 35 | "jest-environment-jsdom": "29.4.2", 36 | "rollup-plugin-visualizer": "^5.9.0", 37 | "typescript": "^5.0.4", 38 | "vite": "^4.2.0", 39 | "vite-plugin-css-injected-by-js": "3.1.0", 40 | "ts-jest": "29.0.3", 41 | "prettier": "^2.8.8" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /code-block/src/Options/defaultHighlightStateOptionValue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In a separate file from Options.ts, becase we don't want to expose it to the outside of the module 3 | */ 4 | export const defaultHighlightStateOptionValue = { 5 | value: '', 6 | color: 'transparent', 7 | } 8 | -------------------------------------------------------------------------------- /code-block/src/Options/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Options' 2 | -------------------------------------------------------------------------------- /code-block/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useMemo } from 'react' 2 | import { useFieldPlugin } from '../useFieldPlugin' 3 | import { CodeEditor } from './CodeEditor' 4 | import { 5 | CodeEditorContent, 6 | parseCodeEditorState, 7 | } from './CodeEditor/CodeEditorContent' 8 | import { parseOptions } from '../Options' 9 | import { ErrorAlert } from './ErrorAlert' 10 | 11 | export const App: FunctionComponent = () => { 12 | const { type, data, actions, error } = useFieldPlugin() 13 | 14 | const options = useMemo(() => parseOptions(data?.options), [data?.options]) 15 | const content = useMemo( 16 | () => parseCodeEditorState(data?.content), 17 | [data?.content], 18 | ) 19 | 20 | if (type === 'loading') { 21 | return <> 22 | } 23 | 24 | if (type === 'error') { 25 | console.error(error.message) 26 | return <> 27 | } 28 | if (options instanceof Error) { 29 | return ( 30 | {options.message} 31 | ) 32 | } 33 | 34 | const setContent = ( 35 | setStateAction: CodeEditorContent | ((content: CodeEditorContent) => void), 36 | ) => { 37 | if (typeof setStateAction === 'function') { 38 | actions.setContent((oldContent) => 39 | setStateAction(parseCodeEditorState(oldContent)), 40 | ) 41 | } else { 42 | actions.setContent(setStateAction) 43 | } 44 | } 45 | 46 | return ( 47 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/CodeEditorContent.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const CodeEditorContentSchema = z.object({ 4 | code: z.string(), 5 | highlightStates: z.array(z.string()).optional(), 6 | title: z.string().optional(), 7 | language: z.string().optional(), 8 | lineNumberStart: z.number().optional(), 9 | }) 10 | 11 | export type CodeEditorContent = z.infer 12 | 13 | export const parseCodeEditorState = (data: unknown): CodeEditorContent => { 14 | const content = CodeEditorContentSchema.safeParse(data) 15 | if (!content.success) { 16 | return { 17 | code: '', 18 | } 19 | } 20 | 21 | return content.data 22 | } 23 | 24 | export type HighlightState = 25 | Required['highlightStates'][number] 26 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/colorFromHighlightState.test.ts: -------------------------------------------------------------------------------- 1 | import { colorFromHighlightState } from './colorFromHighlightState' 2 | 3 | describe('colorFromHighlightState', () => { 4 | it('falls back to the default color when the value is not found in the array', () => { 5 | expect(colorFromHighlightState([{ value: '', color: 'red' }], 'blah')).toBe( 6 | 'red', 7 | ) 8 | }) 9 | it('finds the color', () => { 10 | expect( 11 | colorFromHighlightState( 12 | [ 13 | { value: '', color: 'red' }, 14 | { value: 'pear', color: '#0F0' }, 15 | ], 16 | 'pear', 17 | ), 18 | ).toBe('#0F0') 19 | }) 20 | it('picks the first options when there is ambiguity', () => { 21 | expect( 22 | colorFromHighlightState( 23 | [ 24 | { value: '', color: 'red' }, 25 | { value: 'pear', color: '#0F0' }, 26 | { value: 'pear', color: '#AFA' }, 27 | ], 28 | 'pear', 29 | ), 30 | ).toBe('#0F0') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/colorFromHighlightState.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defaultHighlightStateOption, 3 | HighlightStateOptions, 4 | } from '../../Options' 5 | 6 | export const colorFromHighlightState = ( 7 | highlightStateOptions: HighlightStateOptions, 8 | highlightState: string, 9 | ): string => { 10 | const defaultColor = defaultHighlightStateOption(highlightStateOptions).color 11 | return ( 12 | highlightStateOptions.find((it) => it.value === highlightState)?.color ?? 13 | defaultColor 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/colorFromLineState.test.ts: -------------------------------------------------------------------------------- 1 | import { colorFromLineState } from './colorFromLineState' 2 | 3 | describe('colorFromLineState', () => { 4 | it('falls back to the default color when the value is not found in the array', () => { 5 | expect(colorFromLineState([{ value: '', color: 'red' }], 'blah')).toBe( 6 | 'red', 7 | ) 8 | }) 9 | it('finds the color', () => { 10 | expect( 11 | colorFromLineState( 12 | [ 13 | { value: '', color: 'red' }, 14 | { value: 'pear', color: '#0F0' }, 15 | ], 16 | 'pear', 17 | ), 18 | ).toBe('#0F0') 19 | }) 20 | it('picks the first options when there is ambiguity', () => { 21 | expect( 22 | colorFromLineState( 23 | [ 24 | { value: '', color: 'red' }, 25 | { value: 'pear', color: '#0F0' }, 26 | { value: 'pear', color: '#AFA' }, 27 | ], 28 | 'pear', 29 | ), 30 | ).toBe('#0F0') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/colorFromLineState.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defaultHighlightStateOption, 3 | HighlightStateOptions, 4 | } from '../../Options' 5 | 6 | export const colorFromLineState = ( 7 | lineStateOptions: HighlightStateOptions, 8 | lineState: string, 9 | ): string => { 10 | const defaultColor = defaultHighlightStateOption(lineStateOptions).color 11 | return ( 12 | lineStateOptions.find((it) => it.value === lineState)?.color ?? defaultColor 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CodeEditor' 2 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/mix.tsx: -------------------------------------------------------------------------------- 1 | export const mix = (srgb1: string, srgb2: string, coeff: number) => 2 | `color-mix(in srgb, ${srgb1} ${coeff * 100}%, ${srgb2})` 3 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/nextHighlightState.test.ts: -------------------------------------------------------------------------------- 1 | import { nextHighlightState } from './nextHighlightState' 2 | 3 | describe('nextHighlightState', () => { 4 | it('returns the same element when the array has length 1', () => { 5 | expect(nextHighlightState(['1'], '')).toEqual('1') 6 | }) 7 | it('returns the next element when the array has length > 1', () => { 8 | expect(nextHighlightState(['1', '2'], '0')).toEqual('1') 9 | expect(nextHighlightState(['1', '2', '3', '4'], '2')).toBe('3') 10 | }) 11 | it('returns to the first index when the state is at the last place in the array', () => { 12 | expect(nextHighlightState(['0', '1'], '1')).toEqual('0') 13 | }) 14 | it('returns the first element when the current state does not exist in the array', () => { 15 | expect(nextHighlightState(['0', '1'], 'abc')).toEqual('0') 16 | }) 17 | it('ignores duplicate values', () => { 18 | expect(nextHighlightState(['0', '0', '1'], '0')).toEqual('1') 19 | expect(nextHighlightState(['0', '0', '1', '1'], '1')).toEqual('0') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/nextHighlightState.tsx: -------------------------------------------------------------------------------- 1 | import { HighlightState } from './CodeEditorContent' 2 | import { unique } from '../../utils' 3 | 4 | /** 5 | * Rotates a given highlightState to the next state. 6 | * For example, when the user clicks on a line which is "+", it rotates to "-". 7 | * If they click again, it goes back to "". 8 | * @param currentState 9 | * @param highlightStateValues 10 | */ 11 | export const nextHighlightState = ( 12 | highlightStateValues: [string, ...string[]], 13 | currentState: string, 14 | ): HighlightState => { 15 | const defaultValue = highlightStateValues[0] 16 | const uniqueHighlightStateValues = 17 | unique(highlightStateValues) ?? defaultValue 18 | return ( 19 | uniqueHighlightStateValues[ 20 | (uniqueHighlightStateValues.indexOf(currentState) + 1) % 21 | uniqueHighlightStateValues.length 22 | ] ?? defaultValue 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/onChangeSetAction.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlockOptions, defaultHighlightStateOption } from '../../Options' 2 | import { CodeEditorContent } from './CodeEditorContent' 3 | import { withLength } from '../../utils' 4 | 5 | export const onChangeSetAction = 6 | ( 7 | options: CodeBlockOptions['highlightStates'], 8 | value: string, 9 | lineCount: number, 10 | ) => 11 | (content: CodeEditorContent): CodeEditorContent => ({ 12 | ...content, 13 | code: value, 14 | highlightStates: options 15 | ? withLength( 16 | content.highlightStates ?? [], 17 | lineCount, 18 | defaultHighlightStateOption(options).value, 19 | ) 20 | : undefined, 21 | }) 22 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/onLineClickSetAction.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlockOptions, defaultHighlightStateOption } from '../../Options' 2 | import { CodeEditorContent } from './CodeEditorContent' 3 | import { toggleLine } from './toggleLine' 4 | import { withLength } from '../../utils' 5 | 6 | export const onLineClickSetAction = 7 | ( 8 | options: CodeBlockOptions['highlightStates'], 9 | lineCount: number, 10 | line: number, 11 | ) => 12 | (content: CodeEditorContent): CodeEditorContent => ({ 13 | ...content, 14 | highlightStates: options 15 | ? toggleLine( 16 | options, 17 | withLength( 18 | content.highlightStates ?? [], 19 | lineCount, 20 | defaultHighlightStateOption(options).value, 21 | ), 22 | line, 23 | ) 24 | : undefined, 25 | }) 26 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/toggleLine.test.ts: -------------------------------------------------------------------------------- 1 | import { toggleLine } from './toggleLine' 2 | import { HighlightState } from './CodeEditorContent' 3 | import { nextHighlightState } from './nextHighlightState' 4 | import { HighlightStateOptions } from '../../Options' 5 | 6 | const highlightStateOptions = [ 7 | { 8 | value: '', 9 | color: 'transparent', 10 | }, 11 | { 12 | value: 'a', 13 | color: '#0F0', 14 | }, 15 | { 16 | value: 'b', 17 | color: '#FF0', 18 | }, 19 | { 20 | value: 'c', 21 | color: '#00F', 22 | }, 23 | ] satisfies HighlightStateOptions 24 | 25 | describe('toggleLine', () => { 26 | it('returns an identical array when the index is out of bounds', () => { 27 | const arr = ['', '', ''] satisfies HighlightState[] 28 | expect(toggleLine(highlightStateOptions, arr, -1)).toEqual(arr) 29 | expect(toggleLine(highlightStateOptions, arr, 20)).toEqual(arr) 30 | }) 31 | it('toggles the indicated line', () => { 32 | expect(toggleLine(highlightStateOptions, ['', '', '', ''], 2)).toEqual([ 33 | '', 34 | '', 35 | 'a', 36 | '', 37 | ]) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /code-block/src/components/CodeEditor/toggleLine.ts: -------------------------------------------------------------------------------- 1 | import { HighlightState } from './CodeEditorContent' 2 | import { nextHighlightState } from './nextHighlightState' 3 | import { HighlightStateOptions } from '../../Options' 4 | 5 | /** 6 | * Toggles an index in the array 7 | * @param highlightStateOptions 8 | * @param highlightStates a list of line states 9 | * @param lineNumber the line to toggle 10 | * @returns a new array where the index at lineNumber has been toggled 11 | */ 12 | export const toggleLine = ( 13 | highlightStateOptions: HighlightStateOptions, 14 | highlightStates: HighlightState[], 15 | lineNumber: number, 16 | ): HighlightState[] => { 17 | // .map does not preserve the information that the array has at least one element 18 | const [first, ...rest] = highlightStateOptions 19 | const highlightStateValues = [ 20 | first.value, 21 | ...rest.map((it) => it.value), 22 | ] satisfies [string, ...string[]] 23 | return highlightStates.map((state, line) => 24 | line === lineNumber 25 | ? nextHighlightState(highlightStateValues, state) 26 | : state, 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /code-block/src/components/CodeMirror/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CodeMirror' 2 | -------------------------------------------------------------------------------- /code-block/src/components/CodeMirror/useSyncedFunction.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react' 2 | 3 | /** 4 | * Returns a function whose reference never changes. 5 | * When invoked, this function will call the function that was most recently passed to this hook. 6 | * This function is needed because the components from react-codemirror2 only recognized those functions 7 | * that were passed on the initial render. 8 | * @param callback 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/ban-types 11 | export const useSyncedFunction = ( 12 | callback: C, 13 | ): NonNullable => { 14 | const callbackRef = useRef(callback) 15 | useEffect(() => { 16 | callbackRef.current = callback 17 | }, [callback]) 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | // @ts-ignore 20 | return useCallback>((...args) => { 21 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 22 | callbackRef.current?.(...args) 23 | }, []) 24 | } 25 | -------------------------------------------------------------------------------- /code-block/src/components/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, ReactNode } from 'react' 2 | import { css } from '@emotion/react' 3 | import { red_25, sb_dark_blue } from '../storyblok-design' 4 | 5 | export const ErrorAlert: FunctionComponent<{ 6 | title?: ReactNode 7 | children?: ReactNode 8 | }> = (props) => ( 9 |
22 |

27 | {props.title} 28 |

29 |

{props.children}

30 |
31 | ) 32 | -------------------------------------------------------------------------------- /code-block/src/createRootElement.ts: -------------------------------------------------------------------------------- 1 | export const createRootElement = (id?: string): HTMLElement => { 2 | const rootElement = document.createElement('div') 3 | rootElement.id = id ?? 'app' 4 | return rootElement 5 | } 6 | -------------------------------------------------------------------------------- /code-block/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { createRootElement } from './createRootElement' 4 | import { App } from './components/App' 5 | import './style.css' 6 | 7 | const rootNode = createRootElement() 8 | document.body.appendChild(rootNode) 9 | 10 | createRoot(rootNode).render() 11 | 12 | // This error replaces another error which message is harder to understand and impossible to avoid util the issue https://github.com/storyblok/field-plugin/issues/107 has been resolved. 13 | throw new Error( 14 | `This error can be safely ignored. It is caused by the legacy field plugin API. See issue https://github.com/storyblok/field-plugin/issues/107`, 15 | ) 16 | -------------------------------------------------------------------------------- /code-block/src/storyblok-design/design-tokens.ts: -------------------------------------------------------------------------------- 1 | export const sb_green = '#00b3b0' 2 | export const sb_green_75 = '#40c6c4' 3 | export const sb_green_50 = '#7fd9d7' 4 | export const sb_green_25 = '#d9f4f3' 5 | export const sb_dark_blue = '#1b243f' 6 | export const sb_dark_blue_hover = '#303850' 7 | export const sb_dark_blue_75 = '#545b6f' 8 | export const sb_dark_blue_50 = '#8d919f' 9 | export const sb_dark_blue_25 = '#c6c8cf' 10 | export const green = '#2db47d' 11 | export const green_75 = '#62c79e' 12 | export const green_50 = '#96d9be' 13 | export const green_25 = '#caecde' 14 | export const green_disabled = '#004e4c' 15 | export const yellow = '#fbce41' 16 | export const yellow_75 = '#fcdb71' 17 | export const yellow_50 = '#fde6a0' 18 | export const yellow_25 = '#fef3cf' 19 | export const blue = '#395ece' 20 | export const blue_75 = '#6b87db' 21 | export const blue_50 = '#9caee6' 22 | export const blue_25 = '#cdd7f3' 23 | export const orange = '#ffac00' 24 | export const orange_75 = '#ffc140' 25 | export const orange_50 = '#ffd57f' 26 | export const orange_25 = '#ffeabf' 27 | export const red = '#ff6159' 28 | export const red_75 = '#ff8983' 29 | export const red_50 = '#ffb0ac' 30 | export const red_25 = '#ffd7d5' 31 | export const light = '#dfe3e8' 32 | export const light_75 = '#e7eaee' 33 | export const light_50 = '#eff1f3' 34 | export const light_25 = '#f7f8f9' 35 | export const light_gray = '#b1b5be' 36 | export const black = '#101525' 37 | export const white = '#ffffff' 38 | -------------------------------------------------------------------------------- /code-block/src/storyblok-design/index.ts: -------------------------------------------------------------------------------- 1 | export * from './design-tokens' 2 | export * from './transition' 3 | -------------------------------------------------------------------------------- /code-block/src/storyblok-design/transition.ts: -------------------------------------------------------------------------------- 1 | export const transition = (...properties: string[]) => 2 | properties 3 | .map((property) => `${property} 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms`) 4 | .join(', ') 5 | -------------------------------------------------------------------------------- /code-block/src/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | overflow: hidden; 3 | } 4 | 5 | :root { 6 | font-family: 'Roboto', sans-serif; 7 | font-style: normal; 8 | line-height: 1.5; 9 | font-weight: 400; 10 | font-size: 1rem; 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | #app { 19 | width: 100%; 20 | } 21 | 22 | /* CSS baseline */ 23 | 24 | body { 25 | margin: 0; 26 | display: flex; 27 | box-sizing: border-box; 28 | } 29 | 30 | h1, 31 | h2, 32 | h3, 33 | h4, 34 | h5, 35 | h6, 36 | p { 37 | margin: 0; 38 | } 39 | 40 | p { 41 | font-size: 0.875em; 42 | } 43 | -------------------------------------------------------------------------------- /code-block/src/useFieldPlugin.ts: -------------------------------------------------------------------------------- 1 | import { createFieldPlugin, FieldPluginResponse } from '@storyblok/field-plugin' 2 | import { useEffect, useState } from 'react' 3 | 4 | type UseFieldPlugin = () => FieldPluginResponse 5 | 6 | export const useFieldPlugin: UseFieldPlugin = () => { 7 | const [state, setState] = useState({ 8 | type: 'loading', 9 | }) 10 | 11 | useEffect(() => { 12 | return createFieldPlugin(setState) 13 | }, []) 14 | 15 | return state 16 | } 17 | -------------------------------------------------------------------------------- /code-block/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zeros' 2 | export * from './withLength' 3 | export * from './unique' 4 | export * from './integerFromString' 5 | -------------------------------------------------------------------------------- /code-block/src/utils/integerFromString/index.ts: -------------------------------------------------------------------------------- 1 | export * from './integerFromString' 2 | -------------------------------------------------------------------------------- /code-block/src/utils/integerFromString/integerFromString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses an integer from a string without any suprises. Strings that describe numbers without any other characters yield 3 | * `number`. All other combination of characters yield `undefined`. 4 | */ 5 | export const integerFromString = (str: string): number | undefined => { 6 | const parsed = Number(str) 7 | return str !== '' && 8 | !hasWhiteSpace(str) && 9 | isInt(parsed) && 10 | !isNaN(parsed) && 11 | isFinite(parsed) 12 | ? parsed 13 | : undefined 14 | } 15 | 16 | const isInt = (n: number): boolean => Number(n) === n && n % 1 === 0 17 | 18 | /** 19 | * @param str 20 | * @returns `true` if any character is a whitespace. 21 | */ 22 | const hasWhiteSpace = (str: string): boolean => { 23 | return /\s+/.test(str) 24 | } 25 | -------------------------------------------------------------------------------- /code-block/src/utils/unique/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unique' 2 | -------------------------------------------------------------------------------- /code-block/src/utils/unique/unique.test.ts: -------------------------------------------------------------------------------- 1 | import { unique } from './unique' 2 | 3 | describe('unique()', () => { 4 | it('should not affect arrays with unique elements', () => { 5 | expect(unique(['a', 'b', 'c'])).toStrictEqual(['a', 'b', 'c']) 6 | }) 7 | it('should remove duplicates', () => { 8 | expect(unique(['a', 'b', 'b', 'c'])).toStrictEqual(['a', 'b', 'c']) 9 | }) 10 | it('should remove triplets', () => { 11 | expect(unique(['a', 'b', 'b', 'c', 'b'])).toStrictEqual(['a', 'b', 'c']) 12 | }) 13 | it('should remove many ', () => { 14 | expect(unique(['a', 'b', 'a', 'b', 'c', 'a', 'b', 'c', 'b'])).toStrictEqual( 15 | ['a', 'b', 'c'], 16 | ) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /code-block/src/utils/unique/unique.ts: -------------------------------------------------------------------------------- 1 | export const unique = (arr: string[]): string[] => 2 | arr.filter((value, index, array) => array.indexOf(value) === index) 3 | -------------------------------------------------------------------------------- /code-block/src/utils/withLength/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withLength' 2 | -------------------------------------------------------------------------------- /code-block/src/utils/withLength/withLength.test.ts: -------------------------------------------------------------------------------- 1 | import { withLength } from './withLength' 2 | 3 | describe('withLength', () => { 4 | it('returns the same array when the length is expected, reserving the reference', () => { 5 | const arr = [1, 2, 3, 4] 6 | expect(withLength(arr, 4, 123)).toBe(arr) 7 | }) 8 | it('appends', () => { 9 | const arr = [1, 2] 10 | expect(withLength(arr, 4, 1)).toEqual([...arr, 1, 1]) 11 | }) 12 | it('trims from the tail', () => { 13 | const arr = [1, 2, 3, 4] 14 | expect(withLength(arr, 2, 1)).toEqual([1, 2]) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /code-block/src/utils/withLength/withLength.ts: -------------------------------------------------------------------------------- 1 | import { zeros } from '../../utils' 2 | 3 | /** 4 | * Given an array a new length, returns 5 | * 1. the _same_ array if the length match, 6 | * 2. trims the array from the tail if the array is too long, 7 | * 3. adds new items to the tail to the array if the array is too short 8 | * @param highlightStates 9 | * @param itemCount 10 | * @param fillWith 11 | */ 12 | export const withLength = ( 13 | highlightStates: T[], 14 | itemCount: number, 15 | fillWith: T, 16 | ) => { 17 | const lineCountDiff = itemCount - highlightStates.length 18 | if (lineCountDiff > 0) { 19 | // lines were added -> append 20 | return [...highlightStates, ...zeros(lineCountDiff).map(() => fillWith)] 21 | } 22 | if (lineCountDiff < 0) { 23 | // lines were removed -> trim 24 | return highlightStates.slice(0, itemCount) 25 | } 26 | // no lines were added or removed -> preserve reference 27 | return highlightStates 28 | } 29 | -------------------------------------------------------------------------------- /code-block/src/utils/zeros/index.ts: -------------------------------------------------------------------------------- 1 | export * from './zeros' 2 | -------------------------------------------------------------------------------- /code-block/src/utils/zeros/zeros.test.ts: -------------------------------------------------------------------------------- 1 | import { zeros } from './zeros' 2 | 3 | describe('zeros', () => { 4 | it('can create empty arrays', () => { 5 | expect(zeros(0)).toEqual([]) 6 | }) 7 | it('creates arrays of variable lengths', () => { 8 | expect(zeros(0)).toHaveLength(0) 9 | expect(zeros(1)).toHaveLength(1) 10 | expect(zeros(5)).toHaveLength(5) 11 | expect(zeros(10)).toHaveLength(10) 12 | expect(zeros(512)).toHaveLength(512) 13 | }) 14 | test('that the arrays contain zeros', () => { 15 | expect(zeros(1)).toEqual([0]) 16 | expect(zeros(2)).toEqual([0, 0]) 17 | expect(zeros(3)).toEqual([0, 0, 0]) 18 | expect(zeros(10)).toEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) 19 | }) 20 | it('throws exceptions for array length of floating decimal point', () => { 21 | expect(() => zeros(234.34)).toThrow() 22 | }) 23 | it('throws exceptions for array length of negative numbers', () => { 24 | expect(() => zeros(-1)).toThrow() 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /code-block/src/utils/zeros/zeros.ts: -------------------------------------------------------------------------------- 1 | export const zeros = (length: number): number[] => Array(length).fill(0) 2 | -------------------------------------------------------------------------------- /code-block/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /code-block/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@emotion/react", 19 | "noUncheckedIndexedAccess": true 20 | }, 21 | "include": ["src"], 22 | "references": [ 23 | { 24 | "path": "./tsconfig.node.json" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /code-block/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["jest.config.js", "vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /community-examples.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Storyblok Meta Image", 4 | "description": "A Storyblok field-type plugin which provides an image field with meta data", 5 | "githubUrl": "https://github.com/maoberlehner/storyblok-meta-image" 6 | }, 7 | { 8 | "name": "Hotspot Editor", 9 | "description": "A Storyblok field-type to create hotspot images", 10 | "githubUrl": "https://github.com/cartok/storyblok-hotspot-editor" 11 | } 12 | ] -------------------------------------------------------------------------------- /folder-selection/.env.local.example: -------------------------------------------------------------------------------- 1 | STORYBLOK_PERSONAL_ACCESS_TOKEN= 2 | -------------------------------------------------------------------------------- /folder-selection/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | React: true, 5 | JSX: true, 6 | }, 7 | env: { 8 | browser: true, 9 | es2021: true, 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect', 14 | }, 15 | }, 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:react/recommended', 19 | 'plugin:@typescript-eslint/recommended', 20 | ], 21 | overrides: [], 22 | parser: '@typescript-eslint/parser', 23 | 24 | parserOptions: { 25 | tsconfigRootDir: __dirname, 26 | ecmaVersion: 'latest', 27 | sourceType: 'module', 28 | project: ['./tsconfig.json', './tsconfig.node.json'], 29 | }, 30 | plugins: ['react', '@typescript-eslint'], 31 | rules: { 32 | 'react/prop-types': 'off', 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /folder-selection/.gitignore: -------------------------------------------------------------------------------- 1 | # env files except for .env.local.example 2 | .env 3 | .env.* 4 | !.env.local.example 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | lerna-debug.log* 14 | 15 | node_modules 16 | dist 17 | dist-ssr 18 | *.local 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /folder-selection/.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.0 -------------------------------------------------------------------------------- /folder-selection/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "singleAttributePerLine": true 8 | } 9 | -------------------------------------------------------------------------------- /folder-selection/docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/folder-selection/docs/demo.gif -------------------------------------------------------------------------------- /folder-selection/docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/folder-selection/docs/screenshot.png -------------------------------------------------------------------------------- /folder-selection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | Storyblok Field Plugin 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /folder-selection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "folder-selection", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "deploy": "npm run build && npx @storyblok/field-plugin-cli@latest deploy" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.11.1", 14 | "@emotion/styled": "^11.11.0", 15 | "@mui/material": "^5.14.7", 16 | "@storyblok/field-plugin": "1.0.1", 17 | "@storyblok/mui": "0.2.0", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.28", 23 | "@types/react-dom": "^18.0.11", 24 | "@typescript-eslint/eslint-plugin": "6.1.0", 25 | "@typescript-eslint/parser": "6.1.0", 26 | "@vitejs/plugin-react": "^3.1.0", 27 | "eslint": "latest", 28 | "eslint-plugin-react": "7.30.0", 29 | "typescript": "5.1.6", 30 | "vite": "^4.2.2", 31 | "vite-plugin-css-injected-by-js": "3.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /folder-selection/src/App.tsx: -------------------------------------------------------------------------------- 1 | import FieldPlugin from './components/FieldPlugin' 2 | import { FunctionComponent } from 'react' 3 | import { useFieldPlugin } from '@storyblok/field-plugin/react' 4 | 5 | import { CssBaseline, GlobalStyles, ThemeProvider } from '@mui/material' 6 | import { lightTheme } from '@storyblok/mui' 7 | 8 | const App: FunctionComponent = () => { 9 | const { type, error } = useFieldPlugin() 10 | 11 | if (type === 'loading') { 12 | return 13 | } else if (type === 'error') { 14 | return 15 | } else { 16 | return ( 17 | 18 | 19 | 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | const Loading: FunctionComponent = () =>

Loading...

31 | const Error: FunctionComponent<{ error: Error }> = (props) => { 32 | console.error(props.error) 33 | return

An error occured, please see the console for more details.

34 | } 35 | export default App 36 | -------------------------------------------------------------------------------- /folder-selection/src/createRootElement.ts: -------------------------------------------------------------------------------- 1 | export const createRootElement = (id?: string): HTMLElement => { 2 | // In production, `#app` may or may not exist. 3 | /* eslint-disable functional/immutable-data */ 4 | const rootElement = document.createElement('div') 5 | rootElement.id = id ?? 'app' 6 | return rootElement 7 | } 8 | -------------------------------------------------------------------------------- /folder-selection/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | import './style.css' 5 | import { createRootElement } from './createRootElement' 6 | 7 | const rootNode = createRootElement() 8 | document.body.appendChild(rootNode) 9 | 10 | createRoot(rootNode).render() 11 | 12 | // This error replaces another error which message is harder to understand and impossible to avoid util the issue https://github.com/storyblok/field-plugin/issues/107 has been resolved. 13 | throw new Error( 14 | `This error can be safely ignored. It is caused by the legacy field plugin API. See issue https://github.com/storyblok/field-plugin/issues/107`, 15 | ) 16 | -------------------------------------------------------------------------------- /folder-selection/src/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'); 2 | 3 | #app { 4 | width: 100%; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | display: flex; 10 | box-sizing: border-box; 11 | background-color: transparent; 12 | } 13 | -------------------------------------------------------------------------------- /folder-selection/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /folder-selection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [ 21 | { 22 | "path": "./tsconfig.node.json" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /folder-selection/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": [ 9 | "vite.config.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /folder-selection/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import cssInjectedByJs from 'vite-plugin-css-injected-by-js' 4 | import { plugins } from '@storyblok/field-plugin/vite' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), cssInjectedByJs(), ...plugins], 9 | build: { 10 | rollupOptions: { 11 | output: { 12 | format: 'commonjs', 13 | entryFileNames: `[name].js`, 14 | chunkFileNames: `[name].js`, 15 | assetFileNames: `[name].[ext]`, 16 | }, 17 | }, 18 | }, 19 | server: { 20 | port: 8080, 21 | host: true, 22 | }, 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /hosted-plugin/.env.local.example: -------------------------------------------------------------------------------- 1 | STORYBLOK_PERSONAL_ACCESS_TOKEN= 2 | -------------------------------------------------------------------------------- /hosted-plugin/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | globals: { 4 | React: true, 5 | JSX: true, 6 | }, 7 | env: { 8 | browser: true, 9 | es2021: true, 10 | }, 11 | settings: { 12 | react: { 13 | version: 'detect', 14 | }, 15 | }, 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:react/recommended', 19 | 'plugin:@typescript-eslint/recommended', 20 | ], 21 | overrides: [], 22 | parser: '@typescript-eslint/parser', 23 | 24 | parserOptions: { 25 | tsconfigRootDir: __dirname, 26 | ecmaVersion: 'latest', 27 | sourceType: 'module', 28 | project: ['./tsconfig.json', './tsconfig.node.json'], 29 | }, 30 | plugins: ['react', '@typescript-eslint'], 31 | rules: { 32 | 'react/prop-types': 'off', 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hosted-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # env files except for .env.local.example 2 | .env 3 | .env.* 4 | !.env.local.example 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | lerna-debug.log* 14 | 15 | node_modules 16 | dist 17 | dist-ssr 18 | *.local 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /hosted-plugin/.nvmrc: -------------------------------------------------------------------------------- 1 | 18.16.0 -------------------------------------------------------------------------------- /hosted-plugin/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "singleAttributePerLine": true 8 | } 9 | -------------------------------------------------------------------------------- /hosted-plugin/README.md: -------------------------------------------------------------------------------- 1 | # Hosted Plugin 2 | 3 | This field plugin allows developers to create and deploy plugins independently of the Storyblok interface. Previously, users had to bundle their plugins, copy the bundled output, and paste it inside the Storyblok UI. To streamline this process and reduce steps, we have introduced a proxy plugin that renders an iframe and communicates seamlessly with the Storyblok interface. 4 | 5 | > **Note:** This field plugin is provided "as is" and should be used at your own risk, as some features might be missing. 6 | 7 | ## App Description 8 | A version of the hosted field plugin, has already been published and is free to use at your own risk. Follow the next section to install **sb-hosted-plugin** to your space. 9 | 10 | ### How to Set Up 11 | 1. Add the field plugin to your space by opening the [installation link](https://app.storyblok.com/#!/install/storyblok-gmbh@hosted-field-plugin) and selecting a specific space. 12 | 2. Add a new field to your **Blok Library** inside your story. 13 | 3. Select **Field Type** as **Plugin**. 14 | 4. In the **Custom Type** input, choose **sb-hosted-plugin**. 15 | 5. Copy and paste the URL of your field plugin under Add options → `HOST_URL`. (e.g. `http://localhost:8080`) 16 | 17 | ## Next Steps 18 | For detailed information on field plugins within Storyblok, check out the following articles: 19 | 20 | - [Introduction to Field Plugins](https://www.storyblok.com/docs/plugins/field-plugins/introduction) 21 | 22 | -------------------------------------------------------------------------------- /hosted-plugin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | Storyblok Field Plugin 17 | 18 | 19 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /hosted-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hosted-plugin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "deploy": "npm run build && npx @storyblok/field-plugin-cli@latest deploy" 11 | }, 12 | "dependencies": { 13 | "@storyblok/field-plugin": "0.0.1-beta.4", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^18.0.28", 19 | "@types/react-dom": "^18.0.11", 20 | "@typescript-eslint/eslint-plugin": "6.1.0", 21 | "@typescript-eslint/parser": "6.1.0", 22 | "@vitejs/plugin-react": "^3.1.0", 23 | "eslint": "latest", 24 | "eslint-plugin-react": "7.30.0", 25 | "typescript": "5.1.6", 26 | "vite": "^4.2.2", 27 | "vite-plugin-css-injected-by-js": "3.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /hosted-plugin/src/App.tsx: -------------------------------------------------------------------------------- 1 | import FieldPlugin from './components/FieldPlugin' 2 | import { FunctionComponent } from 'react' 3 | import { FieldPluginProvider } from '@storyblok/field-plugin/react' 4 | 5 | const App: FunctionComponent = () => { 6 | return ( 7 | 11 | 12 | 13 | ) 14 | } 15 | 16 | const Loading: FunctionComponent = () =>

Loading...

17 | const Error: FunctionComponent<{ error: Error }> = (props) => { 18 | console.error(props.error) 19 | return

An error occured, please see the console for more details.

20 | } 21 | export default App 22 | -------------------------------------------------------------------------------- /hosted-plugin/src/components/FieldPlugin.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, useEffect, useRef } from 'react' 2 | import { useFieldPlugin } from '@storyblok/field-plugin/react' 3 | 4 | const FieldPlugin: FunctionComponent = () => { 5 | const iframeRef = useRef(null) 6 | const { data } = useFieldPlugin() 7 | 8 | // data.options.HOST_URL: http://localhost:8081/hi 9 | // childOrigin: http://localhost:8081 10 | const childOrigin = new URL(data.options.HOST_URL).origin 11 | 12 | const iframeSrc = data.options.HOST_URL.includes('?') 13 | ? data.options.HOST_URL + '&' + window.location.search.slice(1) 14 | : data.options.HOST_URL + window.location.search 15 | 16 | useEffect(() => { 17 | window.addEventListener('message', (e) => { 18 | if (e.origin === childOrigin) { 19 | // from child 20 | window.parent.postMessage(e.data, '*') 21 | } else { 22 | // from parent 23 | iframeRef.current?.contentWindow?.postMessage(e.data, '*') 24 | } 25 | }) 26 | }, []) 27 | 28 | return ( 29 |