diff --git a/.github/workflows/github-pages.yaml b/.github/workflows/github-pages.yaml deleted file mode 100644 index be98bd9d..00000000 --- a/.github/workflows/github-pages.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# name: Deploy Sandbox to GitHub Pages - -# on: -# push: -# branches: -# - main - -# # Allows you to run this workflow manually from the Actions tab -# workflow_dispatch: - -# # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -# permissions: -# contents: read -# pages: write -# id-token: write - -# # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -# concurrency: -# group: 'pages' -# cancel-in-progress: false - -# jobs: -# # Single deploy job since we're just deploying -# deploy: -# environment: -# name: github-pages -# url: ${{ steps.deployment.outputs.page_url }} -# runs-on: ubuntu-latest -# timeout-minutes: 10 - -# steps: -# - name: Checkout -# uses: actions/checkout@v4 - -# - name: Use Node.js -# uses: actions/setup-node@v4 -# with: -# node-version-file: '.nvmrc' - -# - name: Install dependencies -# run: pnpm install - -# - name: Build Sandbox -# env: -# # Tell the script to use the sandbox directory -# USE_SANDBOX: true -# run: pnpm run build:sandbox - -# - name: Setup Pages -# uses: actions/configure-pages@v3 - -# - name: Upload artifact -# uses: actions/upload-pages-artifact@v2 -# with: -# # Upload sandbox/dist directory -# path: './sandbox/dist' - -# - name: Deploy to GitHub Pages -# id: deployment -# uses: actions/deploy-pages@v2 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 22a598f0..1b11baec 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -4,6 +4,8 @@ on: push: branches: - main + - alpha + - beta jobs: run-tests: @@ -40,10 +42,10 @@ jobs: # Since we lint in the Tests job, we can just build here run: pnpm run build - # - name: Semantic Release - # uses: cycjimmy/semantic-release-action@v3 - # env: - # # Since branch protections are on (pushing commits) you need to use a bot PAT - # GITHUB_TOKEN: ${{ secrets.KONGPONENTS_BOT_PAT }} - # NPM_TOKEN: ${{ secrets.NPM_TOKEN_PUBLIC_PUBLISH }} + - name: Semantic Release + uses: cycjimmy/semantic-release-action@v3 + env: + # Since branch protections are on (pushing commits) you need to use a bot PAT + GITHUB_TOKEN: ${{ secrets.KONGPONENTS_BOT_PAT }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN_PUBLIC_PUBLISH }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 27ee2fc6..851e2eea 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -9,6 +9,8 @@ on: - labeled branches: - main + - alpha + - beta # Allow calling manually from GitHub workflow_dispatch: @@ -52,57 +54,57 @@ jobs: - name: Test run: pnpm run test - # - name: Publish package preview - # id: package-preview - # # Do not run for `alpha` or `beta` branches - # if: github.event_name == 'pull_request' && (github.actor != 'renovate[bot]' || contains(github.event.pull_request.labels.*.name, 'create preview package')) - # env: - # NPM_TOKEN: ${{ secrets.NPM_TOKEN_PUBLIC_PUBLISH }} - # run: | - # git config user.email "konnectx-engineers+kongponents-bot@konghq.com" - # git config user.name "Kong UI Bot" + - name: Publish package preview + id: package-preview + # Do not run for `alpha` or `beta` branches + if: github.event_name == 'pull_request' && (github.actor != 'renovate[bot]' || contains(github.event.pull_request.labels.*.name, 'create preview package')) + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN_PUBLIC_PUBLISH }} + run: | + git config user.email "konnectx-engineers+kongponents-bot@konghq.com" + git config user.name "Kong UI Bot" - # preid="pr.${{ github.event.pull_request.number }}.$(git rev-parse --short ${{ github.event.pull_request.head.sha }})" - # tag="pr-${{ github.event.pull_request.number }}" - # echo "preid=${preid}" + preid="pr.${{ github.event.pull_request.number }}.$(git rev-parse --short ${{ github.event.pull_request.head.sha }})" + tag="pr-${{ github.event.pull_request.number }}" + echo "preid=${preid}" - # echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc - # pnpm --silent lerna version prerelease --preid ${preid} --allow-branch ${{ github.head_ref }} --conventional-prerelease --yes --amend || true + pnpm --silent lerna version prerelease --preid ${preid} --allow-branch ${{ github.head_ref }} --conventional-prerelease --yes --amend || true - # package_version=$(jq -r ".version" package.json) - # package=@kong/markdown@"${package_version}" + package_version=$(jq -r ".version" package.json) + package=@kong/markdown@"${package_version}" - # npm show "${package}" >/dev/null 2>&1 && npm_show_status=0 || npm_show_status=1 - # if [ $npm_show_status -eq 0 ]; then - # echo "Package ${package} is already published. Skipping publishing." - # exit 0 - # fi + npm show "${package}" >/dev/null 2>&1 && npm_show_status=0 || npm_show_status=1 + if [ $npm_show_status -eq 0 ]; then + echo "Package ${package} is already published. Skipping publishing." + exit 0 + fi - # npm_instructions="" + npm_instructions="" - # pkg=$(npm publish --no-git-checks --access public --report-summary --tag "${tag}" | grep "+ "| sed 's/+ //') + pkg=$(npm publish --no-git-checks --access public --report-summary --tag "${tag}" | grep "+ "| sed 's/+ //') - # if [[ -z "${pkg}" ]]; then - # echo "Error publishing package" - # exit -1 - # fi + if [[ -z "${pkg}" ]]; then + echo "Error publishing package" + exit -1 + fi - # npm_instructions="pnpm add @$(echo ${pkg}|cut -d'@' -f2)@${tag}" + npm_instructions="pnpm add @$(echo ${pkg}|cut -d'@' -f2)@${tag}" - # echo "npm_instructions<> $GITHUB_OUTPUT - # echo -e "$npm_instructions" >> $GITHUB_OUTPUT - # echo "EOF" >> $GITHUB_OUTPUT + echo "npm_instructions<> $GITHUB_OUTPUT + echo -e "$npm_instructions" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT - # - name: Provide preview link info - # if: ${{ steps.package-preview.outputs.npm_instructions != '' }} - # uses: marocchino/sticky-pull-request-comment@v2 - # with: - # header: pr_preview_consumption - # message: | - # ### Install the preview package from this PR + - name: Provide preview link info + if: ${{ steps.package-preview.outputs.npm_instructions != '' }} + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: pr_preview_consumption + message: | + ### Install the preview package from this PR - # ```sh - # ${{ steps.package-preview.outputs.npm_instructions }} - # ``` - # GITHUB_TOKEN: ${{ secrets.KONGPONENTS_BOT_PAT }} + ```sh + ${{ steps.package-preview.outputs.npm_instructions }} + ``` + GITHUB_TOKEN: ${{ secrets.KONGPONENTS_BOT_PAT }} diff --git a/package.json b/package.json index 8a13d814..23d27db0 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "vue": "^3.3.8" }, "dependencies": { + "@kong/icons": "^1.8.7", "@sindresorhus/slugify": "^2.2.1", "@vueuse/core": "^10.7.0", "html-format": "^1.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc776888..21d8d5e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@kong/icons': + specifier: ^1.8.7 + version: 1.8.7(vue@3.3.11) '@sindresorhus/slugify': specifier: ^2.2.1 version: 2.2.1 @@ -820,6 +823,15 @@ packages: resolution: {integrity: sha512-I6C2HHJ+Kx14Exmr9OEe4yzHvhneC5RGrWzViZXcWNOYCl2VN9dMmgjTAaowbdeyLEeZ+4AtC+LJ2JU7eN4o8w==} dev: true + /@kong/icons@1.8.7(vue@3.3.11): + resolution: {integrity: sha512-gSs8VRUyPVacVg/4/bTjnRSuIzcXrWFvV7+4vxjUJXt0dMWBq8GzMZ4JTj9ar7eUHLWeY1nmP55PmwF2Qwsa5A==} + engines: {node: '>=18.17.0'} + peerDependencies: + vue: ^3.3.11 + dependencies: + vue: 3.3.11(typescript@5.3.3) + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} diff --git a/src/components/MarkdownToolbar.vue b/src/components/MarkdownToolbar.vue deleted file mode 100644 index 705b4b71..00000000 --- a/src/components/MarkdownToolbar.vue +++ /dev/null @@ -1,245 +0,0 @@ - - - - - diff --git a/src/components/MarkdownUi.vue b/src/components/MarkdownUi.vue index e1df9ca2..a3bdd05c 100644 --- a/src/components/MarkdownUi.vue +++ b/src/components/MarkdownUi.vue @@ -57,13 +57,13 @@ + + diff --git a/src/components/toolbar/MarkdownToolbar.vue b/src/components/toolbar/MarkdownToolbar.vue new file mode 100644 index 00000000..856cc429 --- /dev/null +++ b/src/components/toolbar/MarkdownToolbar.vue @@ -0,0 +1,313 @@ + + + + + diff --git a/src/components/toolbar/TooltipShortcut.vue b/src/components/toolbar/TooltipShortcut.vue new file mode 100644 index 00000000..683ea2de --- /dev/null +++ b/src/components/toolbar/TooltipShortcut.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts index f37b2062..8e1e475b 100644 --- a/src/composables/useKeyboardShortcuts.ts +++ b/src/composables/useKeyboardShortcuts.ts @@ -1,8 +1,8 @@ import { computed } from 'vue' import type { Ref } from 'vue' -import useMarkdownActions from './useMarkdownActions' +import useMarkdownActions from '@/composables/useMarkdownActions' import { useActiveElement, useMagicKeys } from '@vueuse/core' -import type { InlineFormat } from '../types' +import type { InlineFormat } from '@/types' /** * Utilize keyboard shortcuts in the markdown editor. Must be called at the root of the `setup` function. diff --git a/src/composables/useMarkdownActions.ts b/src/composables/useMarkdownActions.ts index 79a06ae8..76a676ca 100644 --- a/src/composables/useMarkdownActions.ts +++ b/src/composables/useMarkdownActions.ts @@ -1,7 +1,7 @@ import { reactive, nextTick } from 'vue' import type { Ref } from 'vue' -import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE, MARKDOWN_TEMPLATE_CODEBLOCK, MARKDOWN_TEMPLATE_TASK, MARKDOWN_TEMPLATE_UL, MARKDOWN_TEMPLATE_BLOCKQUOTE, MARKDOWN_TEMPLATE_TABLE } from '../constants' -import type { InlineFormat, MarkdownTemplate } from '../types' +import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE, MARKDOWN_TEMPLATE_CODEBLOCK, MARKDOWN_TEMPLATE_TASK, MARKDOWN_TEMPLATE_UL, MARKDOWN_TEMPLATE_BLOCKQUOTE, MARKDOWN_TEMPLATE_TABLE } from '@/constants' +import type { InlineFormat, MarkdownTemplate } from '@/types' /** * Utilize the markdown editor actions. @@ -55,6 +55,21 @@ export default function useMarkdownActions( } } + /** Focus on the textarea and wait a virtual DOM cycle */ + const focusTextarea = async (): Promise => { + const textarea = getTextarea() + + // If no element, exit early + if (!textarea) { + return + } + + // Always focus back on the textarea + textarea.focus() + // Wait for the DOM to cycle + await nextTick() + } + /** * Toggle inline formatting within the textarea. Can be used with and without an active selection. * @param {InlineFormat} format The inline format being added/removed, e.g. 'bold' @@ -116,9 +131,7 @@ export default function useMarkdownActions( rawMarkdown.value = startText.substring(0, startText.length - wrapperLength) + endText.substring(wrapperLength) // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() textarea.selectionEnd = selectedText.start - wrapperLength @@ -128,9 +141,7 @@ export default function useMarkdownActions( rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start) + wrapper + wrapper + rawMarkdown.value.substring(selectedText.end) // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() // Move the cursor between the `wrapper` textarea.selectionEnd = selectedText.start + wrapperLength @@ -169,9 +180,7 @@ export default function useMarkdownActions( } // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() // Adjust the selection position if (!isWrapped) { @@ -238,9 +247,7 @@ export default function useMarkdownActions( } // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() // Move the cursor and selected text if (selectedText.text.length !== 0) { @@ -291,7 +298,10 @@ export default function useMarkdownActions( switch (template) { case 'task': // Do nothing if the template already exists - if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_TASK)) { return } + if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_TASK)) { + await focusTextarea() + return + } // needsNewLine not needed here markdownTemplate = needsNewLine + @@ -299,7 +309,10 @@ export default function useMarkdownActions( break case 'unordered-list': // Do nothing if the template already exists - if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_UL)) { return } + if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_UL)) { + await focusTextarea() + return + } // needsNewLine not needed here markdownTemplate = needsNewLine + @@ -307,7 +320,10 @@ export default function useMarkdownActions( break case 'blockquote': // Do nothing if the template already exists - if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_BLOCKQUOTE)) { return } + if (singleLineTemplateExists(startText, MARKDOWN_TEMPLATE_BLOCKQUOTE)) { + await focusTextarea() + return + } // needsNewLine not needed here markdownTemplate = needsNewLine + @@ -333,9 +349,7 @@ export default function useMarkdownActions( rawMarkdown.value = startText + markdownTemplate + rawMarkdown.value.substring(selectedText.end) // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() switch (template) { case 'codeblock': @@ -410,9 +424,7 @@ export default function useMarkdownActions( rawMarkdown.value = removeNewLineTemplate ? rawMarkdown.value.substring(0, selectedText.start - lastLine.length) + rawMarkdown.value.substring(selectedText.end) : startText + newLineContent + rawMarkdown.value.substring(selectedText.end) // Always focus back on the textarea - textarea.focus() - // Wait for the DOM to cycle - await nextTick() + await focusTextarea() // Update the cursor position textarea.selectionEnd = removeNewLineTemplate ? selectedText.start - templateLength : selectedText.start + newLineContent.length diff --git a/src/composables/useMarkdownIt.ts b/src/composables/useMarkdownIt.ts index 7f3f5aba..c537d950 100644 --- a/src/composables/useMarkdownIt.ts +++ b/src/composables/useMarkdownIt.ts @@ -1,5 +1,5 @@ import { ref } from 'vue' -import useShikiji from './useShikiji' +import useShikiji from '@/composables/useShikiji' // markdown-it import MarkdownIt from 'markdown-it' diff --git a/src/index.ts b/src/index.ts index 8d92dd52..52876641 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import MarkdownUi from './components/MarkdownUi.vue' +import MarkdownUi from '@/components/MarkdownUi.vue' export { MarkdownUi } diff --git a/src/injection-keys.ts b/src/injection-keys.ts index 8739b423..c6829ecc 100644 --- a/src/injection-keys.ts +++ b/src/injection-keys.ts @@ -1,4 +1,6 @@ // NOTE: Do not export these injection keys from the package -export const TEXTAREA_ID = Symbol('Textarea unique id') +export const TEXTAREA_ID_INJECTION_KEY = Symbol('Textarea unique id') export const MODE_INJECTION_KEY = Symbol('Markdown component mode') export const EDITABLE_INJECTION_KEY = Symbol('Is the markdown component editable') +export const FULLSCREEN_INJECTION_KEY = Symbol('Is the markdown component displaying fullscreen') +export const HTML_PREVIEW_INJECTION_KEY = Symbol('Is the html preview enabled') diff --git a/src/types/index.ts b/src/types/index.ts index 4e462224..97fdbc20 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ // Export all types and interfaces from this index.ts // The actual types and interfaces should be contained in separate files within this folder. -export * from './markdown-ui' +export * from '@/types/markdown-ui' diff --git a/src/types/markdown-ui.ts b/src/types/markdown-ui.ts index 1a146e16..3f622145 100644 --- a/src/types/markdown-ui.ts +++ b/src/types/markdown-ui.ts @@ -1,3 +1,6 @@ +// Get a generic `@kong/icons` interface for the option prop +import type { BoldIcon as GenericIcon } from '@kong/icons' + /** The current mode of the markdown component */ export type MarkdownMode = | 'read' @@ -19,6 +22,8 @@ export type InlineFormat = export interface FormatOption { label: string action: InlineFormat + icon: typeof GenericIcon + keys?: string[] } /** The type of markdown template to insert */ @@ -32,6 +37,7 @@ export type MarkdownTemplate = export interface TemplateOption { label: string action: MarkdownTemplate + icon: typeof GenericIcon } export interface TextAreaInputEvent {