commit c7bac1a7a0a04628215575035f966baec953020b Author: jingrow Date: Fri Oct 24 00:40:30 2025 +0800 initial commit diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..e523514 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# Ran prettier on source +# after adding tailwind plugin +54563d9deba9d95c1212c1be2217ce8fa181162b +8e07a190aff0eba2a3e072f9857a654dca6fa0e1 \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9890018 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,18 @@ +name: Publish on NPM +on: + push: + branches: [ main ] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-node@v1 + with: + node-version: 18 + - run: npm install + - run: npm test + - uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/story-publish.yml b/.github/workflows/story-publish.yml new file mode 100644 index 0000000..5ef5884 --- /dev/null +++ b/.github/workflows/story-publish.yml @@ -0,0 +1,38 @@ +name: Build and Deploy Storybook + +on: + push: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies + run: yarn install + + - name: Build package + run: yarn run build + + - name: Build Histoire + run: yarn run story:build + + - name: Create CNAME file + run: echo 'ui.jingrow.com' > ./.histoire/dist/CNAME + + - name: Deploy Histoire to + + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.JINGROW_UI_PAGES_TOKEN }} + publish_dir: ./.histoire/dist + publish_branch: gh-pages diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml new file mode 100644 index 0000000..b90ebc8 --- /dev/null +++ b/.github/workflows/vitest.yml @@ -0,0 +1,25 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run prettier check and tests + run: yarn test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8735994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.DS_Store +node_modules + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.postcssrc.js b/.postcssrc.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/.postcssrc.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d8776e2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +exclude: 'node_modules|.git' +default_stages: [commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + types_or: + - javascript + - vue + additional_dependencies: + - prettier + - prettier-plugin-tailwindcss + exclude: | + (?x)^( + .*node_modules.*| + .*boilerplate.*| + .*src.*.js| + )$ + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..50f7605 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "proseWrap": "always" +} \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..2f9e807 --- /dev/null +++ b/404.html @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/App.vue b/App.vue new file mode 100644 index 0000000..c663fce --- /dev/null +++ b/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/auto-imports.d.ts b/auto-imports.d.ts new file mode 100644 index 0000000..9d24007 --- /dev/null +++ b/auto-imports.d.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +// biome-ignore lint: disable +export {} +declare global { + +} diff --git a/components.d.ts b/components.d.ts new file mode 100644 index 0000000..271cbb8 --- /dev/null +++ b/components.d.ts @@ -0,0 +1,192 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + Alert: typeof import('./src/components/Alert/Alert.vue')['default'] + AlignCenter: typeof import('./src/components/TextEditor/icons/align-center.vue')['default'] + AlignJustify: typeof import('./src/components/TextEditor/icons/align-justify.vue')['default'] + AlignLeft: typeof import('./src/components/TextEditor/icons/align-left.vue')['default'] + AlignRight: typeof import('./src/components/TextEditor/icons/align-right.vue')['default'] + ArrowGoBackLine: typeof import('./src/components/TextEditor/icons/arrow-go-back-line.vue')['default'] + ArrowGoForwardLine: typeof import('./src/components/TextEditor/icons/arrow-go-forward-line.vue')['default'] + Autocomplete: typeof import('./src/components/Autocomplete/Autocomplete.vue')['default'] + 'Autocomplete.story': typeof import('./src/components/Autocomplete/Autocomplete.story.vue')['default'] + Avatar: typeof import('./src/components/Avatar/Avatar.vue')['default'] + 'Avatar.story': typeof import('./src/components/Avatar/Avatar.story.vue')['default'] + AxisChart: typeof import('./src/components/Charts/AxisChart.vue')['default'] + Badge: typeof import('./src/components/Badge/Badge.vue')['default'] + 'Badge.story': typeof import('./src/components/Badge/Badge.story.vue')['default'] + Bold: typeof import('./src/components/TextEditor/icons/bold.vue')['default'] + Breadcrumbs: typeof import('./src/components/Breadcrumbs/Breadcrumbs.vue')['default'] + 'Breadcrumbs.story': typeof import('./src/components/Breadcrumbs/Breadcrumbs.story.vue')['default'] + Button: typeof import('./src/components/Button/Button.vue')['default'] + 'Button.story': typeof import('./src/components/Button/Button.story.vue')['default'] + Calendar: typeof import('./src/components/Calendar/Calendar.vue')['default'] + 'Calendar.story': typeof import('./src/components/Calendar/Calendar.story.vue')['default'] + CalendarDaily: typeof import('./src/components/Calendar/CalendarDaily.vue')['default'] + CalendarEvent: typeof import('./src/components/Calendar/CalendarEvent.vue')['default'] + CalendarMonthly: typeof import('./src/components/Calendar/CalendarMonthly.vue')['default'] + CalendarTimeMarker: typeof import('./src/components/Calendar/CalendarTimeMarker.vue')['default'] + CalendarWeekly: typeof import('./src/components/Calendar/CalendarWeekly.vue')['default'] + Card: typeof import('./src/components/Card.vue')['default'] + 'Charts.story': typeof import('./src/components/Charts/Charts.story.vue')['default'] + Checkbox: typeof import('./src/components/Checkbox/Checkbox.vue')['default'] + 'Checkbox.story': typeof import('./src/components/Checkbox/Checkbox.story.vue')['default'] + CircularProgressBar: typeof import('./src/components/CircularProgressBar/CircularProgressBar.vue')['default'] + 'CircularProgressBar.story': typeof import('./src/components/CircularProgressBar/CircularProgressBar.story.vue')['default'] + CodeBlockComponent: typeof import('./src/components/TextEditor/CodeBlockComponent.vue')['default'] + CodeView: typeof import('./src/components/TextEditor/icons/code-view.vue')['default'] + Combobox: typeof import('./src/components/Combobox/Combobox.vue')['default'] + 'Combobox.story': typeof import('./src/components/Combobox/Combobox.story.vue')['default'] + CommandPalette: typeof import('./src/components/CommandPalette/CommandPalette.vue')['default'] + CommandPaletteItem: typeof import('./src/components/CommandPalette/CommandPaletteItem.vue')['default'] + ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default'] + DateMonthYearPicker: typeof import('./src/components/Calendar/DateMonthYearPicker.vue')['default'] + DatePicker: typeof import('./src/components/DatePicker/DatePicker.vue')['default'] + 'DatePicker.story': typeof import('./src/components/DatePicker/DatePicker.story.vue')['default'] + DateRangePicker: typeof import('./src/components/DatePicker/DateRangePicker.vue')['default'] + DateTimePicker: typeof import('./src/components/DatePicker/DateTimePicker.vue')['default'] + DayIcon: typeof import('./src/components/Calendar/Icon/DayIcon.vue')['default'] + Dialog: typeof import('./src/components/Dialog/Dialog.vue')['default'] + 'Dialog.story': typeof import('./src/components/Dialog/Dialog.story.vue')['default'] + Dialogs: typeof import('./src/components/Dialogs.vue')['default'] + Divider: typeof import('./src/components/Divider/Divider.vue')['default'] + DonutChart: typeof import('./src/components/Charts/DonutChart.vue')['default'] + DoubleQuotesR: typeof import('./src/components/TextEditor/icons/double-quotes-r.vue')['default'] + Dropdown: typeof import('./src/components/Dropdown/Dropdown.vue')['default'] + 'Dropdown.story': typeof import('./src/components/Dropdown/Dropdown.story.vue')['default'] + ECharts: typeof import('./src/components/Charts/ECharts.vue')['default'] + EditLink: typeof import('./src/components/TextEditor/EditLink.vue')['default'] + EmojiList: typeof import('./src/components/TextEditor/extensions/emoji/EmojiList.vue')['default'] + ErrorMessage: typeof import('./src/components/ErrorMessage/ErrorMessage.vue')['default'] + 'ErrorMessage.story': typeof import('./src/components/ErrorMessage/ErrorMessage.story.vue')['default'] + EventModalContent: typeof import('./src/components/Calendar/EventModalContent.vue')['default'] + FeatherIcon: typeof import('./src/components/FeatherIcon.vue')['default'] + FileUploader: typeof import('./src/components/FileUploader/FileUploader.vue')['default'] + 'FileUploader.story': typeof import('./src/components/FileUploader/FileUploader.story.vue')['default'] + FilterIcon: typeof import('./src/components/ListFilter/FilterIcon.vue')['default'] + FloatingPopover: typeof import('./src/components/Calendar/FloatingPopover.vue')['default'] + FontColor: typeof import('./src/components/TextEditor/FontColor.vue')['default'] + FormatClear: typeof import('./src/components/TextEditor/icons/format-clear.vue')['default'] + FormControl: typeof import('./src/components/FormControl/FormControl.vue')['default'] + 'FormControl.story': typeof import('./src/components/FormControl/FormControl.story.vue')['default'] + FormLabel: typeof import('./src/components/FormLabel.vue')['default'] + JingrowUIProvider: typeof import('./src/components/Provider/JingrowUIProvider.vue')['default'] + FunnelChart: typeof import('./src/components/Charts/FunnelChart.vue')['default'] + GreenCheckIcon: typeof import('./src/components/GreenCheckIcon.vue')['default'] + H1: typeof import('./src/components/TextEditor/icons/h-1.vue')['default'] + H2: typeof import('./src/components/TextEditor/icons/h-2.vue')['default'] + H3: typeof import('./src/components/TextEditor/icons/h-3.vue')['default'] + H4: typeof import('./src/components/TextEditor/icons/h-4.vue')['default'] + H5: typeof import('./src/components/TextEditor/icons/h-5.vue')['default'] + H6: typeof import('./src/components/TextEditor/icons/h-6.vue')['default'] + IframeNodeView: typeof import('./src/components/TextEditor/extensions/iframe/IframeNodeView.vue')['default'] + ImageAddLine: typeof import('./src/components/TextEditor/icons/image-add-line.vue')['default'] + ImageGroupNodeView: typeof import('./src/components/TextEditor/extensions/image-group/ImageGroupNodeView.vue')['default'] + ImageGroupUploadDialog: typeof import('./src/components/TextEditor/extensions/image-group/ImageGroupUploadDialog.vue')['default'] + ImageNodeView: typeof import('./src/components/TextEditor/extensions/image/ImageNodeView.vue')['default'] + ImageViewerModal: typeof import('./src/components/TextEditor/ImageViewerModal.vue')['default'] + Input: typeof import('./src/components/Input.vue')['default'] + InsertIframe: typeof import('./src/components/TextEditor/extensions/iframe/InsertIframe.vue')['default'] + InsertImage: typeof import('./src/components/TextEditor/InsertImage.vue')['default'] + InsertLink: typeof import('./src/components/TextEditor/InsertLink.vue')['default'] + InsertVideo: typeof import('./src/components/TextEditor/InsertVideo.vue')['default'] + Italic: typeof import('./src/components/TextEditor/icons/italic.vue')['default'] + KeyboardShortcut: typeof import('./src/components/KeyboardShortcut.vue')['default'] + Layout: typeof import('./src/components/VueGridLayout/Layout.vue')['default'] + Link: typeof import('./src/components/Link.vue')['default'] + ListEmptyState: typeof import('./src/components/ListView/ListEmptyState.vue')['default'] + ListFilter: typeof import('./src/components/ListFilter/ListFilter.vue')['default'] + ListFooter: typeof import('./src/components/ListView/ListFooter.vue')['default'] + ListGroupHeader: typeof import('./src/components/ListView/ListGroupHeader.vue')['default'] + ListGroupRows: typeof import('./src/components/ListView/ListGroupRows.vue')['default'] + ListGroups: typeof import('./src/components/ListView/ListGroups.vue')['default'] + ListHeader: typeof import('./src/components/ListView/ListHeader.vue')['default'] + ListHeaderItem: typeof import('./src/components/ListView/ListHeaderItem.vue')['default'] + ListItem: typeof import('./src/components/ListItem.vue')['default'] + ListOrdered: typeof import('./src/components/TextEditor/icons/list-ordered.vue')['default'] + ListRow: typeof import('./src/components/ListView/ListRow.vue')['default'] + ListRowItem: typeof import('./src/components/ListView/ListRowItem.vue')['default'] + ListRows: typeof import('./src/components/ListView/ListRows.vue')['default'] + ListSelectBanner: typeof import('./src/components/ListView/ListSelectBanner.vue')['default'] + ListTask: typeof import('./src/components/TextEditor/icons/list-task.vue')['default'] + ListUnordered: typeof import('./src/components/TextEditor/icons/list-unordered.vue')['default'] + ListView: typeof import('./src/components/ListView/ListView.vue')['default'] + 'ListView.story': typeof import('./src/components/ListView/ListView.story.vue')['default'] + LoadingIndicator: typeof import('./src/components/LoadingIndicator.vue')['default'] + LoadingText: typeof import('./src/components/LoadingText.vue')['default'] + LucideCalendar: typeof import('~icons/lucide/calendar')['default'] + LucideCheck: typeof import('~icons/lucide/check')['default'] + LucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] + LucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] + LucideX: typeof import('~icons/lucide/x')['default'] + MentionList: typeof import('./src/components/TextEditor/MentionList.vue')['default'] + Menu: typeof import('./src/components/TextEditor/Menu.vue')['default'] + MonthIcon: typeof import('./src/components/Calendar/Icon/MonthIcon.vue')['default'] + NestedPopover: typeof import('./src/components/ListFilter/NestedPopover.vue')['default'] + NewEventModal: typeof import('./src/components/Calendar/NewEventModal.vue')['default'] + NumberChart: typeof import('./src/components/Charts/NumberChart.vue')['default'] + Password: typeof import('./src/components/Password/Password.vue')['default'] + 'Password.story': typeof import('./src/components/Password/Password.story.vue')['default'] + Popover: typeof import('./src/components/Popover/Popover.vue')['default'] + 'Popover.story': typeof import('./src/components/Popover/Popover.story.vue')['default'] + Progress: typeof import('./src/components/Progress/Progress.vue')['default'] + 'Progress.story': typeof import('./src/components/Progress/Progress.story.vue')['default'] + Rating: typeof import('./src/components/Rating/Rating.vue')['default'] + 'Rating.story': typeof import('./src/components/Rating/Rating.story.vue')['default'] + Resource: typeof import('./src/components/Resource.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SearchComplete: typeof import('./src/components/ListFilter/SearchComplete.vue')['default'] + Select: typeof import('./src/components/Select/Select.vue')['default'] + 'Select.story': typeof import('./src/components/Select/Select.story.vue')['default'] + Separator: typeof import('./src/components/TextEditor/icons/separator.vue')['default'] + ShowMoreCalendarEvent: typeof import('./src/components/Calendar/ShowMoreCalendarEvent.vue')['default'] + Sidebar: typeof import('./src/components/Sidebar/Sidebar.vue')['default'] + 'Sidebar.story': typeof import('./src/components/Sidebar/Sidebar.story.vue')['default'] + SidebarHeader: typeof import('./src/components/Sidebar/SidebarHeader.vue')['default'] + SidebarItem: typeof import('./src/components/Sidebar/SidebarItem.vue')['default'] + SidebarSection: typeof import('./src/components/Sidebar/SidebarSection.vue')['default'] + SlashCommandsList: typeof import('./src/components/TextEditor/extensions/slash-commands/SlashCommandsList.vue')['default'] + Spinner: typeof import('./src/components/Spinner/Spinner.vue')['default'] + 'Spinner.story': typeof import('./src/components/Spinner/Spinner.story.vue')['default'] + Strikethrough: typeof import('./src/components/TextEditor/icons/strikethrough.vue')['default'] + SuggestionList: typeof import('./src/components/TextEditor/extensions/suggestion/SuggestionList.vue')['default'] + Switch: typeof import('./src/components/Switch/Switch.vue')['default'] + 'Switch.story': typeof import('./src/components/Switch/Switch.story.vue')['default'] + TabButtons: typeof import('./src/components/TabButtons/TabButtons.vue')['default'] + 'TabButtons.story': typeof import('./src/components/TabButtons/TabButtons.story.vue')['default'] + Table2: typeof import('./src/components/TextEditor/icons/table-2.vue')['default'] + TabList: typeof import('./src/components/Tabs/TabList.vue')['default'] + TabPanel: typeof import('./src/components/Tabs/TabPanel.vue')['default'] + Tabs: typeof import('./src/components/Tabs/Tabs.vue')['default'] + 'Tabs.story': typeof import('./src/components/Tabs/Tabs.story.vue')['default'] + Text: typeof import('./src/components/TextEditor/icons/text.vue')['default'] + Textarea: typeof import('./src/components/Textarea/Textarea.vue')['default'] + 'Textarea.story': typeof import('./src/components/Textarea/Textarea.story.vue')['default'] + TextEditor: typeof import('./src/components/TextEditor/TextEditor.vue')['default'] + 'TextEditor.story': typeof import('./src/components/TextEditor/TextEditor.story.vue')['default'] + TextEditorBubbleMenu: typeof import('./src/components/TextEditor/TextEditorBubbleMenu.vue')['default'] + TextEditorFixedMenu: typeof import('./src/components/TextEditor/TextEditorFixedMenu.vue')['default'] + TextEditorFloatingMenu: typeof import('./src/components/TextEditor/TextEditorFloatingMenu.vue')['default'] + TextInput: typeof import('./src/components/TextInput/TextInput.vue')['default'] + 'TextInput.story': typeof import('./src/components/TextInput/TextInput.story.vue')['default'] + TimePicker: typeof import('./src/components/TimePicker/TimePicker.vue')['default'] + 'TimePicker.story': typeof import('./src/components/TimePicker/TimePicker.story.vue')['default'] + Toast: typeof import('./src/components/Toast/Toast.vue')['default'] + ToastProvider: typeof import('./src/components/Toast/ToastProvider.vue')['default'] + Tooltip: typeof import('./src/components/Tooltip/Tooltip.vue')['default'] + 'Tooltip.story': typeof import('./src/components/Tooltip/Tooltip.story.vue')['default'] + Tree: typeof import('./src/components/Tree/Tree.vue')['default'] + 'Tree.story': typeof import('./src/components/Tree/Tree.story.vue')['default'] + Underline: typeof import('./src/components/TextEditor/icons/underline.vue')['default'] + VideoAddLine: typeof import('./src/components/TextEditor/icons/video-add-line.vue')['default'] + WeekIcon: typeof import('./src/components/Calendar/Icon/WeekIcon.vue')['default'] + } +} diff --git a/docs/Getting Started.story.md b/docs/Getting Started.story.md new file mode 100644 index 0000000..bc7f302 --- /dev/null +++ b/docs/Getting Started.story.md @@ -0,0 +1,57 @@ +# Getting Started + +This page will help you with setting up `jingrow-ui` in a new project as well as +an existing Jingrow project. + +## Quick start + +You can quickly setup `jingrow-ui` using +[`jingrow-ui-starter`](https://github.com/netchampfaris/jingrow-ui-starter). If +you already have a Jingrow app for which you want to build a frontend you can +start with **Step 2**. + +### 1. Create your Jingrow app + +```sh +bench new-app todo +``` + +### 2. Setup jingrow-ui + +```sh +cd apps/todo +# this will setup a vue project with jingrow-ui set up +# inside the frontend directory +npx degit netchampfaris/jingrow-ui-starter frontend +``` + +Refer [jingrow-ui-starter](https://github.com/netchampfaris/jingrow-ui-starter) +for more details. + +### 3. ignore_csrf config + +```sh +bench --site todo.test set-config ignore_csrf 1 +``` + +This will prevent CSRFToken errors while using the vite dev server. In +production environment, the csrf_token is attached to the window object in +index.html for you. + +### 4. Start dev server + +```sh +cd frontend +yarn +yarn dev +``` + +The Vite dev server will start on the port `8080`. This can be changed from +`vite.config.js`. The development server is configured to proxy your jingrow app +(usually running on port 8000). If you have a site named `todo.test`, open +`http://todo.test:8080` in your browser. If you see a button named "Click to +send 'ping' request", congratulations! + +If you notice the browser URL is `/frontend`, this is the base URL where your +frontend app will run in production. To change this, open `src/router.js` and +change the base URL passed to `createWebHistory`. diff --git a/docs/Introduction.story.md b/docs/Introduction.story.md new file mode 100644 index 0000000..c7c13c6 --- /dev/null +++ b/docs/Introduction.story.md @@ -0,0 +1,81 @@ +# What is Jingrow UI? + +Jingrow UI is a set of components and utilities to build frontend apps based on +the [Jingrow Framework](https://framework.jingrow.com). + +Along with generic components which are required to build a frontend like +Button, Link, Dialog, etc., jingrow-ui also contains utilities for handling +server-side data fetching, directives and utilities. + +**Usage example** + +```vue + + + +``` + +## Dependencies + +Jingrow UI is built on top of the following amazing projects – + +- [Vue 3](https://vuejs.org) +- [TailwindCSS](https://tailwindcss.com) +- [Headless UI](https://headlessui.com) +- [PopperJS](https://popper.js.org/) +- [TipTap](https://tiptap.dev) +- [Feather Icons](https://feathericons.com) + +See full list of dependencies: +[package.json](https://github.com/jingrow/jingrow-ui/blob/main/package.json) + +## Motivation + +In 2019, I started building [Jingrow Books](https://jingrowbooks.com) based on an +experimental design system by [Timeless](https://timeless.co). As the product +got built, a set of small reusable components (like Button, Dialog, Card, etc.) +were also built. + +After the launch of Jingrow Books (and me dropping the project) I moved on to +building the UI for [Jingrow Cloud](https://jcloud.jingrow.com) in 2020. It also +needed these components, so I copy-pasted them from Jingrow Books to Jingrow +Cloud. These components evolved over time in Jingrow Cloud. After working on the +Jingrow Cloud UI for about a year and a half, I moved on to my next project. + +At the start of 2022, I started working on +[Gameplan](https://github.com/jingrow/gameplan). I didn't want to copy-paste yet +again, so I extracted these components in a separate package called +[`jingrow-ui`](https://npm.im/jingrow-ui). This package is being developed in +parallel along with the Gameplan project. I keep adding generic components and +utilities useful for frontend development. + +## Products + +Jingrow UI is now being used in a lot of products by Jingrow. + +- [Jingrow Cloud](https://jcloud.jingrow.com) +- [Gameplan](https://github.com/jingrow/gameplan) +- [Jingrow Insights](https://github.com/jingrow/insights) +- [Jingrow Drive](https://github.com/jingrow/drive) + +## License + +Jingrow UI is MIT licensed diff --git a/docs/assets/dialog-slots.png b/docs/assets/dialog-slots.png new file mode 100644 index 0000000..3c15fd4 Binary files /dev/null and b/docs/assets/dialog-slots.png differ diff --git a/docs/components/alert.md b/docs/components/alert.md new file mode 100644 index 0000000..ca74f72 --- /dev/null +++ b/docs/components/alert.md @@ -0,0 +1,45 @@ + + +# Alert + +## Usage + + + + + This is a test alert message + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | +| :------ | :------ | :------- | +| `title` | `null` | `String` | + +## Slots + +| Name | Description | +| :-------- | :--------------------------------- | +| `default` | Default slot to render the message | diff --git a/docs/components/autocomplete.md b/docs/components/autocomplete.md new file mode 100644 index 0000000..6030205 --- /dev/null +++ b/docs/components/autocomplete.md @@ -0,0 +1,64 @@ + + +# Autocomplete + +The Autocomplete component is used to select an option from a list of options. +Additionally, it provides a search input to filter the options. + +## Usage + + +
+ +
Selected Value:
+
{{ JSON.stringify(fruit) }}
+
+
+ +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------ | :------ | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `modelValue` | `null` | `Object` | No need to directly pass this prop. Just use `v-model`. | +| `value` | `null` | `Object` | This prop should be used if you are not using `v-model`. Value must be one of the options object from `options` or `null`. This object must be a direct reference and not a copy. | +| `options` | `null` | `Array` | Array of objects with `label` and `value` keys | +| `placeholder` | `null` | `String` | String to show as placeholder when no value is set | + +## Events + +| Name | Description | +| :------------------ | :----------------------------------------------------------------------------- | +| `update:modelValue` | This event is emitted when value is changed or unset and `v-model` is used. | +| `change` | This event is emitted when value is changed or unset and `value` prop is used. | diff --git a/docs/components/avatar.md b/docs/components/avatar.md new file mode 100644 index 0000000..d568f85 --- /dev/null +++ b/docs/components/avatar.md @@ -0,0 +1,55 @@ + + +# Avatar + +The Avatar component is usually used to show display picture of a user or a +fallback with their initials. + +## Usage + + + + + + + + + + + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :--------- | :------- | :----------------- | :--------------------------------------------------------------------------------- | +| `imageURL` | `null` | `String` | URL to an image | +| `label` | `null` | `String` | First character of the label will be used as a placeholder when imageURL is `null` | +| `size` | `'md'` | `sm \| md \| lg` | Size of the element | +| `shape` | `circle` | `circle \| square` | Shape of the element | diff --git a/docs/components/badge.md b/docs/components/badge.md new file mode 100644 index 0000000..06231a6 --- /dev/null +++ b/docs/components/badge.md @@ -0,0 +1,56 @@ + + +# Badge + +The Badge component is used to show a status badge for an entity. + +## Usage + + + Draft + Pending + Completed + Error + In Progress + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :--------- | :------- | :--------------------------------------- | :--------------------------------------------------- | +| `color` | `'gray'` | `gray \| yellow \| green \| red \| blue` | | +| `colorMap` | `null` | `Object` | An object containing label as key and color as value | +| `label` | `null` | `String` | This must be passed when using `colorMap` | diff --git a/docs/components/button.md b/docs/components/button.md new file mode 100644 index 0000000..da99451 --- /dev/null +++ b/docs/components/button.md @@ -0,0 +1,60 @@ + + +# Button + +The Button component is used to trigger an action such as submitting a form, +opening a Dialog, or canceling an action. + +## Usage + + + + + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------ | :------------ | :------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | +| `label` | `null` | `String` | | +| `appearance` | `'secondary'` | `primary \| secondary \| danger \| success \| warning \| white \| minimal` | | +| `disabled` | `false` | `true \| false` | | +| `active` | `false` | `true \| false` | Only applicable if `appearance` is `minimal` | +| `icon` | `null` | [Feather Icon](/components/feathericon) name | Will only display icon without label. If `label` is provided, `aria-label` will be set to that value. | +| `iconLeft` | `null` | [Feather Icon](/components/feathericon) name | | +| `iconRight` | `null` | [Feather Icon](/components/feathericon) name | | +| `loading` | `false` | `true \| false` | Will show a loading spinner to the left of the button text | +| `loadingText` | `null` | `String` | Set this to change the button text in `loading` state | +| `route` | `null` | `String \| Object` | If you are using `vue-router`, you can pass a valid `route` value and click handler will be added | +| `link` | `null` | `String` | URL to open in a new window on button click | + +## Events + +All attributes and event listeners are passed down to the underlying `button` +element, so `@click` and other events will just work like on a normal button. diff --git a/docs/components/confirm-dialog.md b/docs/components/confirm-dialog.md new file mode 100644 index 0000000..825a77b --- /dev/null +++ b/docs/components/confirm-dialog.md @@ -0,0 +1,64 @@ +# Confirm Dialog + +This component is to confirm an action with the user. + +## Usage + +Call the `confirmDialog` function with options to show a confirmation dialog. +You need to make sure you include the `Dialogs` component in your root component +([`App.vue`](#app-vue)). + + + + + +```vue + + + +``` + +### App.vue + +```vue + + +``` diff --git a/docs/components/datepicker.md b/docs/components/datepicker.md new file mode 100644 index 0000000..43e3439 --- /dev/null +++ b/docs/components/datepicker.md @@ -0,0 +1,48 @@ + + +# DatePicker + +The DatePicker component is a prettier alternative to `` + +## Usage + + +
+ + +
Value: {{ date }}
+
+
+ +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------ | :------ | :--------- | :-------------------------------------------- | +| `placeholder` | `null` | `String` | | +| `formatValue` | `null` | `Function` | Function to format the date value for display | diff --git a/docs/components/dialog.md b/docs/components/dialog.md new file mode 100644 index 0000000..64e6cbc --- /dev/null +++ b/docs/components/dialog.md @@ -0,0 +1,214 @@ + + +# Dialog + +The Dialog is a component that is rendered over the page with an overlay. It is +used to show messages or actions when user's attention is needed. + +## Usage (props) + +The `options` prop allows you set a number of options for a standard Dialog +layout. + + + + + + +```vue + + + +``` + +## Props + +There is only one prop `option` which is an Object. Each of the properties of +this object is described in the table below. + +| Name | Default | Value | Description | +| :------------------------ | :----------------- | :--------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | +| `options.title` | `Untitled` | `String` | Title of the dialog | +| `options.message` | `null` | `String` | Message shown in the body of the dialog | +| `options.icon` | `String \| Object` | [FeatherIcon](./feathericon) name or `Object{name, appearance}` | Icon shown to the left of title | +| `options.icon.name` | `String` | [FeatherIcon](./feathericon) name | | +| `options.icon.appearance` | `null` | `warning \| info \| danger \| success` | Changes the color of icon | +| `options.size` | `'lg'` | `xs \| sm \| md \| lg \| xl \| 2xl \| 3xl \| 4xl \| 5xl \| 6xl \| 7xl` | Change the width of the dialog | +| `options.actions` | `null` | Array of [Button](./button) props | Each object in the array must be an object of props passed to a Button component. Click handler is set via `handler` property. | + +## Usage (slots) + +If you want to take control over the markup of each part of the dialog, you can +use slots. The Dialog components is made up of nested slots which allows you to +override a part in a granular way. + +![Dialog Slots](../assets/dialog-slots.png) + +Here are the slot names corresponding to the marked region in the above +screenshot: + +| Number | Name | Description | +| :----- | :------------- | :----------------------------------- | +| 1 | `body` | Override the full body of the dialog | +| 2 | `body-main` | Override the main content | +| 3 | `actions` | Override the actions | +| 4 | `body-title` | Override the title | +| 5 | `body-content` | Override the message | + +In the following example, the slots `body-title` and `body-content` are used. +Notice that we can still use `options.actions` to show our action buttons. + + + + + + + + + +```vue + +``` + +In the following example, we have used the `body` slot. This is top-most slot +and overrides everything. Use it for fully custom dialog layouts. + + + + + + + + +```vue + +``` diff --git a/docs/components/dropdown.md b/docs/components/dropdown.md new file mode 100644 index 0000000..de1df91 --- /dev/null +++ b/docs/components/dropdown.md @@ -0,0 +1,168 @@ + + +# Dropdown + +The Dropdown component is used to show a list of options when a button is +clicked. + +## Usage + + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :---------- | :------- | :------------------------ | :----------------------------------------------- | +| `options` | `null` | `Array` | See [options](#options) | +| `button` | `null` | `String` | Object that is sent as props to Button component | +| `placement` | `'left'` | `left \| center \| right` | Placement of dropdown with respect to button | + +## `options` + +The only required prop for Dropdown is `options`. It can be a list of options or +a list of groups of options. + +```js +// list of options +options = [ + { + label, + handler, + icon, // optional + component, // optional + }, + ... +] + +// list of groups of options +options = [ + { + group, + hideLabel, + items: [ + { + label, + handler, + icon, // optional + component, // optional + }, + ... + ] + } +] +``` diff --git a/docs/components/errormessage.md b/docs/components/errormessage.md new file mode 100644 index 0000000..121a02d --- /dev/null +++ b/docs/components/errormessage.md @@ -0,0 +1,56 @@ + + +# ErrorMessage + +The ErrorMessage component is used to show an error message when an action has +failed. + +## Usage + +You can pass an error message as a string to `message` prop. You can also pass +an +[`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) +object directly to `message` prop. + +The component won't render if `message` is falsy. So, you don't have to write +the `v-if` directive. + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :-------- | :------ | :---------------- | :----------------------- | +| `message` | `null` | `String \| Error` | Message to show as error | diff --git a/docs/components/feathericon.md b/docs/components/feathericon.md new file mode 100644 index 0000000..03dd03a --- /dev/null +++ b/docs/components/feathericon.md @@ -0,0 +1,42 @@ + + +# FeatherIcon + +The FeatherIcon component can be used to render SVG icons from the +[FeatherIcons](https://feathericons.com) project. + +## Usage + +Setting dimensions (width and height) is required otherwise it will render in a +large size. You can also customize the color by setting the `color` CSS +property. You can customize the `stroke-width` by passing it as a prop. + + + + + + + + +```vue + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------- | :------- | :------- | :------------------------------------- | +| `name` | `circle` | `String` | One of 287 icons from feathericons.com | +| `stroke-width` | `1.5` | `Number` | | diff --git a/docs/components/fileuploader.md b/docs/components/fileuploader.md new file mode 100644 index 0000000..5c680db --- /dev/null +++ b/docs/components/fileuploader.md @@ -0,0 +1,98 @@ + + +# FileUploader + +The FileUploader component is a renderless component used to upload files. It +only works with a Jingrow Framework backend. + +## Usage + +Use the default slot to render any HTML you like. A lot of slot props are +available to render a UI that shows file progress. Make sure to call +`openFileSelector` using a user action like a button click. + +When the file upload is complete, the `success` event is emitted with the File +document as JSON object. + + +::: info Note +The following example can't upload the file because it is not connected to a +Jingrow backend. +::: + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------- | :------ | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `fileTypes` | `null` | `String` | String passed to `accept` attribute of file input. Use it to restrict file types to be uploaded. | +| `uploadArgs` | `null` | `Object` | See [uploadArgs](#uploadargs) | +| `validateFile` | `null` | `Function` | Validator function to validate the selected file. File object is passed as first parameter. Return an error message or throw an Error to prevent file upload. | + +## `uploadArgs` + +Options passed to `/api/action/upload_file` as arguments. Object structure looks +like this: + +```js +{ + private, folder, file_url, pagetype, docname, fieldname, method, type +} +``` diff --git a/docs/components/input.md b/docs/components/input.md new file mode 100644 index 0000000..d80295c --- /dev/null +++ b/docs/components/input.md @@ -0,0 +1,52 @@ + + +# Input + +The Input component is a prettier version of `` with some additional +features. + +## Usage + + +
+ + + + + +
+
+ +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------ | :------- | :------------------------------------------------------------------------------ | :------------------------------------------------------ | +| `label` | `null` | `String` | Input label | +| `type` | `'text'` | `text \| number \| checkbox \| textarea \| select \| email \| password \| date` | Type of input | +| `placeholder` | `null` | `String` | Input placeholder | +| `inputClass` | `null` | `String \| Object \| Array` | Classes to apply to `input` element and not the wrapper | +| `iconLeft` | `null` | [FeatherIcon](/components/feathericon) name | Show an icon to the left of the input | +| `debounce` | `null` | `Number` | Debounce (in ms) applied to the `input` event | +| `options` | `null` | `{label, value}[] \| String[]` | Only applicable if `type` is `select` | +| `disabled` | `false` | `Boolean` | Disable input by setting this to `true` | +| `rows` | `3` | `Number` | Only applicable if `type` is `textarea` | diff --git a/docs/components/loading-indicator.md b/docs/components/loading-indicator.md new file mode 100644 index 0000000..252dffe --- /dev/null +++ b/docs/components/loading-indicator.md @@ -0,0 +1,31 @@ + + +# Loading Indicator + +This component is used to show a pending state for async and long running +actions. + +## Usage + +This component doesn't accept any props. However, you can customize its size and +color using the `class` or `style` attribute. + + + + + + + +```vue + + + +``` diff --git a/docs/components/popover.md b/docs/components/popover.md new file mode 100644 index 0000000..2ef382e --- /dev/null +++ b/docs/components/popover.md @@ -0,0 +1,192 @@ + + +# Popover + +The Popover component is used whenever a piece of UI needs to be shown in a +popup. + +## Usage + +Popover is a headless component that will let you create custom popups. The +`target` slot provides slotProps like `togglePopover`, `open`, `close` to +trigger the popover. Here you can render a button or any element that will open +the popover. In the `body` and `body-main` slots you can render the contents of +the popover. + + + + + + + + +```vue + + + +``` + +## Props + +| Name | Default | Value | Description | +| :------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------- | +| `trigger` | `'click'` | `click \| hover` | See [trigger](#trigger) | +| `hoverDelay` | `0` | `Number` in seconds | Only applicable if `trigger` is `hover` | +| `leaveDelay` | `0` | `Number` in seconds | Only applicable if `trigger` is `hover` | +| `placement` | `'bottom-start'` | `top-start \| top \| top-end \| bottom-start \| bottom \| bottom-end \| right-start \| right \| right-end \| left-start \| left \| left-end` | Placement of the popup with respect to the trigger | +| `popoverClass` | `null` | `String` | Class to apply to the popover container | +| `transition` | `null` | `Object \| 'default'` | See [transition](#transition) | +| `hideOnBlur` | `true` | `Boolean` | Whether to close the popup on clicking outside | +| `show` | `undefined` | `Boolean` | Control when popup shows based on this prop | + +### `trigger` + +The trigger prop allows you control whether the popup should open on `click` of +the target element or `hover`. If you set `trigger` as `hover` you get two more +props to control its behaviour: `hoverDelay` and `leaveDelay`. If you keep your +mouse pointer on the popup content the popup wont close. + + + + + + + + +```vue + +``` + +### `transition` + +The transition prop is an object that can be used to add transitions the enter +and exit states of the popup. Internally, this prop is directly passed to the +`transition` component. + + + + + + + + + + + + +```vue + +``` + +## Slots + +| Name | Description | +| :---------- | :------------------------------------------------------------------------------ | +| `target` | Reference element against which the popup is positioned | +| `body-main` | Popup content rendered inside a `div` with white background and rounded corners | +| `body` | Popup content without any markup | diff --git a/docs/components/resource.md b/docs/components/resource.md new file mode 100644 index 0000000..6b5291e --- /dev/null +++ b/docs/components/resource.md @@ -0,0 +1,56 @@ + + +# Resource + +This is a headless component wrapper over [Resource](/resources/resource). + +## Usage + +The Resource component does not render any markup. It provides the resource +object via slotProps. + + + +
+ +
{{ resource.data }}
+
+
+
+ +```vue + + + +``` + +## Props + +| Name | Default | Value | +| :-------- | :------ | :------------------------------------------------------------------- | +| `options` | `null` | [Resource Options](/resources/resource.html#list-of-options-and-api) | diff --git a/docs/components/text-editor.md b/docs/components/text-editor.md new file mode 100644 index 0000000..654d016 --- /dev/null +++ b/docs/components/text-editor.md @@ -0,0 +1,366 @@ + + +# Text Editor + +The Text Editor component is used for rich-text editing. It is based on +[Tiptap](https://tiptap.dev). + +## Usage + +The TextEditor component is very flexible in terms of layout and features. It +provides building blocks like menus and slots for building any type of editor +experience you might want. + +Here is a basic version with fixed menu at the top. + + + + + +```vue + + + +``` + +If you are on Vue version 3.2 or earlier, you need to add this line in your main.js file: +```js +app.config.unwrapInjectedRef = true +``` +You can read more about it here: https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity + + +## Props + +| Name | Default | Value | Description | +| :------------------ | :------ | :-------------------------- | :----------------------------------------------------------------------------------------------------- | +| `content` | `null` | `String` | HTML string to set as the initial value in the editor | +| `placeholder` | `null` | `String` | Placeholder text to show when the content is empty | +| `editorClass` | `null` | `String \| Object \| Array` | Valid [CSS class values](https://vuejs.org/guide/essentials/class-and-style.html#binding-html-classes) | +| `editable` | `true` | `Boolean` | Enable/disable editing | +| `fixedMenu` | `null` | `true \| false \| Array` | See [customizing menu](#customizing-menu) | +| `bubbleMenu` | `null` | `true \| false \| Array` | See [customizing menu](#customizing-menu) | +| `floatingMenu` | `null` | `true \| false \| Array` | See [customizing menu](#customizing-menu) | +| `extensions` | `null` | `Array` | [Tiptap extensions](https://tiptap.dev/extensions) | +| `starterkitOptions` | `null` | `Object` | Options to pass to the [Starterkit Extension](https://tiptap.dev/api/extensions/starter-kit) | +| `mentions` | `null` | `Array` | Array of `{label, value}` for mentions list | + +## Customizing Menu + +There are three types of menus available for the editor. + +- Fixed Menu: Menu that is always visible at a fixed place +- Bubble Menu: Menu that shows up when you select some text +- Floating Menu: Menu that shows up on a new line + +You can choose to use any of these or a combination of these in your editor. +Here are some examples of customized editors: + +### Fixed menu with custom buttons + +You can customize which buttons show up in the menu by passing an array of +button names. You can find the list of all buttons available +[here](https://github.com/jingrow/jingrow-ui/blob/main/src/components/TextEditor/commands.js). + + + + + +```vue + + +``` + +### Minimal editor with bubble menu + +Setting `bubbleMenu` to `true` will show a bubble menu on text selection. You +can also pass a list of button names, just like the previous example. + + + + + +```vue + + +``` + +### Floating menu and bubble menu + +You can combine multiple menus. You can also pass an array of button names. + + + + + +```vue + + +``` + +### Comment Editor + +An example of how to use slots and various features of the TextEditor to make a +customized editor experience. + +There are three slots available: `top`, `bottom` and `editor`. The `top` and +`bottom` are slots for placing menus at the top or bottom of the editor. If you +are using these slots, you must import and render the Menu components manually. +They are available to import as `TextEditorFixedMenu`, `TextEditorBubbleMenu`, +and `TextEditorFloatingMenu`. + +The `editor` slot renders the `TextEditorContent` component, you can override it +and render the `TextEditorContent` component if you want some custom behaviour. + + + + + + + + + +```vue + + + +``` diff --git a/docs/components/toast.md b/docs/components/toast.md new file mode 100644 index 0000000..becd030 --- /dev/null +++ b/docs/components/toast.md @@ -0,0 +1,135 @@ + + +# Toast + +This component is used to show a message in a floating box relative to the +browser window. + +## Usage + +Call the `toast` function with options to create a toast. You need to make sure +you include the `Toasts` component in your root component +([`App.vue`](#app-vue)). + + + + + + +```vue + + + +``` + +### App.vue + +```vue + + +``` + +### `position` + +Toasts can be positioned in six places with respect to the browser window. + +- `top-left` +- `top-center` +- `top-right` +- `bottom-left` +- `bottom-center` +- `bottom-right` + + +
+ + + + + + +
+ +
+ + +```vue + + + +``` + + +## Options + +| Name | Default | Value | +| :------------ | :------------- | :-------------------------------------------- | +| `title` | `null` | `String` | +| `text` | `null` | `String` | +| `timeout` | `5` | `Number` in seconds | +| `position` | `'top-center'` | See [position](#position) | +| `icon` | `null` | [FeatherIcon](/components/feathericon) name | +| `iconClasses` | `null` | CSS Classes to apply to FeatherIcon component | diff --git a/docs/components/tooltip.md b/docs/components/tooltip.md new file mode 100644 index 0000000..7eb2754 --- /dev/null +++ b/docs/components/tooltip.md @@ -0,0 +1,42 @@ + + +# Tooltip + +This component is a wrapper over the [Popover](/components/popover) component +for an easy way to show tooltips on hover over elements. + +## Usage + + + +