├── .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 |
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 |
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 |
33 | )
34 | }
35 |
36 | export default FieldPlugin
37 |
--------------------------------------------------------------------------------
/hosted-plugin/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 |
--------------------------------------------------------------------------------
/hosted-plugin/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 |
--------------------------------------------------------------------------------
/hosted-plugin/src/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | }
4 |
5 | html,
6 | body {
7 | height: 100%;
8 | }
9 |
10 | iframe {
11 | width: 100%;
12 | height: 100%;
13 | border: 0;
14 | }
15 |
16 | #app {
17 | width: 100%;
18 | }
19 |
20 | /*Element Styles*/
21 | body {
22 | margin: 0;
23 | display: flex;
24 | box-sizing: border-box;
25 | }
26 |
--------------------------------------------------------------------------------
/hosted-plugin/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/hosted-plugin/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 |
--------------------------------------------------------------------------------
/hosted-plugin/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 |
--------------------------------------------------------------------------------
/hosted-plugin/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 |
--------------------------------------------------------------------------------
/material-icon-selector/README.md:
--------------------------------------------------------------------------------
1 | # Material Icon Selector Field-Type
2 |
3 | This field-type allows users to choose icons with each of the available icon styles from [Material Icons](https://material.io/icons/).
4 |
5 | Name | Description | Author
6 | ------------ | ------------- | -------------
7 | Material Icon Selector | Allows users to select any material icon | [Riley MacIsaac](https://github.com/rileymacisaac)
8 |
9 |
10 | ## Usage
11 |
12 | Select the desired style, and search/filter for your desired icon.
13 |
14 | Returns an object with the class and icon name:
15 | ```
16 | {
17 | "plugin": "material-icon-selector",
18 | "icon": "", // string
19 | "class": "", // string
20 | ...
21 | }
22 | ```
23 |
24 | ## Output
25 | To ouput this, load the stylesheet in the `` of your site, for example, if you want to include all of the icon styles you would use this link from Google:
26 | `https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined|Material+Icons+Round|Material+Icons+Two+Tone|Material+Icons+Sharp`
27 |
28 | Example of a decorative icon:
29 | ```
30 |
31 | book
32 |
33 | ```
34 |
35 | Example of a non-decorative icon:
36 | ```
37 |
38 | book
39 |
40 | ```
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "field-plugin-examples",
3 | "version": "0.0.0",
4 | "description": "A collection of open-source field plugins",
5 | "private": true,
6 | "scripts": {
7 | "lint": "eslint .",
8 | "docs": "node generate-readme.js"
9 | },
10 | "keywords": [
11 | "storyblok"
12 | ],
13 | "workspaces": [
14 | "code-block",
15 | "tags",
16 | "star-rating",
17 | "picker-starter"
18 | ],
19 | "dependencies": {
20 | "gray-matter": "^4.0.2",
21 | "markdown-magic": "^0.1.25"
22 | },
23 | "devDependencies": {
24 | "eslint": "^7.17.0",
25 | "eslint-plugin-vue": "^7.4.1",
26 | "prettier": "^2.8.8"
27 | }
28 | }
--------------------------------------------------------------------------------
/palette/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
--------------------------------------------------------------------------------
/palette/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | storybook-static/
3 | dist/
4 | certificates/
--------------------------------------------------------------------------------
/palette/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | 'browser': true,
5 | 'es2021': true,
6 | jest: true
7 | },
8 | // 'parser': '@typescript-eslint/parser',
9 | overrides: [{
10 | "files": "*.ts",
11 | "parser": "@typescript-eslint/parser",
12 | extends: ['plugin:@typescript-eslint/recommended'],
13 | "rules": {} // Override rules as well for TS files. Basically any config option can be modified here
14 | }, {
15 | "files": "*.vue",
16 | "parser": "vue-eslint-parser",
17 | extends: [
18 | "plugin:vue/essential",
19 | "eslint:recommended",
20 | '@vue/typescript'],
21 | "rules": {} // Override rules as well for TS files. Basically any config option can be modified here
22 | }, {
23 | files: "*.test.ts",
24 | env: {
25 | jest: true,
26 | }
27 | }],
28 |
29 | extends: [
30 | 'eslint:recommended',
31 | 'plugin:vue/recommended',
32 | 'plugin:prettier-vue/recommended',
33 | 'prettier',
34 | 'plugin:storybook/recommended'
35 | ],
36 | parserOptions: {
37 | ecmaVersion: 12,
38 | sourceType: 'module'
39 | },
40 | plugins: [
41 | '@typescript-eslint',
42 | 'vue',
43 | 'prettier'
44 | ],
45 | rules: {
46 | 'prettier-vue/prettier': ['warn', {
47 | bracketSpacing: true,
48 | printWidth: 80,
49 | semi: false,
50 | singleQuote: true,
51 | trailingComma: 'all',
52 | singleAttributePerLine: true
53 | }],
54 | '@typescript-eslint/no-unused-vars': ['warn', {
55 | args: "none",
56 | argsIgnorePattern: '^_',
57 | varsIgnorePattern: '^_',
58 | caughtErrorsIgnorePattern: '^_'
59 | }]
60 | }
61 | };
--------------------------------------------------------------------------------
/palette/.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 |
23 | # NPM
24 | package-lock.json
--------------------------------------------------------------------------------
/palette/.nvmrc:
--------------------------------------------------------------------------------
1 | 16.19.0
--------------------------------------------------------------------------------
/palette/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets
2 |
3 | This folder contains assets for the apps in the App Directory.
--------------------------------------------------------------------------------
/palette/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/palette/assets/screenshot.png
--------------------------------------------------------------------------------
/palette/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
--------------------------------------------------------------------------------
/palette/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/palette/docs/demo.gif
--------------------------------------------------------------------------------
/palette/jest.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('jest').Config} */
2 | const config = {
3 | preset: 'ts-jest',
4 | testEnvironment: 'jsdom',
5 | moduleNameMapper: {
6 | '^@/(.*)$': '/src/$1',
7 | },
8 | }
9 |
10 | module.exports = config
11 |
--------------------------------------------------------------------------------
/palette/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | storyblok-fieldtype
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/palette/src/components/Checkmark.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
20 |
21 |
--------------------------------------------------------------------------------
/palette/src/components/PalettePlugin.vue:
--------------------------------------------------------------------------------
1 |
42 |
43 |
--------------------------------------------------------------------------------
/palette/src/entries/main.ts:
--------------------------------------------------------------------------------
1 | import { loadPlugin } from '@/lib/loadPlugin'
2 | import PalettePlugin from '@/components/PalettePlugin.vue'
3 |
4 | loadPlugin('storyblok-palette', {}, PalettePlugin)
5 |
--------------------------------------------------------------------------------
/palette/src/lib/README.md:
--------------------------------------------------------------------------------
1 | This folder contains a module that will be extracted to a general-purpose library for Storyblok field types
--------------------------------------------------------------------------------
/palette/src/lib/components/FieldModal/FieldModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/palette/src/lib/components/FieldModal/index.ts:
--------------------------------------------------------------------------------
1 | // TODO use to simulate the Storyblok Visual Editor and enable running locally without Storyblok's field type editor
2 | export { default as FieldModal } from './FieldModal.vue'
3 |
--------------------------------------------------------------------------------
/palette/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loadPlugin'
2 | export * from './types'
3 | export * from './makeStoryblokPluginComponent'
4 |
--------------------------------------------------------------------------------
/palette/src/lib/pluginPropsDef.ts:
--------------------------------------------------------------------------------
1 | import { RecordPropsDefinition } from 'vue/types/options'
2 | import { WrapperPluginProps } from '@/lib/types'
3 |
4 | export const pluginPropsDef: RecordPropsDefinition = {
5 | isModalOpen: {
6 | type: Boolean,
7 | required: true,
8 | },
9 | setModalOpen: {
10 | type: Function,
11 | required: true,
12 | },
13 | /**
14 | * The options is a Record
15 | */
16 | options: {
17 | type: Object,
18 | required: true,
19 | },
20 | setValue: {
21 | type: Function,
22 | required: true,
23 | },
24 | value: {
25 | type: Object,
26 | required: true,
27 | },
28 | name: {
29 | type: String,
30 | required: true,
31 | },
32 | spaceId: {
33 | type: Number,
34 | default: null,
35 | },
36 | token: {
37 | type: String,
38 | default: null,
39 | },
40 | }
41 |
--------------------------------------------------------------------------------
/palette/src/lib/types/PluginComponentProps.ts:
--------------------------------------------------------------------------------
1 | export type PluginComponentProps = Record
2 |
--------------------------------------------------------------------------------
/palette/src/lib/types/RootComponentData.ts:
--------------------------------------------------------------------------------
1 | export type RootComponentData = {
2 | isModalOpen: boolean
3 | }
4 |
--------------------------------------------------------------------------------
/palette/src/lib/types/RootPluginComponentComputed.ts:
--------------------------------------------------------------------------------
1 | export type RootPluginComponentComputed = Record
2 |
--------------------------------------------------------------------------------
/palette/src/lib/types/RootPluginComponentMethods.ts:
--------------------------------------------------------------------------------
1 | export type RootPluginComponentMethods = {
2 | setModalOpen(value: boolean): void
3 | getPluginName(): string
4 | initWith(): {
5 | plugin: string
6 | } & Record
7 | setValue(value: Record): void
8 | }
9 |
--------------------------------------------------------------------------------
/palette/src/lib/types/RootPluginProps.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The props to the root component that uses the mixin from the global Storyblok variable
3 | */
4 | export type RootPluginProps = {
5 | token: string | undefined | null
6 | spaceId: number | undefined | null
7 | options: Record
8 | name: string
9 | }
10 |
--------------------------------------------------------------------------------
/palette/src/lib/types/StoryblokFieldType.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'vue'
2 | import { WrapperPluginProps } from '@/lib/types/WrapperPluginProps'
3 |
4 | export type StoryblokFieldType = {
5 | name: string
6 | initialValue: Record
7 | Component: Component
8 | }
9 |
--------------------------------------------------------------------------------
/palette/src/lib/types/WrapperPluginProps.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The props that are passed to the top-level component
3 | */
4 | export type WrapperPluginProps = {
5 | name: string
6 | isModalOpen: boolean
7 | setModalOpen: (value: boolean) => void
8 | options: Record
9 | setValue: (value: Record) => void
10 | value: Record // TODO generic type for output
11 | spaceId: number | null // or is it number?
12 | token: string | null
13 | }
14 |
--------------------------------------------------------------------------------
/palette/src/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StoryblokFieldType'
2 | export * from './WrapperPluginProps'
3 | export * from './RootComponentData'
4 | export * from './RootPluginComponentComputed'
5 | export * from './PluginComponentProps'
6 | export * from './RootPluginComponentMethods'
7 |
--------------------------------------------------------------------------------
/palette/src/utils/SRGB.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * sRGB is a standard vector
3 | * https://www.w3.org/Graphics/Color/sRGB.html
4 | */
5 | export type SRGB = {
6 | r: number
7 | g: number
8 | b: number
9 | }
10 |
--------------------------------------------------------------------------------
/palette/src/utils/contrastRatio/contrastRatio.test.ts:
--------------------------------------------------------------------------------
1 | import { contrastRatio } from '@/utils'
2 |
3 | describe('contrastRatio', () => {
4 | test('that the contrast between a luminance and itself is 1', () => {
5 | expect(contrastRatio(0, 0)).toBeCloseTo(1, 2)
6 | expect(contrastRatio(0.4, 0.4)).toBeCloseTo(1, 2)
7 | expect(contrastRatio(1, 1)).toBeCloseTo(1, 2)
8 | })
9 | test('that the contrast ratio between black and white is 21:1 -- the highest possible contrast ratio', () => {
10 | expect(contrastRatio(0, 1)).toBeCloseTo(21, 2)
11 | })
12 | test('that the contrast ratio between white and black is 21:1 -- the highest possible contrast ratio', () => {
13 | expect(contrastRatio(1, 0)).toBeCloseTo(21, 2)
14 | })
15 | test('that the contrast ratio is symmetric with respect to the order of the arguments -- the relative luminance of the lighter color is always the dividend', () => {
16 | expect(contrastRatio(1, 0)).toBe(contrastRatio(0, 1))
17 | expect(contrastRatio(0.1, 0.7)).toBe(contrastRatio(0.7, 0.1))
18 | })
19 | test('that the contrast between 0.6 and 0.3 is 1.8571 -- a hand-calculates value', () => {
20 | expect(contrastRatio(0.6, 0.3)).toBeCloseTo(1.8571, 2)
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/palette/src/utils/contrastRatio/contrastRatio.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Calculates the contrast ratio between two relative luminances based on the Web Content Accessibility Guidelines (WCAG) 2.1 (https://www.w3.org/TR/WCAG/#dfn-contrast-ratio)
3 | * The result is a value between 0 and 1.
4 | * @param relativeLuminance1
5 | * @param relativeLuminance2
6 | */
7 | export const contrastRatio = (
8 | relativeLuminance1: number,
9 | relativeLuminance2: number,
10 | ): number => {
11 | return relativeLuminance1 > relativeLuminance2
12 | ? (relativeLuminance1 + 0.05) / (relativeLuminance2 + 0.05)
13 | : (relativeLuminance2 + 0.05) / (relativeLuminance1 + 0.05)
14 | }
15 |
--------------------------------------------------------------------------------
/palette/src/utils/hexToSRGB/hexToSRGB.test.ts:
--------------------------------------------------------------------------------
1 | import { hexToSRGB, SRGB } from '@/utils'
2 |
3 | const black: SRGB = {
4 | r: 0,
5 | g: 0,
6 | b: 0,
7 | }
8 | const white: SRGB = {
9 | r: 1,
10 | g: 1,
11 | b: 1,
12 | }
13 | const red: SRGB = {
14 | r: 1,
15 | g: 0,
16 | b: 0,
17 | }
18 | const green: SRGB = {
19 | r: 0,
20 | g: 1,
21 | b: 0,
22 | }
23 | const blue: SRGB = {
24 | r: 0,
25 | g: 0,
26 | b: 1,
27 | }
28 |
29 | describe('hexToSRGB', () => {
30 | it('parses red color', () => {
31 | expect(hexToSRGB('#FF0000')).toEqual(red)
32 | })
33 | it('parses green color', () => {
34 | expect(hexToSRGB('#00FF00')).toEqual(green)
35 | })
36 | it('parses blue color', () => {
37 | expect(hexToSRGB('#0000FF')).toEqual(blue)
38 | })
39 | it('parses white color', () => {
40 | expect(hexToSRGB('#FFFFFF')).toEqual(white)
41 | })
42 | it('parses black color', () => {
43 | expect(hexToSRGB('#000000')).toEqual(black)
44 | })
45 | it('gived undefined if parsing failed', () => {
46 | expect(hexToSRGB('dummy')).toBeUndefined()
47 | expect(hexToSRGB('FFFFFF')).toBeUndefined()
48 | expect(hexToSRGB('#FFF')).toBeUndefined()
49 | expect(hexToSRGB('0x80')).toBeUndefined()
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/palette/src/utils/hexToSRGB/hexToSRGB.ts:
--------------------------------------------------------------------------------
1 | import { SRGB } from '@/utils/SRGB'
2 | import { numberFromString } from '@/utils/numberFromString'
3 |
4 | /**
5 | * Transforms a hexadecimal color into sRGB color space. For example "#FF0080" -> { r: 1, g: 0, b: 0.5}.
6 | * @param hex
7 | */
8 | export const hexToSRGB = (hex: string): SRGB | undefined => {
9 | const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
10 | hex = hex.replace(shorthandRegex, function (m, r, g, b) {
11 | return r + r + g + g + b + b
12 | })
13 |
14 | const result = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
15 | const r8bit = numberFromString(`0x${result?.[1]}`)
16 | const green8bit = numberFromString(`0x${result?.[2]}`)
17 | const b8bit = numberFromString(`0x${result?.[3]}`)
18 | if (
19 | typeof r8bit === 'undefined' ||
20 | typeof green8bit === 'undefined' ||
21 | typeof b8bit === 'undefined'
22 | ) {
23 | return undefined
24 | }
25 | return {
26 | r: r8bit / 255,
27 | g: green8bit / 255,
28 | b: b8bit / 255,
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/palette/src/utils/hexToSRGB/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hexToSRGB'
2 |
--------------------------------------------------------------------------------
/palette/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SRGB'
2 | export * from './contrastRatio/contrastRatio'
3 | export * from './hexToSRGB/hexToSRGB'
4 | export * from './relativeLuminance/relativeLuminance'
5 | export * from './numberFromString'
6 |
--------------------------------------------------------------------------------
/palette/src/utils/numberFromString/index.ts:
--------------------------------------------------------------------------------
1 | export * from './numberFromString'
2 |
--------------------------------------------------------------------------------
/palette/src/utils/numberFromString/numberFromString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Use instead of parseInt
3 | */
4 | export const numberFromString = (str: string): number | undefined => {
5 | // eslint-disable-next-line no-restricted-syntax
6 | const parsed = Number(str)
7 | return !isNaN(parsed) && isFinite(parsed) ? parsed : undefined
8 | }
9 |
--------------------------------------------------------------------------------
/palette/src/utils/relativeLuminance/relativeLuminance.test.ts:
--------------------------------------------------------------------------------
1 | import { relativeLuminance, SRGB } from '@/utils'
2 |
3 | const black: SRGB = {
4 | r: 0,
5 | g: 0,
6 | b: 0,
7 | }
8 | const white: SRGB = {
9 | r: 1,
10 | g: 1,
11 | b: 1,
12 | }
13 | const red: SRGB = {
14 | r: 1,
15 | g: 0,
16 | b: 0,
17 | }
18 |
19 | describe('relativeLuminance', () => {
20 | test('that the relative luminance of black is 0', () => {
21 | expect(relativeLuminance(black)).toBeCloseTo(0, 1)
22 | })
23 | test('that the relative luminance of white is 1', () => {
24 | expect(relativeLuminance(white)).toBeCloseTo(1, 1)
25 | })
26 | test('that the relative luminance of red is 0.2126', () => {
27 | // Hand-calculated
28 | expect(relativeLuminance(red)).toBeCloseTo(0.2126, 1)
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/palette/src/utils/relativeLuminance/relativeLuminance.ts:
--------------------------------------------------------------------------------
1 | import { SRGB } from '@/utils/SRGB'
2 |
3 | export const f = (sRGB: number): number =>
4 | sRGB <= 0.03928 ? sRGB / 12.92 : Math.pow((sRGB + 0.055) / 1.055, 2.4)
5 |
6 | /**
7 | * Calculates the luminance from an RGB-value based on the Web Content Accessibility Guidelines (WCAG) 2.1
8 | * (https://www.w3.org/TR/WCAG/#dfn-relative-luminance)
9 | * @param rgb
10 | */
11 | export const relativeLuminance = (rgb: SRGB): number => {
12 | const { r, g, b } = rgb
13 | return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b)
14 | }
15 |
--------------------------------------------------------------------------------
/palette/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "moduleResolution": "node",
8 | "resolveJsonModule": true,
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "useDefineForClassFields": true,
15 | "noUncheckedIndexedAccess": true,
16 | "sourceMap": true,
17 | "baseUrl": ".",
18 | "types": [
19 | "webpack-env",
20 | "jest"
21 | ],
22 | "paths": {
23 | "@/*": [
24 | "src/*"
25 | ]
26 | },
27 | "lib": [
28 | "esnext",
29 | "dom",
30 | "dom.iterable",
31 | "scripthost"
32 | ],
33 | },
34 | "include": [
35 | "src/**/*.ts",
36 | "src/**/*.tsx",
37 | "src/**/*.vue",
38 | "tests/**/*.ts",
39 | "tests/**/*.tsx"
40 | ],
41 | "exclude": [
42 | "node_modules"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/palette/vue.config.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path');
3 |
4 | const entriesDir = './src/entries'
5 | const entry = fs
6 | .readdirSync(entriesDir)
7 | .reduce(
8 | (entry, filename) => {
9 | const name = path.basename(filename, '.ts')
10 | entry[name] = `${entriesDir}/${filename}`
11 | return entry
12 | },
13 | {}
14 | )
15 |
16 | const pages = process.env.NODE_ENV === 'production' ? (
17 | entry
18 | ) : undefined
19 |
20 | const publicUrl = 'http://localhost:8080'
21 |
22 | module.exports = {
23 | pages,
24 | configureWebpack: {
25 | entry: entry,
26 | output: {
27 | filename: '[name].js'
28 | },
29 | optimization: {
30 | splitChunks: false
31 | },
32 | plugins: [
33 | new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
34 | analyzerMode: 'static',
35 | openAnalyzer: false,
36 | reportFilename: 'reports/bundle-size.html'
37 | })
38 | ],
39 | },
40 | filenameHashing: false,
41 | runtimeCompiler: true,
42 | productionSourceMap: false,
43 | css: {
44 | extract: false
45 | },
46 | devServer: {
47 | // Using secure tunnel
48 | public: publicUrl,
49 | disableHostCheck: true,
50 | },
51 | }
--------------------------------------------------------------------------------
/picker-starter/.env.local.example:
--------------------------------------------------------------------------------
1 | STORYBLOK_PERSONAL_ACCESS_TOKEN=
2 | VITE_PLUGIN_NAME=
3 | VITE_DEFAULT_PER_PAGE=
4 | VITE_THROTTLE_MS=
5 |
--------------------------------------------------------------------------------
/picker-starter/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
3 |
--------------------------------------------------------------------------------
/picker-starter/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | jest: true,
7 | },
8 | overrides: [
9 | {
10 | files: '*.ts',
11 | parser: '@typescript-eslint/parser',
12 | extends: ['plugin:@typescript-eslint/recommended'],
13 | rules: {}, // Override rules as well for TS files. Basically any config option can be modified here
14 | },
15 | {
16 | files: '*.vue',
17 | parser: 'vue-eslint-parser',
18 | parserOptions: {
19 | parser: '@typescript-eslint/parser',
20 | sourceType: 'module',
21 | },
22 | extends: ['plugin:vue/vue3-recommended'],
23 | rules: {
24 | 'vue/multi-word-component-names': 'off',
25 | 'vue/html-self-closing': [
26 | 'warn',
27 | {
28 | html: {
29 | void: 'always',
30 | },
31 | },
32 | ],
33 | }, // Override rules as well for TS files. Basically any config option can be modified here
34 | },
35 | ],
36 |
37 | extends: ['eslint:recommended'],
38 | parserOptions: {
39 | ecmaVersion: 12,
40 | sourceType: 'module',
41 | parser: 'babel-eslint',
42 | },
43 | plugins: ['@typescript-eslint'],
44 | rules: {
45 | curly: 'error',
46 | '@typescript-eslint/no-unused-vars': [
47 | 'warn',
48 | {
49 | args: 'none',
50 | argsIgnorePattern: '^_',
51 | varsIgnorePattern: '^_',
52 | caughtErrorsIgnorePattern: '^_',
53 | },
54 | ],
55 | },
56 | }
57 |
--------------------------------------------------------------------------------
/picker-starter/.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 |
--------------------------------------------------------------------------------
/picker-starter/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.14.0
--------------------------------------------------------------------------------
/picker-starter/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "semi": false,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "singleAttributePerLine": true
8 | }
9 |
--------------------------------------------------------------------------------
/picker-starter/docs/get-options-function.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/get-options-function.png
--------------------------------------------------------------------------------
/picker-starter/docs/loaded-sandbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/loaded-sandbox.png
--------------------------------------------------------------------------------
/picker-starter/docs/open-sandbox-url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/open-sandbox-url.png
--------------------------------------------------------------------------------
/picker-starter/docs/passing-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/passing-options.png
--------------------------------------------------------------------------------
/picker-starter/docs/picker.config-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/picker.config-1.png
--------------------------------------------------------------------------------
/picker-starter/docs/picker.config-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/picker.config-2.png
--------------------------------------------------------------------------------
/picker-starter/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/picker-starter/docs/screenshot.png
--------------------------------------------------------------------------------
/picker-starter/field-plugin.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "options": []
3 | }
4 |
--------------------------------------------------------------------------------
/picker-starter/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@storyblok/design-system'
2 | declare module '@storyblok/design-system/src/directives'
3 |
--------------------------------------------------------------------------------
/picker-starter/index.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
16 | Storyblok Field Plugin
17 |
18 |
19 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/picker-starter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "picker-starter",
3 | "private": false,
4 | "version": "0.0.0",
5 | "license": "MIT",
6 | "type": "module",
7 | "scripts": {
8 | "dev": "vite",
9 | "build": "vite build",
10 | "test": "vitest",
11 | "prettier": "prettier --check .",
12 | "lint": "eslint .",
13 | "check:types": "vue-tsc --noEmit",
14 | "preview": "vite preview",
15 | "deploy": "npm run build && npx @storyblok/field-plugin-cli@latest deploy"
16 | },
17 | "dependencies": {
18 | "@storyblok/design-system": "3.19.3",
19 | "@storyblok/field-plugin": "^1.4.0",
20 | "debounce": "^1.2.1",
21 | "vue": "^3.2.47",
22 | "vue-draggable-next": "^2.2.1"
23 | },
24 | "devDependencies": {
25 | "@testing-library/jest-dom": "^6.1.4",
26 | "@types/debounce": "^1.2.4",
27 | "@typescript-eslint/eslint-plugin": "^6.12.0",
28 | "@typescript-eslint/parser": "^6.12.0",
29 | "@vitejs/plugin-vue": "^4.1.0",
30 | "eslint": "^8.54.0",
31 | "eslint-plugin-vue": "^9.18.1",
32 | "prettier": "^3.1.0",
33 | "sass": "^1.69.5",
34 | "ts-node": "^10.9.1",
35 | "typescript": "5.1.6",
36 | "vite": "^4.2.2",
37 | "vite-plugin-css-injected-by-js": "^3.3.0",
38 | "vitest": "^0.34.6",
39 | "vue-tsc": "1.8.6"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/picker-starter/src/App.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/picker-starter/src/components/AddAssetButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default as AddAssetButton } from './AddAssetButton.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Avatar/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Avatar } from './Avatar.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Card/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
21 |
22 |
37 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Card/CardContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Card/CardFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Card/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Card } from './Card.vue'
2 | export { default as CardContent } from './CardContent.vue'
3 | export { default as CardFooter } from './CardFooter.vue'
4 |
--------------------------------------------------------------------------------
/picker-starter/src/components/CartList/CartListItemImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
10 |
11 |
12 |
13 |
24 |
25 |
34 |
--------------------------------------------------------------------------------
/picker-starter/src/components/CartList/CartListItemImageContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
26 |
--------------------------------------------------------------------------------
/picker-starter/src/components/CartList/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CartList } from './CartList.vue'
2 | export { default as CartListItem } from './CartListItem.vue'
3 | export { default as CartListItemImage } from './CartListItemImage.vue'
4 | export { default as CartListItemImageContainer } from './CartListItemImageContainer.vue'
5 |
--------------------------------------------------------------------------------
/picker-starter/src/components/CartList/move/index.ts:
--------------------------------------------------------------------------------
1 | export * from './move'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/CartList/move/move.ts:
--------------------------------------------------------------------------------
1 | export const move = (
2 | items: T[],
3 | fromIndex: number,
4 | toIndex: number,
5 | ): T[] => {
6 | if (fromIndex === toIndex) {
7 | return items
8 | }
9 |
10 | if (
11 | fromIndex < 0 ||
12 | toIndex < 0 ||
13 | fromIndex > items.length - 1 ||
14 | toIndex > items.length - 1
15 | ) {
16 | return items
17 | }
18 |
19 | const item = items[fromIndex] as T
20 |
21 | const arrayWithout = [
22 | ...items.slice(0, fromIndex),
23 | ...items.slice(fromIndex + 1),
24 | ]
25 |
26 | return [
27 | ...arrayWithout.slice(0, toIndex),
28 | item,
29 | ...arrayWithout.slice(toIndex),
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/picker-starter/src/components/EmptyScreen/EmptyScreen.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
20 |
21 |
22 |
23 |
24 |
45 |
46 |
65 |
--------------------------------------------------------------------------------
/picker-starter/src/components/EmptyScreen/index.ts:
--------------------------------------------------------------------------------
1 | export { default as EmptyScreen } from './EmptyScreen.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ErrorNotification/ErrorNotification.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
16 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ErrorNotification/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ErrorNotification } from './ErrorNotification.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/FieldPlugin.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
49 |
50 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Filter/SearchField.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
37 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Filter/SelectFilter.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
35 |
36 |
41 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Filter/index.ts:
--------------------------------------------------------------------------------
1 | export { default as SelectFilter } from './SelectFilter.vue'
2 | export { default as SearchField } from './SearchField.vue'
3 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Grid/Grid.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
21 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Grid/GridItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Grid/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Grid } from './Grid.vue'
2 | export { default as GridItem } from './GridItem.vue'
3 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Icons/StoryblokIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
45 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as StoryblokIcon } from './StoryblokIcon.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemCard/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ItemCard } from './ItemCard.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemGrid/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ItemGrid } from './ItemGrid.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemImage/ItemImage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![]()
9 |
14 |
15 |
16 |
17 |
33 |
34 |
45 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemImage/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ItemImage } from './ItemImage.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemList/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ItemList } from './ItemList.vue'
2 | export { default as ItemListItem } from './ItemListItem.vue'
3 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ItemPicker } from './ItemPicker.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/mixin/disableLoadMore.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../state/types/State'
2 | import { isCursorPaginatedResult } from '@/core'
3 |
4 | export const disableLoadMore = (state: State): boolean =>
5 | isCursorPaginatedResult(state.pageInfo) && !state.pageInfo.hasNextPage
6 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/mixin/isLoadingItems.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../state/types/State'
2 |
3 | export const isLoadingItems = (state: State): boolean =>
4 | state.loadingState !== 'notLoading'
5 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/mixin/isPageEmpty.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../state/types/State'
2 | import { isLoadingItems } from './isLoadingItems'
3 |
4 | export const isPageEmpty = (state: State): boolean =>
5 | !isLoadingItems(state) && state.items.length === 0
6 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/mixin/showCursorPagination.ts:
--------------------------------------------------------------------------------
1 | import { State } from '../state/types/State'
2 | import { isCursorPaginatedResult } from '@/core'
3 |
4 | export const showCursorPagination = (state: State): boolean =>
5 | isCursorPaginatedResult(state.pageInfo) && state.pageInfo.hasNextPage
6 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/mixin/showPagePagination.ts:
--------------------------------------------------------------------------------
1 | import { defaultPerPage } from '@/settings'
2 | import { State } from '../state/types/State'
3 | import { isPagePaginatedResult } from '@/core'
4 |
5 | export const showPagePagination = (state: State): boolean =>
6 | isPagePaginatedResult(state.pageInfo) &&
7 | Number.isInteger(state.pageInfo.totalCount) &&
8 | state.pageInfo.totalCount > defaultPerPage
9 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/state/dispatch.test.ts:
--------------------------------------------------------------------------------
1 | import {} from './dispatch'
2 | describe('ItemPicker Actions', () => {
3 | describe('setFilterSelection', () => {
4 | it.todo('should reset the page')
5 | })
6 | describe('setSearchTerm', () => {
7 | it.todo('should reset the page')
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/state/types/Action.ts:
--------------------------------------------------------------------------------
1 | import { BasketItem, FilterItem, QueryResponse } from '@/core'
2 |
3 | export type RequestFiltersAction = {
4 | type: 'requestFilters'
5 | }
6 |
7 | export type ReceiveItemsAction = {
8 | type: 'receiveItems'
9 | response: QueryResponse
10 | }
11 |
12 | export type ReceiveFiltersAction = {
13 | type: 'receiveFilters'
14 | filterList: FilterItem[]
15 | }
16 |
17 | export type ReceiveErrorAction = {
18 | type: 'receiveError'
19 | error: Error
20 | }
21 |
22 | export type LoadMoreAction = {
23 | type: 'loadMore'
24 | }
25 |
26 | export type LoadPageAction = {
27 | type: 'loadPage'
28 | page: number
29 | }
30 |
31 | export type SetUserSelectionAction = {
32 | type: 'setFilterSelection'
33 | name: string
34 | value: string | string[]
35 | }
36 |
37 | export type SetSearchTermAction = {
38 | type: 'setSearchTerm'
39 | searchTerm: string
40 | }
41 |
42 | export type Action =
43 | | RequestFiltersAction
44 | | ReceiveFiltersAction
45 | | ReceiveItemsAction
46 | | ReceiveErrorAction
47 | | SetUserSelectionAction
48 | | LoadMoreAction
49 | | LoadPageAction
50 | | SetSearchTermAction
51 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ItemPicker/utils/state/types/State.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BasketItem,
3 | CursorPaginatedPageInfo,
4 | ItemQueryParams,
5 | PagePaginatedPageInfo,
6 | FilterItem,
7 | } from '@/core'
8 |
9 | export type State = {
10 | loadingState:
11 | | 'notLoading'
12 | | 'loadingFilters'
13 | | 'loadingNewPage' // Results will replace current list of items
14 | | 'loadingMore' // Results will append current list of items
15 | query: ItemQueryParams
16 | items: BasketItem[]
17 | filterList: FilterItem[]
18 | pageInfo: PagePaginatedPageInfo | CursorPaginatedPageInfo | undefined
19 | }
20 |
--------------------------------------------------------------------------------
/picker-starter/src/components/List/List.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/picker-starter/src/components/List/ListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
26 |
--------------------------------------------------------------------------------
/picker-starter/src/components/List/index.ts:
--------------------------------------------------------------------------------
1 | export { default as List } from './List.vue'
2 | export { default as ListItem } from './ListItem.vue'
3 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ModalPage/ModalContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
25 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ModalPage/ModalHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
31 |
32 |
67 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ModalPage/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ModalPage } from './ModalPage.vue'
2 | export { default as ModalContainer } from './ModalContainer.vue'
3 | export { default as ModalHeader } from './ModalHeader.vue'
4 |
--------------------------------------------------------------------------------
/picker-starter/src/components/NonModalPage/add-items-label/addItemsLabel.test.ts:
--------------------------------------------------------------------------------
1 | import { addItemsLabel } from './addItemsLabel'
2 |
3 | describe('addedItemsLabel', () => {
4 | it('should use the correct grammatical number', () => {
5 | expect(addItemsLabel(undefined)).toMatch(/items$/)
6 | expect(addItemsLabel(0)).toMatch(/items$/)
7 | expect(addItemsLabel(1)).toMatch(/item$/)
8 | expect(addItemsLabel(2)).toMatch(/items$/)
9 | expect(addItemsLabel(3)).toMatch(/items$/)
10 | })
11 | it.todo('test NaN')
12 | })
13 |
--------------------------------------------------------------------------------
/picker-starter/src/components/NonModalPage/add-items-label/addItemsLabel.ts:
--------------------------------------------------------------------------------
1 | export const addItemsLabel = (maxCount: number | undefined): string => {
2 | return maxCount === 1 ? 'Add an item' : 'Add items'
3 | }
4 |
--------------------------------------------------------------------------------
/picker-starter/src/components/NonModalPage/add-items-label/index.ts:
--------------------------------------------------------------------------------
1 | export * from './addItemsLabel'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/NonModalPage/index.ts:
--------------------------------------------------------------------------------
1 | export { default as NonModalPage } from './NonModalPage.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/NotificationProvider/index.ts:
--------------------------------------------------------------------------------
1 | export { default as NotificationProvider } from './NotificationProvider.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Picker/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Picker } from './Picker.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Picker/pluginPropsDef.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsOptions, PropType } from 'vue'
2 |
3 | export type WrapperPluginProps = {
4 | name: string
5 | isModalOpen: boolean
6 | setModalOpen: (value: boolean) => void
7 | unvalidatedOptions: Record
8 | setValue: (value: Record) => void
9 | value: Record // TODO generic type for output
10 | spaceId: number | null // or is it number?
11 | token: string | null
12 | }
13 |
14 | export const pluginPropsDef: ComponentPropsOptions = {
15 | isModalOpen: {
16 | type: Boolean,
17 | required: true,
18 | },
19 | setModalOpen: {
20 | type: Function as PropType<(isModal: boolean) => void>,
21 | required: true,
22 | },
23 | /**
24 | * The options is a Record
25 | */
26 | unvalidatedOptions: {
27 | type: Object,
28 | required: true,
29 | },
30 | setValue: {
31 | type: Function as PropType<(value: unknown) => void>,
32 | required: true,
33 | },
34 | value: {
35 | type: Object,
36 | required: true,
37 | },
38 | name: {
39 | type: String,
40 | required: true,
41 | },
42 | spaceId: {
43 | type: Number,
44 | default: null,
45 | },
46 | token: {
47 | type: String,
48 | default: null,
49 | },
50 | }
51 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Skeleton/Skeleton.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
22 |
23 |
53 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Skeleton/SkeletonCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
25 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Skeleton/SkeletonListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
13 |
14 |
26 |
--------------------------------------------------------------------------------
/picker-starter/src/components/Skeleton/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Skeleton } from './Skeleton.vue'
2 | export { default as SkeletonCard } from './SkeletonCard.vue'
3 | export { default as SkeletonListItem } from './SkeletonListItem.vue'
4 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ValidationError/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ValidationError } from './ValidationError.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewCartButton/ViewCartButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 | {{ label }}
10 |
11 |
12 |
13 |
42 |
43 |
50 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewCartButton/added-items-count-label/addedItemsCountLabel.test.ts:
--------------------------------------------------------------------------------
1 | import { addedItemsCountLabel } from './addedItemsCountLabel'
2 |
3 | describe('addedItemsLabel', () => {
4 | it('should use the correct grammatical number', () => {
5 | expect(addedItemsCountLabel(1, undefined)).toMatch(/item$/)
6 | expect(addedItemsCountLabel(2, undefined)).toMatch(/items$/)
7 | expect(addedItemsCountLabel(0, 0)).toMatch(/items$/)
8 | expect(addedItemsCountLabel(1, 1)).toMatch(/item$/)
9 | expect(addedItemsCountLabel(1, 2)).toMatch(/items$/)
10 | })
11 | it('should include maxCount when defined', () => {
12 | expect(addedItemsCountLabel(0, 1)).toMatch(/1 item$/)
13 | expect(addedItemsCountLabel(0, 2)).toMatch(/2 items$/)
14 | expect(addedItemsCountLabel(0, 3)).toMatch(/3 items$/)
15 | })
16 | it('should exclude maxCount when undefined', () => {
17 | expect(addedItemsCountLabel(1, undefined)).toMatch(/1 item$/)
18 | expect(addedItemsCountLabel(2, undefined)).toMatch(/2 items$/)
19 | expect(addedItemsCountLabel(3, undefined)).toMatch(/3 items$/)
20 | })
21 | })
22 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewCartButton/added-items-count-label/addedItemsCountLabel.ts:
--------------------------------------------------------------------------------
1 | export const addedItemsCountLabel = (
2 | count: number,
3 | maxCount: number | undefined,
4 | ): string => {
5 | const singular =
6 | typeof maxCount === 'undefined' ? count === 1 : maxCount === 1
7 | const limitLabel =
8 | typeof maxCount === 'undefined' ? '' : ` of ${maxCount.toString(10)}`
9 | const itemsLabel = singular ? 'item' : 'items'
10 | return `Added ${count}${limitLabel} ${itemsLabel}`
11 | }
12 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewCartButton/added-items-count-label/index.ts:
--------------------------------------------------------------------------------
1 | export * from './addedItemsCountLabel'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewCartButton/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ViewCartButton } from './ViewCartButton.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewModeSwitch/ViewModeSwitch.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
14 |
21 |
22 |
23 |
24 |
46 |
47 |
68 |
--------------------------------------------------------------------------------
/picker-starter/src/components/ViewModeSwitch/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ViewModeSwitch } from './ViewModeSwitch.vue'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AddAssetButton'
2 | export * from './Avatar'
3 | export * from './Card'
4 | export * from './CartList'
5 | export * from './EmptyScreen'
6 | export * from './ErrorNotification'
7 | export * from './Grid'
8 | export * from './Icons'
9 | export * from './ItemCard'
10 | export * from './ItemGrid'
11 | export * from './ItemImage'
12 | export * from './ItemList'
13 | export * from './ItemPicker'
14 | export * from './List'
15 | export * from './ModalPage'
16 | export * from './NonModalPage'
17 | export * from './NotificationProvider'
18 | export * from './Skeleton'
19 | export * from './Filter'
20 | export * from './ValidationError'
21 | export * from './ViewCartButton'
22 | export * from './ViewModeSwitch'
23 | export * from './Picker'
24 |
--------------------------------------------------------------------------------
/picker-starter/src/components/styles.scss:
--------------------------------------------------------------------------------
1 | $card-padding-y: 15px;
2 | $move-fade-transition:
3 | opacity 243ms cubic-bezier(0.4, 0, 0.2, 1) 0ms,
4 | transform 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
5 |
6 | $breakpoint-mobile: 480;
7 | $breakpoint-tablet: 768;
8 | $breakpoint-desktop: 880;
9 |
10 | $avatar-height: 32px;
11 | $card-image-height: 140px;
12 | $card-image-height-small: 80px;
13 |
14 | $card-height: calc(#{$card-padding-y * 2 + $card-image-height-small});
15 |
16 | @mixin transition($props...) {
17 | transition-property: $props;
18 | will-change: $props;
19 | transition-duration: 100ms;
20 | transition-timing-function: ease-in-out;
21 | }
22 |
23 | @mixin typography-title {
24 | color: var(--sb-color-secondary-950);
25 | font-family: Roboto, sans-serif;
26 | font-size: 14px;
27 | font-weight: 500;
28 | display: block;
29 | overflow: hidden;
30 | line-height: 15px;
31 | text-overflow: ellipsis;
32 | }
33 |
34 | @mixin typography-description {
35 | padding-left: 0;
36 | color: var(--sb-color-base-700);
37 | font-size: 12px;
38 | background-color: inherit;
39 | font-family: Roboto, sans-serif;
40 | text-overflow: ellipsis;
41 | }
42 |
43 | @mixin max-lines($lines) {
44 | overflow: hidden;
45 | display: -webkit-box;
46 | line-height: 1em;
47 | max-height: #{$lines}em;
48 | -webkit-line-clamp: $lines;
49 | -webkit-box-orient: vertical;
50 | }
51 |
--------------------------------------------------------------------------------
/picker-starter/src/composables/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useErrorNotification'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/composables/useErrorNotification.ts:
--------------------------------------------------------------------------------
1 | import { ref } from 'vue'
2 | import { NotificationError } from '@/core'
3 |
4 | const errorNotification = ref()
5 |
6 | export const useErrorNotification = () => {
7 | const setErrorNotification = (err?: NotificationError) => {
8 | errorNotification.value = err
9 | }
10 |
11 | return {
12 | errorNotification,
13 | setErrorNotification,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/picker-starter/src/core/basket/BasketItem.ts:
--------------------------------------------------------------------------------
1 | export type BasketItem = {
2 | type: Type
3 | id: string
4 | name: string
5 | image: string | undefined
6 | description: string | undefined
7 | }
8 |
--------------------------------------------------------------------------------
/picker-starter/src/core/basket/basket.ts:
--------------------------------------------------------------------------------
1 | import { BasketItem } from './BasketItem'
2 |
3 | export type Basket = Readonly<{
4 | items: ReadonlyArray
5 | contains: (item: BasketItem) => boolean
6 | addItem: (item: BasketItem) => void
7 | remove: (item: BasketItem) => void
8 | clear: () => void
9 | set: (items: BasketItem[]) => void
10 | size: () => number
11 | isEmpty: () => boolean
12 | }>
13 |
14 | type SetItems = (getItems: (current: BasketItem[]) => BasketItem[]) => void
15 |
16 | /**
17 | * Provides a set of operations to mutate the state of list of items.
18 | * The list that is provided as argument is never mutated, and the function does not create a state.
19 | * The state needs to be stored outside this function's scope, and the mutation is done with the setState argument.
20 | * @param items
21 | * @param setItems
22 | */
23 | export const createBasket = (
24 | items: BasketItem[],
25 | setItems: SetItems,
26 | ): Basket => {
27 | return {
28 | items,
29 | isEmpty() {
30 | return items.length === 0
31 | },
32 | contains(item: BasketItem) {
33 | return items.some((it) => it.id === item.id)
34 | },
35 | set(items: BasketItem[]) {
36 | setItems(() => items)
37 | },
38 | addItem: (item) => setItems((items) => [...items, item]),
39 | remove(item: BasketItem) {
40 | setItems((items) => items.filter((it) => it.id !== item.id))
41 | },
42 | clear() {
43 | setItems(() => [])
44 | },
45 | size() {
46 | return items.length
47 | },
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/picker-starter/src/core/basket/index.ts:
--------------------------------------------------------------------------------
1 | export * from './basket'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './basket/BasketItem'
2 | export * from './basket'
3 | export * from './matchCategories'
4 | export * from './matchSearchTerm'
5 | export * from './service'
6 | export * from './types'
7 | export * from './setup'
8 |
--------------------------------------------------------------------------------
/picker-starter/src/core/matchCategories/index.ts:
--------------------------------------------------------------------------------
1 | export * from './matchCategories'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/core/matchCategories/matchCategories.ts:
--------------------------------------------------------------------------------
1 | import { MockItem } from '@/data'
2 |
3 | export const matchCategories =
4 | (categoryNames: string[]) => (items: MockItem) => {
5 | if (categoryNames.length === 0) {
6 | return true
7 | }
8 | return categoryNames.some((categoryName) => items.category === categoryName)
9 | }
10 |
--------------------------------------------------------------------------------
/picker-starter/src/core/matchSearchTerm/index.ts:
--------------------------------------------------------------------------------
1 | export * from './matchSearchTerm'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/core/matchSearchTerm/matchSearchTerm.test.ts:
--------------------------------------------------------------------------------
1 | import { matchSearchTerm } from './matchSearchTerm'
2 | import { BasketItem } from '../'
3 |
4 | const templateItem: BasketItem = {
5 | type: 'item',
6 | id: '123',
7 | description: undefined,
8 | image: undefined,
9 | name: '',
10 | }
11 |
12 | describe('matchSearchTerm', () => {
13 | it('should search the name', () => {
14 | expect(
15 | matchSearchTerm('gorilla')({
16 | ...templateItem,
17 | name: 'gorilla',
18 | }),
19 | ).toBeTruthy()
20 | })
21 | it('should search the description', () => {
22 | expect(
23 | matchSearchTerm('gorilla')({
24 | ...templateItem,
25 | description: 'gorilla',
26 | }),
27 | ).toBeTruthy()
28 | })
29 | it('should be case insensitive', () => {
30 | expect(
31 | matchSearchTerm('gorilla')({
32 | ...templateItem,
33 | name: 'Gorilla',
34 | }),
35 | ).toBeTruthy()
36 | })
37 | it('should not match', () => {
38 | expect(
39 | matchSearchTerm('gorilla')({
40 | ...templateItem,
41 | }),
42 | ).toBeFalsy()
43 | })
44 | test('that empty strings match all', () => {
45 | expect(
46 | matchSearchTerm('')({
47 | ...templateItem,
48 | }),
49 | ).toBeTruthy()
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/picker-starter/src/core/matchSearchTerm/matchSearchTerm.ts:
--------------------------------------------------------------------------------
1 | import { BasketItem } from '../'
2 |
3 | const stringContains = (searchTerm: string) => (text: string | undefined) =>
4 | typeof text === 'string' && new RegExp(searchTerm, 'gi').test(text)
5 |
6 | /**
7 | * Searches an item for a text occurrence. Case-insensitive.
8 | * @param searchTerm
9 | * @returns true if the item contains the text
10 | */
11 | export const matchSearchTerm =
12 | (searchTerm: string) =>
13 | (item: BasketItem): boolean => {
14 | const containsSearchTerm = stringContains(searchTerm)
15 | return containsSearchTerm(item.name) || containsSearchTerm(item.description)
16 | }
17 |
--------------------------------------------------------------------------------
/picker-starter/src/core/service.ts:
--------------------------------------------------------------------------------
1 | import { CursorPaginatedPageInfo, PagePaginatedPageInfo } from '.'
2 | import { hasKey } from '../utils'
3 |
4 | export const isPagePaginatedResult = (
5 | pageInfo: undefined | PagePaginatedPageInfo | CursorPaginatedPageInfo,
6 | ): pageInfo is PagePaginatedPageInfo => hasKey(pageInfo, 'totalCount')
7 |
8 | export const isCursorPaginatedResult = (
9 | pageInfo: undefined | PagePaginatedPageInfo | CursorPaginatedPageInfo,
10 | ): pageInfo is CursorPaginatedPageInfo => hasKey(pageInfo, 'endCursor')
11 |
12 | export const isUnpaginatedResult = (
13 | pageInfo: undefined | PagePaginatedPageInfo | CursorPaginatedPageInfo,
14 | ): pageInfo is undefined => typeof pageInfo === 'undefined'
15 |
--------------------------------------------------------------------------------
/picker-starter/src/core/setup.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'vue'
2 | import { OptionsParams, PickerPluginParams, TabItem } from './types'
3 |
4 | export type PickerConfig = {
5 | title?: string
6 | icon?: Component
7 | validateOptions?: () => ValidationResult
8 | tabs?: TabItem[]
9 | }
10 |
11 | export type ValidationResult = {
12 | isValid: boolean
13 | error?: string
14 | }
15 |
16 | export type PickerConfigFn = (optionsParams: OptionsParams) => PickerConfig
17 |
18 | export type PickerBuilderFn = (
19 | optionsParams: OptionsParams,
20 | ) => PickerPluginParams
21 |
22 | export const defineConfig =
23 | (fn: PickerConfigFn): PickerBuilderFn =>
24 | (optionsParams: OptionsParams): PickerPluginParams => {
25 | const { title, icon, tabs, validateOptions } = fn(optionsParams)
26 |
27 | return {
28 | title,
29 | icon,
30 | makeService: () => {
31 | const validation = validateOptions?.()
32 |
33 | if (validation?.isValid === false) {
34 | return {
35 | error: validation.error || 'Unknown error',
36 | }
37 | }
38 |
39 | return {
40 | value: { tabs: tabs || [] },
41 | }
42 | },
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/picker-starter/src/data/categories.ts:
--------------------------------------------------------------------------------
1 | export type MockCategory = {
2 | name: string
3 | }
4 |
5 | export const categories: MockCategory[] = [
6 | {
7 | name: 'Kitchen',
8 | },
9 | {
10 | name: 'Food',
11 | },
12 | {
13 | name: 'Beverages',
14 | },
15 | {
16 | name: 'Electronics',
17 | },
18 | {
19 | name: 'Clothing',
20 | },
21 | {
22 | name: 'Books',
23 | },
24 | {
25 | name: 'Home Decor',
26 | },
27 | {
28 | name: 'Literature',
29 | },
30 | {
31 | name: 'Hygiene',
32 | },
33 | ]
34 |
--------------------------------------------------------------------------------
/picker-starter/src/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './categories'
2 | export * from './items'
3 |
--------------------------------------------------------------------------------
/picker-starter/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { Tooltip } from '@storyblok/design-system/src/directives'
3 | import './style.css'
4 | import App from './App.vue'
5 |
6 | if (!document.querySelector('#app')) {
7 | // In production, `#app` may or may not exist.
8 | const rootElement = document.createElement('div')
9 | rootElement.id = 'app'
10 | document.body.appendChild(rootElement)
11 | }
12 |
13 | const app = createApp(App)
14 | app.directive('tooltip', Tooltip)
15 |
16 | app.mount('#app')
17 |
18 | // 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.
19 | throw new Error(
20 | `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`,
21 | )
22 |
--------------------------------------------------------------------------------
/picker-starter/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { numberFromString } from './utils'
2 |
3 | export const pluginName =
4 | import.meta.env.VITE_PLUGIN_NAME || 'picker-starter-plugin'
5 |
6 | export const throttleMs =
7 | numberFromString(import.meta.env.VITE_THROTTLE_MS) || 300
8 |
9 | export const defaultPerPage =
10 | numberFromString(import.meta.env.VITE_DEFAULT_PER_PAGE) || 12
11 |
--------------------------------------------------------------------------------
/picker-starter/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/vitest'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: auto;
3 | height: 100%;
4 | }
5 |
6 | #app {
7 | width: 100%;
8 | padding: 0 32px 32px;
9 | }
10 |
11 | /*
12 | ** Element Styles
13 | */
14 | body {
15 | margin: 0;
16 | display: flex;
17 | box-sizing: border-box;
18 | overflow: hidden;
19 | }
20 |
21 | /*
22 | ** Main Design Token variables
23 | */
24 | :root {
25 | --sb-color-base-50: #f6f7f8;
26 | --sb-color-base-700: #5f616e;
27 |
28 | --sb-color-primary-700: #05807f;
29 |
30 | --sb-color-secondary-100: #dde3ee;
31 | --sb-color-secondary-950: #1b243f;
32 |
33 | --sb-color-neutral-white: #ffffff;
34 |
35 | --sb-color-success-100: #d7f4e3;
36 | --sb-color-success-800: #13523b;
37 |
38 | --sb-color-danger-600: #e5271d;
39 | }
40 |
41 | /*
42 | ** Design System styles
43 | */
44 | /* -- SbNotification */
45 | .sb-notification.sb-notification--full {
46 | padding: 12px;
47 | }
48 | .sb-notification.sb-notification--negative {
49 | border: 1px solid var(--sb-color-danger-600);
50 | }
51 |
52 | /* -- SbButton */
53 | .sb-button.sb-button--primary {
54 | background-color: var(--sb-color-primary-700);
55 | }
56 |
57 | /* -- SbCheckbox */
58 | .sb-checkbox .sb-checkbox__native:checked + .sb-checkbox__input {
59 | background-color: var(--sb-color-primary-700);
60 | }
61 | .sb-checkbox .sb-checkbox__native:checked + .sb-checkbox__input {
62 | border-color: var(--sb-color-primary-700);
63 | }
64 |
65 | /* -- SbHeader */
66 | .sb-header .sb-header__title {
67 | font-size: 20px;
68 | }
69 | .sb-header .sb-header__subtitle {
70 | margin: 0;
71 | }
72 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/capitalizeWord/capitalizeWord.test.ts:
--------------------------------------------------------------------------------
1 | import { capitalizeWord } from './capitalizeWord'
2 |
3 | describe('capitalize', () => {
4 | it('should capitalize first letter', () => {
5 | expect(capitalizeWord('johannes')).toEqual('Johannes')
6 | })
7 | it('should leave all characters be except for the first letter', () => {
8 | expect(capitalizeWord('johanneS lindgren')).toEqual('JohanneS lindgren')
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/capitalizeWord/capitalizeWord.ts:
--------------------------------------------------------------------------------
1 | export const capitalizeWord = (word: string): string =>
2 | word.slice(0, 1).toUpperCase() + word.slice(1, word.length)
3 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/capitalizeWord/index.ts:
--------------------------------------------------------------------------------
1 | export * from './capitalizeWord'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/compareName/compareName.ts:
--------------------------------------------------------------------------------
1 | import { BasketItem } from '@/core'
2 |
3 | export const compareName = - (
4 | a: Item,
5 | b: Item,
6 | ): number => a.name.localeCompare(b.name)
7 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/compareName/index.ts:
--------------------------------------------------------------------------------
1 | export * from './compareName'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/getPage/getPage.test.ts:
--------------------------------------------------------------------------------
1 | import { getPage } from './getPage'
2 |
3 | const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
4 |
5 | describe('getPage', () => {
6 | it('should return the first page', () => {
7 | expect(getPage(items, 1, 2)).toEqual([1, 2])
8 | })
9 | it('should return all items when perPage is large', () => {
10 | expect(getPage(items, 1, 100)).toEqual(items)
11 | })
12 | it('should return an empty array when page is too large', () => {
13 | expect(getPage(items, 100, 10)).toEqual([])
14 | })
15 | it('should return a sliced array on the last page', () => {
16 | expect(getPage(items, 2, 8)).toEqual([9, 10, 11, 12])
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/getPage/getPage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param list
4 | * @param page counted from 1, not 0
5 | * @param pageSize
6 | */
7 | export const getPage = (list: T[], page: number, pageSize: number): T[] =>
8 | list.slice(pageSize * (page - 1), page * pageSize)
9 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/getPage/index.ts:
--------------------------------------------------------------------------------
1 | export * from './getPage'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/hasKey/hasKey.test.ts:
--------------------------------------------------------------------------------
1 | import { hasKey } from './hasKey'
2 |
3 | describe('hasKey', () => {
4 | it('should be false for numbers', () => {
5 | expect(hasKey(123, 'myKey')).toBeFalsy()
6 | })
7 | it('should be false for strings', () => {
8 | expect(hasKey(123, 'string')).toBeFalsy()
9 | })
10 | it('should be false for arrays', () => {
11 | expect(hasKey([1, 2, 3], 'myKey')).toBeFalsy()
12 | })
13 | it('should be false for undefined', () => {
14 | expect(hasKey(undefined, 'myKey')).toBeFalsy()
15 | })
16 | it('should be false for null', () => {
17 | expect(hasKey(null, 'myKey')).toBeFalsy()
18 | })
19 | it('should be false for object when key does not exist', () => {
20 | expect(hasKey({ a: 1 }, 'myKey')).toBeFalsy()
21 | })
22 | it('should be true for object when key exist', () => {
23 | expect(hasKey({ myKey: 1 }, 'myKey')).toBeTruthy()
24 | })
25 | it('should be true for object when key exist with other properties', () => {
26 | expect(hasKey({ a: 1, myKey: 1 }, 'myKey')).toBeTruthy()
27 | })
28 | })
29 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/hasKey/hasKey.ts:
--------------------------------------------------------------------------------
1 | export const hasKey = (
2 | obj: unknown,
3 | key: K,
4 | ): obj is { [P in K]: unknown } =>
5 | typeof obj === 'object' && obj !== null && key in obj
6 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/hasKey/index.ts:
--------------------------------------------------------------------------------
1 | export * from './hasKey'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './capitalizeWord'
2 | export * from './compareName'
3 | export * from './getPage'
4 | export * from './initials'
5 | export * from './hasKey'
6 | export * from './pseudoRandom'
7 | export * from './pseudoRandomColor'
8 | export * from './unsafeHash'
9 | export * from './numberFromString'
10 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/initials/index.ts:
--------------------------------------------------------------------------------
1 | export * from './initials'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/initials/initials.test.ts:
--------------------------------------------------------------------------------
1 | import { initials } from './initials'
2 |
3 | describe('initials()', () => {
4 | it('Should give zero letters for an empty string', () => {
5 | expect(initials('')).toEqual('')
6 | })
7 | it('Should give one letter for a single word', () => {
8 | expect(initials('Johannes')).toEqual('J')
9 | })
10 | it('Should give two letters for a double words', () => {
11 | expect(initials('Johannes Lindgren')).toEqual('JL')
12 | })
13 | it('Should give three letters for a triple words', () => {
14 | expect(initials('Johannes Arnold Lindgren')).toEqual('JAL')
15 | })
16 | it('Should give two letters for a many words', () => {
17 | expect(initials('Johannes Arnold Thomas Lindgren')).toEqual('JAT')
18 | })
19 | it('The letters should be capitalized', () => {
20 | expect(initials('johannes lindgren')).toEqual('JL')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/initials/initials.ts:
--------------------------------------------------------------------------------
1 | export const initials = (text: string): string =>
2 | text
3 | .split(' ')
4 | .map((word) => word[0])
5 | .slice(0, 3)
6 | .join('')
7 | .toUpperCase()
8 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/numberFromString/index.ts:
--------------------------------------------------------------------------------
1 | export * from './numberFromString'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/numberFromString/numberFromString.test.ts:
--------------------------------------------------------------------------------
1 | import { numberFromString } from './numberFromString'
2 |
3 | describe('numberFromString', () => {
4 | it('should parse natural numbers', () => {
5 | expect(numberFromString('123')).toEqual(123)
6 | })
7 | it('should parse integers', () => {
8 | expect(numberFromString('123')).toEqual(123)
9 | expect(numberFromString('-123')).toEqual(-123)
10 | })
11 | it('should parse floating numbers', () => {
12 | const parsed = numberFromString('1.5')
13 | const epsilon = 1e-3
14 | expect(parsed).toBeLessThanOrEqual(1.5 + epsilon)
15 | expect(parsed).toBeGreaterThanOrEqual(1.5 - epsilon)
16 | })
17 | it('should not return infinity for large numbers', () => {
18 | const googolStr = `1${new Array(1000).fill(0).join('')}`
19 | expect(numberFromString(googolStr)).toBeUndefined()
20 | })
21 | it('should not return infinity for small numbers', () => {
22 | const googolStr = `-1${new Array(1000).fill(0).join('')}`
23 | expect(numberFromString(googolStr)).toBeUndefined()
24 | })
25 | describe('malformatted numbers', () => {
26 | test('fractions', () => {
27 | expect(numberFromString('1/2')).toBeUndefined()
28 | })
29 | test('text followed by numbers', () => {
30 | expect(numberFromString('hello123')).toBeUndefined()
31 | })
32 | test('numbers followed by text', () => {
33 | expect(numberFromString('123hello')).toBeUndefined()
34 | })
35 | test('numbers in text', () => {
36 | expect(numberFromString('hello123hello')).toBeUndefined()
37 | })
38 | test('text', () => {
39 | expect(numberFromString('hello')).toBeUndefined()
40 | })
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/numberFromString/numberFromString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Use instead of parseInt
3 | */
4 | export const numberFromString = (str: string): number | undefined => {
5 | const parsed = Number(str)
6 | return !isNaN(parsed) && isFinite(parsed) ? parsed : undefined
7 | }
8 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/pseudoRandom/index.ts:
--------------------------------------------------------------------------------
1 | export * from './pseudoRandom'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/pseudoRandom/pseudoRandom.ts:
--------------------------------------------------------------------------------
1 | import { unsafeHash } from '../unsafeHash'
2 |
3 | /**
4 | * Min = 0
5 | * @param seed
6 | * @param max
7 | */
8 | export const pseudoRandom = (seed: string, max: number): number => {
9 | return Math.abs(unsafeHash(seed)) % (max + 1)
10 | }
11 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/pseudoRandomColor/index.ts:
--------------------------------------------------------------------------------
1 | export * from './pseudoRandomColor'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/pseudoRandomColor/pseudoRandomColor.ts:
--------------------------------------------------------------------------------
1 | import { pseudoRandom } from '../'
2 |
3 | const palette = [
4 | '#00B3B0',
5 | '#40C6C4',
6 | '#7FD9D7',
7 | '#D9F4F3',
8 | '#1B243F',
9 | '#545B6F',
10 | '#8D919F',
11 | '#C6C8CF',
12 | '#2DB47D',
13 | '#62C79E',
14 | '#96D9BE',
15 | '#CAECDE',
16 | '#395ECE',
17 | '#6B87DB',
18 | '#9CAEE6',
19 | '#CDD7F3',
20 | '#FBCE41',
21 | '#FCDB71',
22 | '#FDE6A0',
23 | '#FEF3CF',
24 | '#FFAC00',
25 | '#FFC140',
26 | '#FFD57F',
27 | '#FFEABF',
28 | '#FF6159',
29 | '#FF8983',
30 | '#FFB0AC',
31 | '#FFD7D5',
32 | ] as const
33 |
34 | // TODO rename to psuedoRandomColor
35 | export const pseudoRandomColor = (seed: string): string => {
36 | const max = palette.length - 1
37 | const index = pseudoRandom(seed, max)
38 |
39 | return palette[index] ?? palette[0]
40 | }
41 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/unsafeHash/index.ts:
--------------------------------------------------------------------------------
1 | export * from './unsafeHash'
2 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/unsafeHash/unsafeHash.test.ts:
--------------------------------------------------------------------------------
1 | import { unsafeHash } from './unsafeHash'
2 |
3 | describe('unsafeHash', () => {
4 | describe('purity', () => {
5 | it('should always returns the same output for the same input', () => {
6 | expect(unsafeHash('Hello')).toEqual(unsafeHash('Hello'))
7 | expect(unsafeHash('ABC')).toEqual(unsafeHash('ABC'))
8 | expect(unsafeHash('123')).toEqual(unsafeHash('123'))
9 | expect(unsafeHash('Hello Johannes')).toEqual(unsafeHash('Hello Johannes'))
10 | const longString =
11 | '9083274509238(*&(*#&%$(*&$#)(%IOKJDkjshfdkjsdahfkj320948u0329uoiksdajfds09U09 U)(*U*)(ODFJQ ()*J#@'
12 | expect(unsafeHash(longString)).toEqual(unsafeHash(longString))
13 | })
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/picker-starter/src/utils/unsafeHash/unsafeHash.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns a hash code from a string. Used to generate pseudo-random numbers.
3 | * @param {String} str The string to hash.
4 | * @return {Number} A 32bit integer
5 | * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
6 | */
7 | export const unsafeHash = (str: string): number => {
8 | let hash = 0
9 | for (let i = 0, len = str.length; i < len; i++) {
10 | const chr = str.charCodeAt(i)
11 | hash = (hash << 5) - hash + chr
12 | hash |= 0
13 | }
14 | return hash
15 | }
16 |
--------------------------------------------------------------------------------
/picker-starter/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/picker-starter/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "skipLibCheck": true,
14 | "noEmit": true,
15 | "allowJs": true,
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | },
19 | "types": ["vite/client", "jest"]
20 | },
21 | "include": [
22 | "src/**/*.ts",
23 | "src/**/*.d.ts",
24 | "src/**/*.tsx",
25 | "src/**/*.vue",
26 | "index.d.ts"
27 | ],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/picker-starter/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/picker-starter/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite'
3 | import vue from '@vitejs/plugin-vue'
4 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
5 | import { plugins } from '@storyblok/field-plugin/vite'
6 | import { resolve } from 'path'
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | test: {
11 | globals: true,
12 | environment: 'jsdom',
13 | setupFiles: ['./src/setupTests.ts'],
14 | },
15 | plugins: [vue(), cssInjectedByJsPlugin(), ...plugins],
16 | resolve: {
17 | alias: {
18 | '@': resolve(__dirname, 'src'),
19 | },
20 | },
21 | build: {
22 | rollupOptions: {
23 | output: {
24 | format: 'commonjs',
25 | entryFileNames: `[name].js`,
26 | chunkFileNames: `[name].js`,
27 | assetFileNames: `[name].[ext]`,
28 | },
29 | },
30 | },
31 | server: {
32 | port: 8080,
33 | host: true,
34 | },
35 | })
36 |
--------------------------------------------------------------------------------
/slider/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
--------------------------------------------------------------------------------
/slider/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | storybook-static/
3 | dist/
4 | certificates/
--------------------------------------------------------------------------------
/slider/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | 'browser': true,
5 | 'es2021': true,
6 | jest: true
7 | },
8 | // 'parser': '@typescript-eslint/parser',
9 | overrides: [{
10 | "files": "*.ts",
11 | "parser": "@typescript-eslint/parser",
12 | extends: ['plugin:@typescript-eslint/recommended'],
13 | "rules": {} // Override rules as well for TS files. Basically any config option can be modified here
14 | }, {
15 | "files": "*.vue",
16 | "parser": "vue-eslint-parser",
17 | extends: [
18 | "plugin:vue/essential",
19 | "eslint:recommended",
20 | '@vue/typescript'],
21 | "rules": {} // Override rules as well for TS files. Basically any config option can be modified here
22 | }],
23 |
24 | extends: [
25 | 'eslint:recommended',
26 | 'plugin:vue/recommended',
27 | 'plugin:prettier-vue/recommended',
28 | 'prettier',
29 | 'plugin:storybook/recommended'
30 | ],
31 | parserOptions: {
32 | ecmaVersion: 12,
33 | sourceType: 'module'
34 | },
35 | plugins: [
36 | '@typescript-eslint',
37 | 'vue',
38 | 'prettier'
39 | ],
40 | rules: {
41 | curly: 'error',
42 | 'prettier-vue/prettier': ['warn', {
43 | bracketSpacing: true,
44 | printWidth: 80,
45 | semi: false,
46 | singleQuote: true,
47 | trailingComma: 'all',
48 | singleAttributePerLine: true
49 | }],
50 | '@typescript-eslint/no-unused-vars': ['warn', {
51 | args: "none",
52 | argsIgnorePattern: '^_',
53 | varsIgnorePattern: '^_',
54 | caughtErrorsIgnorePattern: '^_'
55 | }]
56 | }
57 | };
--------------------------------------------------------------------------------
/slider/.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*
--------------------------------------------------------------------------------
/slider/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets
2 |
3 | This folder contains assets for the apps in the App Directory.
--------------------------------------------------------------------------------
/slider/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/slider/assets/screenshot.png
--------------------------------------------------------------------------------
/slider/babel.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
--------------------------------------------------------------------------------
/slider/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/slider/docs/demo.gif
--------------------------------------------------------------------------------
/slider/docs/gigabit-per-second.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/slider/docs/gigabit-per-second.gif
--------------------------------------------------------------------------------
/slider/docs/percent.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/slider/docs/percent.gif
--------------------------------------------------------------------------------
/slider/docs/rotation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/slider/docs/rotation.gif
--------------------------------------------------------------------------------
/slider/jest.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testRegex: 'src/.*\\.test\\.(js|jsx|ts|tsx)$',
5 | coverageDirectory: 'dist/reports/coverage',
6 | moduleNameMapper: {
7 | "^@/(.*)$": "/src/$1",
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/slider/public/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/slider/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | storyblok-fieldtype
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/slider/src/components/HorizontalSlider/index.ts:
--------------------------------------------------------------------------------
1 | export { default as HorizontalSlider } from './HorizontalSlider.vue'
2 |
--------------------------------------------------------------------------------
/slider/src/components/HorizontalSlider/valueFromCoordinate.ts:
--------------------------------------------------------------------------------
1 | export const valueFromCoordinate = (
2 | x: number,
3 | width: number,
4 | minValue: number,
5 | maxValue: number,
6 | ): number => {
7 | return (x * (maxValue - minValue)) / width + minValue
8 | }
9 |
--------------------------------------------------------------------------------
/slider/src/components/HorizontalSlider/xCoordinateFromValue.ts:
--------------------------------------------------------------------------------
1 | export const xCoordinateFromValue = (
2 | value: number,
3 | width: number,
4 | minValue: number,
5 | maxValue: number,
6 | ): number => {
7 | return ((value - minValue) * width) / (maxValue - minValue)
8 | }
9 |
--------------------------------------------------------------------------------
/slider/src/components/Thumb/Thumb.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
31 |
32 |
--------------------------------------------------------------------------------
/slider/src/components/Thumb/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Thumb } from './Thumb.vue'
2 |
--------------------------------------------------------------------------------
/slider/src/components/Tooltip/Tooltip.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
28 |
29 |
--------------------------------------------------------------------------------
/slider/src/components/Tooltip/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Tooltip } from './Tooltip.vue'
2 |
--------------------------------------------------------------------------------
/slider/src/entries/preview.ts:
--------------------------------------------------------------------------------
1 | import { loadPlugin } from '@/lib/loadPlugin'
2 | import SliderPlugin from '@/components/SliderPlugin.vue'
3 |
4 | loadPlugin(
5 | 'storyblok-slider-preview',
6 | {
7 | value: undefined,
8 | },
9 | SliderPlugin,
10 | )
11 |
--------------------------------------------------------------------------------
/slider/src/entries/production.ts:
--------------------------------------------------------------------------------
1 | import { loadPlugin } from '@/lib/loadPlugin'
2 | import SliderPlugin from '@/components/SliderPlugin.vue'
3 |
4 | loadPlugin(
5 | 'storyblok-slider',
6 | {
7 | value: undefined,
8 | },
9 | SliderPlugin,
10 | )
11 |
--------------------------------------------------------------------------------
/slider/src/lib/Plugin.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'vue'
2 |
3 | type Data = {
4 | isModalOpen: boolean
5 | }
6 |
7 | export type Plugin = {
8 | name: string
9 | initialValue: Record
10 | Component: Component
11 | }
12 |
13 | export const makeStoryblokPluginComponent = (plugin: Plugin) => {
14 | const Vue = window.Storyblok.vue
15 | const pluginMixin = window.Storyblok.plugin
16 |
17 | return Vue.component('TestComponent', {
18 | mixins: [pluginMixin],
19 | data() {
20 | return {
21 | isModalOpen: false,
22 | }
23 | },
24 | methods: {
25 | setModalOpen(value: boolean) {
26 | this.isModalOpen = value
27 | this.$emit('toggle-modal', value)
28 | },
29 | getPluginName() {
30 | return plugin.name
31 | },
32 | initWith() {
33 | return {
34 | plugin: this.getPluginName(),
35 | ...plugin.initialValue,
36 | }
37 | },
38 | setValue(value: Record) {
39 | this.$emit('changed-model', {
40 | plugin: this.getPluginName(),
41 | ...value,
42 | })
43 | },
44 | },
45 | render(createElement) {
46 | return createElement(plugin.Component, {
47 | props: {
48 | isModalOpen: this.isModalOpen,
49 | setModalOpen: (value: boolean) => this.setModalOpen(value),
50 | options: this.options,
51 | setValue: (value: Record) => this.setValue(value),
52 | value: this.model,
53 | },
54 | })
55 | },
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/slider/src/lib/README.md:
--------------------------------------------------------------------------------
1 | This folder contains a module that will be extracted to a general-purpose library for Storyblok field types
--------------------------------------------------------------------------------
/slider/src/lib/components/FieldModal/FieldModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/slider/src/lib/components/FieldModal/index.ts:
--------------------------------------------------------------------------------
1 | // TODO use to simulate the Storyblok Visual Editor and enable running locally without Storyblok's field type editor
2 | export { default as FieldModal } from './FieldModal.vue'
3 |
--------------------------------------------------------------------------------
/slider/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loadPlugin'
2 | export * from './types'
3 | export * from './makeStoryblokPluginComponent'
4 |
--------------------------------------------------------------------------------
/slider/src/lib/pluginPropsDef.ts:
--------------------------------------------------------------------------------
1 | import { RecordPropsDefinition } from 'vue/types/options'
2 | import { WrapperPluginProps } from '@/lib/types'
3 |
4 | export const pluginPropsDef: RecordPropsDefinition = {
5 | isModalOpen: {
6 | type: Boolean,
7 | required: true,
8 | },
9 | setModalOpen: {
10 | type: Function,
11 | required: true,
12 | },
13 | /**
14 | * The options is a Record
15 | */
16 | options: {
17 | type: Object,
18 | required: true,
19 | },
20 | setValue: {
21 | type: Function,
22 | required: true,
23 | },
24 | value: {
25 | type: Object,
26 | required: true,
27 | },
28 | name: {
29 | type: String,
30 | required: true,
31 | },
32 | spaceId: {
33 | type: Number,
34 | default: null,
35 | },
36 | token: {
37 | type: String,
38 | default: null,
39 | },
40 | }
41 |
--------------------------------------------------------------------------------
/slider/src/lib/types/PluginComponentProps.ts:
--------------------------------------------------------------------------------
1 | export type PluginComponentProps = Record
2 |
--------------------------------------------------------------------------------
/slider/src/lib/types/RootComponentData.ts:
--------------------------------------------------------------------------------
1 | export type RootComponentData = {
2 | isModalOpen: boolean
3 | }
4 |
--------------------------------------------------------------------------------
/slider/src/lib/types/RootPluginComponentComputed.ts:
--------------------------------------------------------------------------------
1 | export type RootPluginComponentComputed = Record
2 |
--------------------------------------------------------------------------------
/slider/src/lib/types/RootPluginComponentMethods.ts:
--------------------------------------------------------------------------------
1 | export type RootPluginComponentMethods = {
2 | setModalOpen(value: boolean): void
3 | getPluginName(): string
4 | initWith(): {
5 | plugin: string
6 | } & Record
7 | setValue(value: Record): void
8 | }
9 |
--------------------------------------------------------------------------------
/slider/src/lib/types/RootPluginProps.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The props to the root component that uses the mixin from the global Storyblok variable
3 | */
4 | export type RootPluginProps = {
5 | token: string | undefined | null
6 | spaceId: number | undefined | null
7 | options: Record
8 | name: string
9 | }
10 |
--------------------------------------------------------------------------------
/slider/src/lib/types/StoryblokFieldType.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'vue'
2 | import { WrapperPluginProps } from '@/lib/types/WrapperPluginProps'
3 |
4 | export type StoryblokFieldType = {
5 | name: string
6 | initialValue: Record
7 | Component: Component
8 | }
9 |
--------------------------------------------------------------------------------
/slider/src/lib/types/WrapperPluginProps.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The props that are passed to the top-level component
3 | */
4 | export type WrapperPluginProps = {
5 | name: string
6 | isModalOpen: boolean
7 | setModalOpen: (value: boolean) => void
8 | options: Record
9 | setValue: (value: Record) => void
10 | value: Record // TODO generic type for output
11 | spaceId: number | null // or is it number?
12 | token: string | null
13 | }
14 |
--------------------------------------------------------------------------------
/slider/src/lib/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './StoryblokFieldType'
2 | export * from './WrapperPluginProps'
3 | export * from './RootComponentData'
4 | export * from './RootPluginComponentComputed'
5 | export * from './PluginComponentProps'
6 | export * from './RootPluginComponentMethods'
7 |
--------------------------------------------------------------------------------
/slider/src/styles.scss:
--------------------------------------------------------------------------------
1 | $color-ink: #1B243F;
2 | $color-teal: #00b3b0;
3 | $color-white: #ffffff;
4 | $color-grey: #dfe3e8;
5 | $color-text-primary: $color-ink;
6 | $color-text-secondary: #8D919C;
7 | $border: 1px solid $color-grey;
8 | $box-shadow: 0px 2px 17px 3px rgba(34, 42, 69, 0.07);
9 |
10 | @mixin transition($props...){
11 | transition-property: $props;
12 | will-change: $props;
13 | //transition-duration: 1000ms;
14 | transition-duration: 100ms;
15 | transition-timing-function: ease-in-out;
16 | }
17 |
18 | @mixin typography-label {
19 | font-size: 1.1rem;
20 | font-style: normal;
21 | font-weight: 400;
22 | line-height: 11px;
23 | }
24 |
25 | @mixin centerX {
26 | left: 0;
27 | transform: translateX(50%);
28 | }
29 |
30 | @mixin centerY {
31 | top: 50%;
32 | transform: translateY(-50%);
33 | }
--------------------------------------------------------------------------------
/slider/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './numberFromString'
2 | export * from './roundToNearest'
3 |
--------------------------------------------------------------------------------
/slider/src/utils/numberFromString/index.ts:
--------------------------------------------------------------------------------
1 | export * from './numberFromString'
2 |
--------------------------------------------------------------------------------
/slider/src/utils/numberFromString/numberFromString.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Parses a number 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 numberFromString = (str: string): number | undefined => {
6 | // eslint-disable-next-line no-restricted-syntax
7 | const parsed = Number(str)
8 | return str !== '' && !hasWhiteSpace(str) && !isNaN(parsed) && isFinite(parsed)
9 | ? parsed
10 | : undefined
11 | }
12 |
13 | /**
14 | * @param str
15 | * @returns `true` if any character is a whitespace.
16 | */
17 | const hasWhiteSpace = (str: string): boolean => {
18 | return /\s+/.test(str)
19 | }
20 |
--------------------------------------------------------------------------------
/slider/src/utils/roundToNearest/index.ts:
--------------------------------------------------------------------------------
1 | export * from './roundToNearest'
2 |
--------------------------------------------------------------------------------
/slider/src/utils/roundToNearest/roundToNearest.ts:
--------------------------------------------------------------------------------
1 | export const roundToNearest = (value: number, multiple: number): number => {
2 | return Math.round(value / multiple) * multiple
3 | }
4 |
--------------------------------------------------------------------------------
/slider/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "moduleResolution": "node",
8 | "resolveJsonModule": true,
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "useDefineForClassFields": true,
15 | "noUncheckedIndexedAccess": true,
16 | "sourceMap": true,
17 | "baseUrl": ".",
18 | "types": [
19 | "webpack-env","jest"
20 | ],
21 | "paths": {
22 | "@/*": [
23 | "src/*"
24 | ]
25 | },
26 | "lib": [
27 | "esnext",
28 | "dom",
29 | "dom.iterable",
30 | "scripthost"
31 | ]
32 | },
33 | "include": [
34 | "src/**/*.ts",
35 | "src/**/*.tsx",
36 | "src/**/*.vue",
37 | "tests/**/*.ts",
38 | "tests/**/*.tsx"
39 | ],
40 | "exclude": [
41 | "node_modules"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/slider/vue.config.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path');
3 |
4 | const entriesDir = './src/entries'
5 | const entry = fs
6 | .readdirSync(entriesDir)
7 | .reduce(
8 | (entry, filename) => {
9 | const name = path.basename(filename, '.ts')
10 | entry[name] = `${entriesDir}/${filename}`
11 | return entry
12 | },
13 | {}
14 | )
15 |
16 | const pages = process.env.NODE_ENV === 'production' ? (
17 | entry
18 | ) : undefined
19 |
20 | const publicUrl = 'http://localhost:8080'
21 |
22 | module.exports = {
23 | pages,
24 | configureWebpack: {
25 | entry: entry,
26 | output: {
27 | filename: '[name].js'
28 | },
29 | optimization: {
30 | splitChunks: false
31 | },
32 | plugins: [
33 | new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)({
34 | analyzerMode: 'static',
35 | openAnalyzer: false,
36 | reportFilename: 'reports/bundle-size.html'
37 | })
38 | ],
39 | },
40 | filenameHashing: false,
41 | runtimeCompiler: true,
42 | productionSourceMap: false,
43 | css: {
44 | extract: false
45 | },
46 | devServer: {
47 | // Using secure tunnel
48 | public: publicUrl,
49 | disableHostCheck: true,
50 | },
51 | }
--------------------------------------------------------------------------------
/star-rating/.env.local.example:
--------------------------------------------------------------------------------
1 | STORYBLOK_PERSONAL_ACCESS_TOKEN=
2 |
--------------------------------------------------------------------------------
/star-rating/.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 |
--------------------------------------------------------------------------------
/star-rating/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.16.0
--------------------------------------------------------------------------------
/star-rating/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "semi": false,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "singleAttributePerLine": true
8 | }
9 |
--------------------------------------------------------------------------------
/star-rating/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/demo.gif
--------------------------------------------------------------------------------
/star-rating/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/screenshot.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-1.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-2.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-3.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-4.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-5.png
--------------------------------------------------------------------------------
/star-rating/docs/setup-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/setup-6.png
--------------------------------------------------------------------------------
/star-rating/docs/star-rating-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/star-rating-demo.gif
--------------------------------------------------------------------------------
/star-rating/docs/star-rating-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/star-rating/docs/star-rating-screenshot.png
--------------------------------------------------------------------------------
/star-rating/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Star Rating Field Plugin
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/star-rating/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "star-rating",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vue-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.2",
14 | "vue": "^3.2.47"
15 | },
16 | "devDependencies": {
17 | "@vitejs/plugin-vue": "^4.1.0",
18 | "typescript": "^4.9.3",
19 | "vite": "^4.2.2",
20 | "vite-plugin-css-injected-by-js": "3.1.0",
21 | "vue-tsc": "^1.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/star-rating/src/App.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 | Loading...
9 | Error
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/star-rating/src/components/AmountInvalidAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Invalid Option
4 |
5 | The amount
option should be greater than 0
6 |
7 |
8 |
9 |
10 |
22 |
--------------------------------------------------------------------------------
/star-rating/src/components/StarIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/star-rating/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './style.css'
3 | import App from './App.vue'
4 |
5 | if (!document.querySelector('#app')) {
6 | // In production, `#app` may or may not exist.
7 | const rootElement = document.createElement('div')
8 | rootElement.id = 'app'
9 | document.body.appendChild(rootElement)
10 | }
11 | createApp(App).mount('#app')
12 |
13 | // 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.
14 | throw new Error(
15 | `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`,
16 | )
17 |
--------------------------------------------------------------------------------
/star-rating/src/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
2 |
3 | html {
4 | overflow: hidden;
5 | }
6 |
7 | :root {
8 | /*Storyblok colors*/
9 | --sb_green: #00b3b0;
10 | --sb_green_75: #40c6c4;
11 | --sb_green_50: #7fd9d7;
12 | --sb_green_25: #d9f4f3;
13 | --sb_dark_blue: #1b243f;
14 | --sb_dark_blue_75: #545b6f;
15 | --sb_dark_blue_50: #8d919f;
16 | --sb_dark_blue_25: #c6c8cf;
17 | --green: #2db47d;
18 | --green_75: #62c79e;
19 | --green_50: #96d9be;
20 | --green_25: #caecde;
21 | --green_disabled: #004e4c;
22 | --yellow: #fbce41;
23 | --yellow_75: #fcdb71;
24 | --yellow_50: #fde6a0;
25 | --yellow_25: #fef3cf;
26 | --blue: #395ece;
27 | --blue_75: #6b87db;
28 | --blue_50: #9caee6;
29 | --blue_25: #cdd7f3;
30 | --orange: #ffac00;
31 | --orange_75: #ffc140;
32 | --orange_50: #ffd57f;
33 | --orange_25: #ffeabf;
34 | --red: #ff6159;
35 | --red_75: #ff8983;
36 | --red_50: #ffb0ac;
37 | --red_25: #ffd7d5;
38 | --light: #dfe3e8;
39 | --light_75: #e7eaee;
40 | --light_50: #eff1f3;
41 | --light_25: #f7f8f9;
42 | --light_gray: #b1b5be;
43 | --black: #101525;
44 | --white: #fff;
45 |
46 | font-family: 'Roboto', sans-serif;
47 | font-style: normal;
48 | line-height: 1.5;
49 | font-weight: 400;
50 | font-size: 1rem;
51 | font-synthesis: none;
52 | text-rendering: optimizeLegibility;
53 | -webkit-font-smoothing: antialiased;
54 | -moz-osx-font-smoothing: grayscale;
55 | -webkit-text-size-adjust: 100%;
56 | }
57 |
58 | #app {
59 | width: 100%;
60 | }
61 |
62 | /*Element Styles*/
63 | body {
64 | margin: 0;
65 | display: flex;
66 | box-sizing: border-box;
67 | }
68 |
--------------------------------------------------------------------------------
/star-rating/src/useFieldPlugin.ts:
--------------------------------------------------------------------------------
1 | import { FieldPluginResponse } from '@storyblok/field-plugin'
2 | import { inject } from 'vue'
3 |
4 | export function useFieldPlugin() {
5 | const plugin = inject(
6 | 'field-plugin',
7 | () => {
8 | throw new Error(
9 | `You need to wrap your app with \`\` component.`,
10 | )
11 | },
12 | true,
13 | )
14 |
15 | if (plugin.type !== 'loaded') {
16 | throw new Error(
17 | 'The plugin is not loaded, yet `useFieldPlugin()` was invoked. Ensure that the component that invoked `useFieldPlugin()` is wrapped within ``, and that it is placed within the default slot.'
18 | )
19 | }
20 |
21 | return plugin as Extract
22 | }
23 |
--------------------------------------------------------------------------------
/star-rating/src/utils/convertToRaw.ts:
--------------------------------------------------------------------------------
1 | import { toRaw, isProxy, isRef, unref } from 'vue'
2 |
3 | export function convertToRaw(value: any): any {
4 | let rawValue = value
5 | if (isProxy(rawValue)) {
6 | rawValue = toRaw(rawValue)
7 | }
8 | if (isRef(rawValue)) {
9 | rawValue = unref(rawValue)
10 | }
11 |
12 | if (isObject(rawValue)) {
13 | return Object.keys(rawValue).reduce((result, key) => {
14 | result[key] = convertToRaw(rawValue[key])
15 | return result
16 | }, rawValue)
17 | } else if (Array.isArray(rawValue)) {
18 | return rawValue.map(convertToRaw)
19 | } else {
20 | return rawValue
21 | }
22 | }
23 |
24 | function isObject(value: unknown) {
25 | return Object.prototype.toString.call(value) === '[object Object]'
26 | }
27 |
--------------------------------------------------------------------------------
/star-rating/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './convertToRaw'
2 |
--------------------------------------------------------------------------------
/star-rating/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/star-rating/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "moduleResolution": "Node",
7 | "strict": true,
8 | "jsx": "preserve",
9 | "resolveJsonModule": true,
10 | "isolatedModules": true,
11 | "esModuleInterop": true,
12 | "lib": ["ESNext", "DOM"],
13 | "skipLibCheck": true,
14 | "noEmit": true,
15 | "types": ["vite/client"]
16 | },
17 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
18 | "references": [{ "path": "./tsconfig.node.json" }]
19 | }
20 |
--------------------------------------------------------------------------------
/star-rating/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts",
10 | "fieldPlugin.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/tags/.env.local.example:
--------------------------------------------------------------------------------
1 | STORYBLOK_PERSONAL_ACCESS_TOKEN=
2 |
--------------------------------------------------------------------------------
/tags/.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 |
--------------------------------------------------------------------------------
/tags/.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 |
--------------------------------------------------------------------------------
/tags/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.16.0
--------------------------------------------------------------------------------
/tags/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 80,
4 | "semi": false,
5 | "singleQuote": true,
6 | "trailingComma": "all",
7 | "singleAttributePerLine": true
8 | }
9 |
--------------------------------------------------------------------------------
/tags/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets
2 |
3 | This folder contains assets for the apps in the App Directory.
--------------------------------------------------------------------------------
/tags/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/tags/docs/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/tags/docs/demo.gif
--------------------------------------------------------------------------------
/tags/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/storyblok/field-type-examples/7b91b7a486c8385fa3ea78105436d82d95c4c9f8/tags/docs/screenshot.png
--------------------------------------------------------------------------------
/tags/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Storyblok Field Plugin
10 |
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tags/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tags",
3 | "private": false,
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.13.7",
16 | "@storyblok/field-plugin": "0.0.1-beta.2",
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": "latest",
25 | "@typescript-eslint/parser": "5.55.0",
26 | "@vitejs/plugin-react": "^3.1.0",
27 | "eslint": "latest",
28 | "eslint-plugin-react": "7.30.0",
29 | "prettier": "^3.0.0",
30 | "typescript": "^4.9.3",
31 | "vite": "^4.2.2",
32 | "vite-plugin-css-injected-by-js": "3.1.0"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tags/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { FunctionComponent } from 'react'
2 | import { FieldPluginProvider } from './FieldPluginProvider'
3 | import { CssBaseline, ThemeProvider } from '@mui/material'
4 | import { lightTheme } from '@storyblok/mui'
5 | import Tag from './components/Tag'
6 |
7 | const App: FunctionComponent = () => {
8 | return (
9 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | const Loading: FunctionComponent = () => Loading...
22 | const Error: FunctionComponent<{ error: Error }> = (props) => {
23 | console.error(props.error)
24 | return An error occured, please see the console for more details.
25 | }
26 | export default App
27 |
--------------------------------------------------------------------------------
/tags/src/FieldPluginProvider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FunctionComponent,
3 | createContext,
4 | useEffect,
5 | useState,
6 | ComponentType,
7 | } from 'react'
8 | import {
9 | type FieldPluginResponse,
10 | createFieldPlugin,
11 | } from '@storyblok/field-plugin'
12 | import { ReactNode } from 'react'
13 |
14 | export const FieldPluginContext = createContext<
15 | Extract | undefined
16 | >(undefined)
17 |
18 | type Props = {
19 | Error?: ComponentType<{ error: Error }>
20 | Loading?: ComponentType
21 | children?: ReactNode
22 | }
23 |
24 | export const FieldPluginProvider: FunctionComponent = ({
25 | Error,
26 | Loading,
27 | children,
28 | }) => {
29 | const [state, setState] = useState({
30 | type: 'loading',
31 | })
32 |
33 | useEffect(() => createFieldPlugin(setState), [])
34 |
35 | if (state.type === 'loading') {
36 | return Loading ? : <>>
37 | } else if (state.type === 'error') {
38 | return Error ? : <>>
39 | } else {
40 | return (
41 |
42 | {children}
43 |
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tags/src/createRootElement.ts:
--------------------------------------------------------------------------------
1 | export const createRootElement = (id?: string): HTMLElement => {
2 | // In production, `#app` may or may not exist.
3 | const rootElement = document.createElement('div')
4 | rootElement.id = id ?? 'app'
5 | return rootElement
6 | }
7 |
--------------------------------------------------------------------------------
/tags/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client'
2 | import App from './App'
3 | import './style.css'
4 | import { createRootElement } from './createRootElement'
5 |
6 | const rootNode = createRootElement()
7 | document.body.appendChild(rootNode)
8 |
9 | createRoot(rootNode).render()
10 |
11 | // 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.
12 | throw new Error(
13 | `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`,
14 | )
15 |
--------------------------------------------------------------------------------
/tags/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 | }
12 |
--------------------------------------------------------------------------------
/tags/src/useFieldPlugin.ts:
--------------------------------------------------------------------------------
1 | import { FieldPluginResponse } from '@storyblok/field-plugin'
2 | import { useContext } from 'react'
3 | import { FieldPluginContext } from './FieldPluginProvider'
4 |
5 | export const useFieldPlugin = (): Extract<
6 | FieldPluginResponse,
7 | { type: 'loaded' }
8 | > => {
9 | const plugin = useContext(FieldPluginContext)
10 | if (!plugin) {
11 | throw new Error(
12 | 'The plugin is not loaded, yet `useFieldPlugin()` was invoked. Ensure that the component that invoked `useFieldPlugin()` is wrapped within ``.',
13 | )
14 | }
15 |
16 | return plugin
17 | }
18 |
--------------------------------------------------------------------------------
/tags/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tags/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 | },
19 | "include": ["src"],
20 | "references": [
21 | {
22 | "path": "./tsconfig.node.json"
23 | }
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/tags/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": [
9 | "vite.config.ts",
10 | "fieldPlugin.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------