frappe-ui 1.0.0-beta.2 → 1.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/experimental/FloatingWindow/FloatingWindow.cy.ts +328 -0
- package/experimental/FloatingWindow/FloatingWindow.vue +303 -0
- package/experimental/FloatingWindow/index.ts +8 -0
- package/experimental/FloatingWindow/types.ts +31 -0
- package/experimental/FloatingWindow/useFloatingWindow.ts +236 -0
- package/experimental/MultiEmailInput/MultiEmailInput.cy.ts +155 -0
- package/experimental/MultiEmailInput/MultiEmailInput.vue +472 -0
- package/experimental/MultiEmailInput/index.ts +17 -0
- package/experimental/MultiEmailInput/stories/AsyncSuggestions.vue +54 -0
- package/experimental/MultiEmailInput/stories/Basic.vue +18 -0
- package/experimental/MultiEmailInput/stories/CustomChip.vue +39 -0
- package/experimental/MultiEmailInput/stories/Labeling.vue +28 -0
- package/experimental/MultiEmailInput/types.ts +138 -0
- package/experimental/MultiEmailInput/utils.ts +60 -0
- package/experimental.ts +12 -0
- package/frappe/Billing/SignupBanner.vue +1 -1
- package/frappe/Billing/TrialBanner.vue +1 -1
- package/frappe/DataImport/DataImport.vue +1 -1
- package/frappe/DataImport/DataImportList.vue +1 -1
- package/frappe/DataImport/ImportSteps.vue +6 -6
- package/frappe/DataImport/MappingStep.vue +1 -1
- package/frappe/DataImport/PreviewStep.vue +9 -9
- package/frappe/DataImport/TemplateModal.vue +1 -1
- package/frappe/DataImport/UploadStep.vue +5 -5
- package/frappe/Filter/Filter.vue +1 -1
- package/frappe/Help/HelpModal.vue +1 -1
- package/frappe/Link/stories/Suffix.vue +2 -2
- package/frappe/Onboarding/GettingStartedBanner.vue +1 -1
- package/frappe/drive/components/AppSwitcher.vue +1 -1
- package/frappe/drive/components/InfoDialog.vue +3 -3
- package/frappe/drive/components/MoveDialog.vue +2 -2
- package/frappe/drive/components/Select/Select.vue +2 -2
- package/frappe/drive/components/ShareDialog.vue +1 -1
- package/frappe/telemetry/index.ts +88 -14
- package/frappe/telemetry/pulse.ts +78 -164
- package/frappe/telemetry/telemetry.test.ts +13 -79
- package/icons/IconPicker.vue +1 -1
- package/package.json +104 -34
- package/src/components/Alert/Alert.md +4 -0
- package/src/components/Alert/Alert.vue +4 -4
- package/src/components/Autocomplete/Autocomplete.vue +6 -6
- package/src/components/Avatar/Avatar.md +4 -0
- package/src/components/Avatar/Avatar.vue +23 -4
- package/src/components/Badge/Badge.api.md +1 -1
- package/src/components/Badge/Badge.cy.ts +34 -16
- package/src/components/Badge/Badge.md +20 -4
- package/src/components/Badge/Badge.vue +60 -45
- package/src/components/Badge/stories/CallMeta.vue +35 -0
- package/src/components/Badge/stories/EventStatus.vue +16 -0
- package/src/components/Badge/stories/ListStatus.vue +91 -0
- package/src/components/Badge/stories/Reactions.vue +33 -0
- package/src/components/Badge/stories/RowTags.vue +39 -0
- package/src/components/Badge/types.ts +1 -2
- package/src/components/BottomSheet/BottomSheet.api.md +57 -0
- package/src/components/BottomSheet/BottomSheet.vue +232 -0
- package/src/components/BottomSheet/index.ts +6 -0
- package/src/components/BottomSheet/types.ts +23 -0
- package/src/components/Breadcrumbs/Breadcrumbs.md +4 -0
- package/src/components/Breadcrumbs/Breadcrumbs.vue +3 -3
- package/src/components/Button/Button.api.md +37 -32
- package/src/components/Button/Button.cy.ts +18 -2
- package/src/components/Button/Button.md +20 -8
- package/src/components/Button/Button.vue +304 -284
- package/src/components/Button/stories/InlineActions.vue +10 -0
- package/src/components/Button/stories/LiveClassCard.vue +34 -0
- package/src/components/Button/stories/SectionAction.vue +17 -0
- package/src/components/Button/stories/SectionControls.vue +28 -0
- package/src/components/Button/stories/SelectionToolbar.vue +20 -0
- package/src/components/Button/stories/StackedActions.vue +10 -0
- package/src/components/Button/types.ts +38 -32
- package/src/components/Calendar/Calendar.api.md +16 -16
- package/src/components/Calendar/Calendar.vue +100 -73
- package/src/components/Calendar/CalendarDaily.vue +21 -20
- package/src/components/Calendar/CalendarMonthEvent.vue +30 -9
- package/src/components/Calendar/CalendarMonthly.vue +60 -39
- package/src/components/Calendar/CalendarTimeMarker.vue +9 -8
- package/src/components/Calendar/CalendarWeekDayEvent.vue +98 -42
- package/src/components/Calendar/CalendarWeekly.vue +77 -41
- package/src/components/Calendar/EventModalContent.vue +32 -16
- package/src/components/Calendar/NewEventModal.vue +19 -16
- package/src/components/Calendar/ShowMoreCalendarEvent.vue +11 -17
- package/src/components/Calendar/{calendarUtils.js → calendarUtils.ts} +76 -52
- package/src/components/Calendar/composables/{useCalendarData.js → useCalendarData.ts} +23 -11
- package/src/components/Calendar/composables/{useEventModal.js → useEventModal.ts} +16 -12
- package/src/components/Calendar/index.ts +12 -2
- package/src/components/Calendar/stories/CustomHeader.vue +1 -1
- package/src/components/Calendar/types.ts +106 -0
- package/src/components/Calendar/{useEventBase.js → useEventBase.ts} +40 -26
- package/src/components/Card.vue +2 -2
- package/src/components/Charts/ECharts.vue +1 -1
- package/src/components/Charts/NumberChart.vue +7 -7
- package/src/components/Charts/eChartOptions.ts +28 -13
- package/src/components/Checkbox/Checkbox.api.md +6 -0
- package/src/components/Checkbox/Checkbox.cy.ts +10 -2
- package/src/components/Checkbox/Checkbox.md +28 -0
- package/src/components/Checkbox/Checkbox.vue +9 -22
- package/src/components/Checkbox/stories/Group.vue +27 -0
- package/src/components/Checkbox/stories/HorizontalGroup.vue +24 -0
- package/src/components/Checkbox/stories/SettingRow.vue +23 -0
- package/src/components/Checkbox/stories/WithDescription.vue +27 -0
- package/src/components/Checkbox/types.ts +7 -0
- package/src/components/CodeEditor/CodeEditor.api.md +139 -0
- package/src/components/CodeEditor/CodeEditor.cy.ts +160 -0
- package/src/components/CodeEditor/CodeEditor.md +104 -0
- package/src/components/CodeEditor/CodeEditor.vue +556 -0
- package/src/components/CodeEditor/CodePreview.test.ts +49 -0
- package/src/components/CodeEditor/CodePreview.vue +45 -0
- package/src/components/CodeEditor/index.ts +17 -0
- package/src/components/CodeEditor/languages.ts +63 -0
- package/src/components/CodeEditor/stories/Default.vue +16 -0
- package/src/components/CodeEditor/stories/Labeling.vue +25 -0
- package/src/components/CodeEditor/stories/MaxHeight.vue +24 -0
- package/src/components/CodeEditor/stories/Preview.vue +36 -0
- package/src/components/CodeEditor/stories/Sizes.vue +15 -0
- package/src/components/CodeEditor/stories/Variants.vue +13 -0
- package/src/components/CodeEditor/theme.ts +272 -0
- package/src/components/CodeEditor/types.ts +69 -0
- package/src/components/Combobox/Combobox.api.md +19 -20
- package/src/components/Combobox/Combobox.cy.ts +54 -14
- package/src/components/Combobox/Combobox.md +4 -0
- package/src/components/Combobox/Combobox.vue +233 -247
- package/src/components/Combobox/ComboboxResults.vue +1 -1
- package/src/components/Combobox/stories/Clearable.vue +2 -2
- package/src/components/Combobox/stories/CreateNew.vue +1 -1
- package/src/components/Combobox/stories/Footer.vue +10 -2
- package/src/components/Combobox/stories/InDialog.vue +1 -1
- package/src/components/Combobox/types.ts +8 -29
- package/src/components/CommandPalette/CommandPaletteItem.vue +1 -1
- package/src/components/ContextMenu/ContextMenu.api.md +52 -0
- package/src/components/ContextMenu/ContextMenu.cy.ts +191 -0
- package/src/components/ContextMenu/ContextMenu.md +25 -0
- package/src/components/ContextMenu/ContextMenu.vue +106 -0
- package/src/components/ContextMenu/index.ts +2 -0
- package/src/components/ContextMenu/stories/FileList.vue +105 -0
- package/src/components/ContextMenu/stories/Groups.vue +95 -0
- package/src/components/ContextMenu/stories/Simple.vue +72 -0
- package/src/components/ContextMenu/types.ts +39 -0
- package/src/components/DatePicker/CalendarPanel.vue +128 -86
- package/src/components/DatePicker/DatePicker.vue +3 -9
- package/src/components/DatePicker/DateRangePicker.vue +4 -8
- package/src/components/DatePicker/DateTimePicker.cy.ts +4 -2
- package/src/components/DatePicker/DateTimePicker.vue +3 -9
- package/src/components/DatePicker/composables.ts +15 -37
- package/src/components/DatePicker/stories/Range.vue +1 -1
- package/src/components/DatePicker/types.ts +1 -1
- package/src/components/DesktopShell/DesktopShell.api.md +40 -0
- package/src/components/DesktopShell/DesktopShell.cy.ts +39 -0
- package/src/components/DesktopShell/DesktopShell.md +31 -0
- package/src/components/DesktopShell/DesktopShell.vue +81 -0
- package/src/components/DesktopShell/index.ts +3 -0
- package/src/components/DesktopShell/stories/Default.vue +97 -0
- package/src/components/DesktopShell/types.ts +2 -0
- package/src/components/Dialog/Dialog.api.md +7 -8
- package/src/components/Dialog/Dialog.md +4 -0
- package/src/components/Dialog/Dialog.vue +8 -7
- package/src/components/Dialog/stories/CommandPalette.vue +1 -1
- package/src/components/Dialog/stories/Wizard.vue +1 -1
- package/src/components/Dialogs.vue +11 -9
- package/src/components/Divider/Divider.api.md +1 -1
- package/src/components/Divider/Divider.md +4 -0
- package/src/components/Dropdown/Dropdown.api.md +10 -35
- package/src/components/Dropdown/Dropdown.md +10 -6
- package/src/components/Dropdown/Dropdown.vue +33 -47
- package/src/components/Dropdown/stories/{05_KebabMenu.vue → KebabMenu.vue} +3 -1
- package/src/components/Dropdown/stories/{06_UserMenu.vue → UserMenu.vue} +1 -1
- package/src/components/Dropdown/types.ts +25 -189
- package/src/components/Duration/Duration.api.md +92 -0
- package/src/components/Duration/Duration.cy.ts +183 -0
- package/src/components/Duration/Duration.md +98 -0
- package/src/components/Duration/Duration.vue +114 -0
- package/src/components/Duration/duration.ts +185 -0
- package/src/components/Duration/index.ts +8 -0
- package/src/components/Duration/stories/Default.vue +10 -0
- package/src/components/Duration/stories/Formats.vue +16 -0
- package/src/components/Duration/stories/RealWorld.vue +50 -0
- package/src/components/Duration/stories/Sizes.vue +15 -0
- package/src/components/Duration/stories/States.vue +21 -0
- package/src/components/Duration/types.ts +52 -0
- package/src/components/ErrorMessage/ErrorMessage.cy.ts +1 -1
- package/src/components/ErrorMessage/ErrorMessage.md +4 -0
- package/src/components/ErrorMessage/ErrorMessage.vue +1 -1
- package/src/components/FileUploader/FileUploader.api.md +36 -11
- package/src/components/FileUploader/FileUploader.cy.ts +66 -0
- package/src/components/FileUploader/FileUploader.md +4 -1
- package/src/components/FileUploader/FileUploader.vue +147 -106
- package/src/components/FileUploader/index.ts +6 -0
- package/src/components/FileUploader/types.ts +43 -0
- package/src/components/FormControl/FormControl.api.md +3 -4
- package/src/components/FormControl/FormControl.md +4 -0
- package/src/components/FormControl/stories/RealForm.vue +3 -3
- package/src/components/FormLabel.vue +5 -8
- package/src/components/HoverCard/HoverCard.api.md +100 -0
- package/src/components/HoverCard/HoverCard.cy.ts +103 -0
- package/src/components/HoverCard/HoverCard.md +52 -0
- package/src/components/HoverCard/HoverCard.vue +91 -0
- package/src/components/HoverCard/index.ts +2 -0
- package/src/components/HoverCard/stories/Arrow.vue +28 -0
- package/src/components/HoverCard/stories/Delays.vue +16 -0
- package/src/components/HoverCard/stories/Example.vue +28 -0
- package/src/components/HoverCard/types.ts +47 -0
- package/src/components/Icon/Icon.cy.ts +78 -0
- package/src/components/Icon/Icon.vue +50 -0
- package/src/components/Icon/index.ts +1 -0
- package/src/components/InputLabeling/InputDescription.vue +1 -1
- package/src/components/InputLabeling/InputError.vue +1 -1
- package/src/components/InputLabeling/InputLabel.vue +21 -7
- package/src/components/InputLabeling/RequiredIndicator.vue +1 -1
- package/src/components/ItemListRow/ItemListRow.vue +1 -1
- package/src/components/ItemListRow/stories/RowStates.vue +1 -1
- package/src/components/KeyboardShortcut.vue +4 -8
- package/src/components/KeyboardShortcutsModal/KeyboardShortcutsModal.api.md +22 -14
- package/src/components/KeyboardShortcutsModal/KeyboardShortcutsModal.vue +2 -2
- package/src/components/ListFilter/ListFilter.api.md +5 -6
- package/src/components/ListFilter/ListFilter.vue +161 -57
- package/src/components/ListFilter/NestedPopover.vue +17 -10
- package/src/components/ListFilter/SearchComplete.vue +31 -38
- package/src/components/ListItem.vue +1 -1
- package/src/components/ListView/ListEmptyState.vue +1 -1
- package/src/components/ListView/ListGroupHeader.vue +2 -2
- package/src/components/ListView/ListRow.vue +1 -1
- package/src/components/ListView/ListSelectBanner.vue +1 -1
- package/src/components/ListView/ListView.api.md +322 -6
- package/src/components/ListView/index.ts +25 -0
- package/src/components/ListView/stories/CustomList.vue +1 -1
- package/src/components/ListView/stories/GroupedRows.vue +1 -1
- package/src/components/LoadingIndicator.vue +13 -22
- package/src/components/LoadingText.vue +3 -13
- package/src/components/{Dropdown/DropdownMenuList.vue → Menu/Menu.vue} +110 -75
- package/src/components/{Dropdown/DropdownMenuItemContent.vue → Menu/MenuItemContent.vue} +23 -33
- package/src/components/{Dropdown/DropdownRenderContent.vue → Menu/MenuRenderContent.vue} +1 -1
- package/src/components/Menu/types.ts +225 -0
- package/src/components/{Dropdown → Menu}/utils.ts +36 -40
- package/src/components/MobileNav/MobileNav.api.md +70 -0
- package/src/components/MobileNav/MobileNav.cy.ts +89 -0
- package/src/components/MobileNav/MobileNav.md +34 -0
- package/src/components/MobileNav/MobileNav.vue +15 -0
- package/src/components/MobileNav/MobileNavItem.vue +125 -0
- package/src/components/MobileNav/index.ts +4 -0
- package/src/components/MobileNav/stories/Default.vue +40 -0
- package/src/components/MobileNav/types.ts +33 -0
- package/src/components/MobileShell/MobileShell.api.md +23 -0
- package/src/components/MobileShell/MobileShell.cy.ts +25 -0
- package/src/components/MobileShell/MobileShell.md +24 -0
- package/src/components/MobileShell/MobileShell.vue +41 -0
- package/src/components/MobileShell/index.ts +3 -0
- package/src/components/MobileShell/stories/Default.vue +75 -0
- package/src/components/MobileShell/types.ts +7 -0
- package/src/components/MonthPicker/MonthPicker.cy.ts +16 -0
- package/src/components/MonthPicker/MonthPicker.vue +19 -8
- package/src/components/MultiSelect/MultiSelect.api.md +3 -4
- package/src/components/MultiSelect/MultiSelect.md +42 -10
- package/src/components/MultiSelect/MultiSelect.vue +199 -227
- package/src/components/MultiSelect/MultiSelectResults.vue +1 -1
- package/src/components/MultiSelect/stories/Members.vue +1 -1
- package/src/components/MultiSelect/stories/TagsTrigger.vue +3 -3
- package/src/components/MultiSelect/types.ts +3 -3
- package/src/components/PageHeader/PageHeader.api.md +112 -0
- package/src/components/PageHeader/PageHeader.cy.ts +68 -0
- package/src/components/PageHeader/PageHeader.md +25 -0
- package/src/components/PageHeader/PageHeader.vue +13 -0
- package/src/components/PageHeader/PageHeaderBackButton.vue +29 -0
- package/src/components/PageHeader/PageHeaderBase.vue +51 -0
- package/src/components/PageHeader/PageHeaderMobile.vue +68 -0
- package/src/components/PageHeader/PageHeaderMobileTitle.vue +16 -0
- package/src/components/PageHeader/PageHeaderTarget.vue +13 -0
- package/src/components/PageHeader/PageHeaderTitle.vue +12 -0
- package/src/components/PageHeader/index.ts +7 -0
- package/src/components/PageHeader/stories/Default.vue +19 -0
- package/src/components/PageHeader/stories/Mobile.vue +24 -0
- package/src/components/PageHeader/target.ts +19 -0
- package/src/components/PageHeader/types.ts +19 -0
- package/src/components/Password/Password.md +4 -0
- package/src/components/Password/Password.vue +2 -2
- package/src/components/Popover/Popover.api.md +126 -42
- package/src/components/Popover/Popover.cy.ts +425 -41
- package/src/components/Popover/Popover.md +99 -4
- package/src/components/Popover/Popover.vue +288 -215
- package/src/components/Popover/index.ts +8 -1
- package/src/components/Popover/stories/Arrow.vue +16 -0
- package/src/components/Popover/stories/Bare.vue +20 -0
- package/src/components/Popover/stories/Click.vue +7 -4
- package/src/components/Popover/stories/Controlled.vue +22 -0
- package/src/components/Popover/stories/Dismissible.vue +19 -0
- package/src/components/Popover/stories/MatchTriggerWidth.vue +16 -0
- package/src/components/Popover/stories/SideAlign.vue +17 -0
- package/src/components/Popover/types.ts +101 -23
- package/src/components/Progress/Progress.cy.ts +2 -2
- package/src/components/Progress/Progress.md +4 -0
- package/src/components/Progress/Progress.vue +4 -4
- package/src/components/Rail/Rail.api.md +91 -0
- package/src/components/Rail/Rail.cy.ts +84 -0
- package/src/components/Rail/Rail.md +30 -0
- package/src/components/Rail/Rail.vue +16 -0
- package/src/components/Rail/RailItem.vue +134 -0
- package/src/components/Rail/RailItemBadge.vue +64 -0
- package/src/components/Rail/index.ts +2 -0
- package/src/components/Rail/stories/Default.vue +44 -0
- package/src/components/Rail/types.ts +37 -0
- package/src/components/Rating/Rating.md +4 -0
- package/src/components/Rating/Rating.vue +5 -8
- package/src/components/Rating/stories/CustomSlot.vue +2 -2
- package/src/components/ScrollArea/ScrollArea.api.md +59 -0
- package/src/components/ScrollArea/ScrollArea.vue +46 -0
- package/src/components/ScrollArea/ScrollBar.vue +101 -0
- package/src/components/ScrollArea/index.ts +2 -0
- package/src/components/Select/Select.cy.ts +16 -15
- package/src/components/Select/Select.md +4 -0
- package/src/components/Select/Select.vue +217 -203
- package/src/components/Select/stories/Example.vue +4 -3
- package/src/components/Select/stories/States.vue +1 -1
- package/src/components/Select/stories/TriggerSlots.vue +2 -2
- package/src/components/Select/utils.ts +4 -3
- package/src/components/SettingsDialog/SettingsBody.vue +23 -0
- package/src/components/SettingsDialog/SettingsContent.vue +7 -0
- package/src/components/SettingsDialog/SettingsDialog.api.md +273 -0
- package/src/components/SettingsDialog/SettingsDialog.cy.ts +126 -0
- package/src/components/SettingsDialog/SettingsDialog.md +43 -0
- package/src/components/SettingsDialog/SettingsDialog.vue +76 -0
- package/src/components/SettingsDialog/SettingsHeader.vue +36 -0
- package/src/components/SettingsDialog/SettingsNavGroup.vue +20 -0
- package/src/components/SettingsDialog/SettingsNavItem.vue +33 -0
- package/src/components/SettingsDialog/SettingsPanel.cy.ts +77 -0
- package/src/components/SettingsDialog/SettingsPanel.vue +26 -0
- package/src/components/SettingsDialog/SettingsRow.vue +52 -0
- package/src/components/SettingsDialog/SettingsSidebar.vue +16 -0
- package/src/components/SettingsDialog/index.ts +10 -0
- package/src/components/SettingsDialog/stories/Default.vue +99 -0
- package/src/components/SettingsDialog/stories/PanelBasic.vue +52 -0
- package/src/components/SettingsDialog/stories/panels/NotificationsPanel.vue +57 -0
- package/src/components/SettingsDialog/stories/panels/PreferencesPanel.vue +57 -0
- package/src/components/SettingsDialog/stories/panels/ProfilePanel.vue +38 -0
- package/src/components/SettingsDialog/stories/panels/UsersPanel.vue +83 -0
- package/src/components/SettingsDialog/types.ts +21 -0
- package/src/components/Sidebar/Sidebar.api.md +124 -65
- package/src/components/Sidebar/Sidebar.cy.ts +127 -76
- package/src/components/Sidebar/Sidebar.md +47 -3
- package/src/components/Sidebar/Sidebar.vue +64 -56
- package/src/components/Sidebar/SidebarCollapseToggle.vue +23 -0
- package/src/components/Sidebar/SidebarHeader.vue +76 -52
- package/src/components/Sidebar/SidebarItem.vue +159 -95
- package/src/components/Sidebar/SidebarItemIcon.vue +25 -0
- package/src/components/Sidebar/SidebarLabel.vue +34 -0
- package/src/components/Sidebar/SidebarSection.vue +29 -14
- package/src/components/Sidebar/index.ts +13 -2
- package/src/components/Sidebar/stories/Collapsed.vue +40 -0
- package/src/components/Sidebar/stories/Default.vue +153 -0
- package/src/components/Sidebar/stories/Legacy.vue +36 -0
- package/src/components/Sidebar/types.ts +105 -20
- package/src/components/Skeleton/Skeleton.cy.ts +21 -0
- package/src/components/Skeleton/Skeleton.md +21 -0
- package/src/components/Skeleton/Skeleton.vue +6 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Skeleton/stories/Card.vue +12 -0
- package/src/components/Skeleton/stories/List.vue +15 -0
- package/src/components/Skeleton/stories/Text.vue +11 -0
- package/src/components/Slider/Slider.api.md +1 -1
- package/src/components/Slider/Slider.cy.ts +29 -0
- package/src/components/Slider/Slider.md +10 -0
- package/src/components/Slider/Slider.vue +37 -9
- package/src/components/Slider/stories/NegativeValues.vue +12 -0
- package/src/components/Slider/types.ts +1 -1
- package/src/components/Spinner/Spinner.api.md +32 -0
- package/src/components/Spinner/Spinner.cy.ts +87 -0
- package/src/components/Spinner/Spinner.md +21 -0
- package/src/components/Spinner/Spinner.vue +168 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Spinner/stories/InContext.vue +49 -0
- package/src/components/Spinner/stories/Sizes.vue +12 -0
- package/src/components/Spinner/stories/Themes.vue +10 -0
- package/src/components/Spinner/stories/Track.vue +12 -0
- package/src/components/Spinner/types.ts +14 -0
- package/src/components/Switch/Switch.md +4 -0
- package/src/components/Switch/Switch.vue +6 -23
- package/src/components/TabButtons/Pill.vue +161 -0
- package/src/components/TabButtons/TabButtons.api.md +52 -2
- package/src/components/TabButtons/TabButtons.cy.ts +18 -5
- package/src/components/TabButtons/TabButtons.md +49 -0
- package/src/components/TabButtons/TabButtons.vue +207 -109
- package/src/components/TabButtons/index.ts +11 -1
- package/src/components/TabButtons/pillTypes.ts +43 -0
- package/src/components/TabButtons/stories/PrefixSuffix.vue +65 -0
- package/src/components/TabButtons/stories/Sizes.vue +26 -0
- package/src/components/TabButtons/stories/Variants.vue +40 -0
- package/src/components/TabButtons/stories/Vertical.vue +64 -0
- package/src/components/TabButtons/types.ts +49 -0
- package/src/components/Tabs/Tabs.md +4 -0
- package/src/components/Tabs/Tabs.vue +1 -1
- package/src/components/TextEditor/TextEditor.cy.ts +1 -1
- package/src/components/TextEditor/TextEditor.md +3 -0
- package/src/components/TextEditor/TextEditor.vue +7 -4
- package/src/components/TextEditor/components/CodeBlockComponent.vue +4 -194
- package/src/components/TextEditor/components/FontColor.vue +1 -1
- package/src/components/TextEditor/components/LinkPopup.vue +1 -1
- package/src/components/TextEditor/components/MediaNodeView.vue +17 -17
- package/src/components/TextEditor/components/MentionList.vue +1 -1
- package/src/components/TextEditor/components/TextEditorBubbleMenu.vue +14 -5
- package/src/components/TextEditor/components/TextEditorFixedMenu.vue +3 -3
- package/src/components/TextEditor/components/{Menu.vue → TextEditorMenu.vue} +3 -3
- package/src/components/TextEditor/extensions/iframe/InsertIframe.vue +2 -2
- package/src/components/TextEditor/extensions/iframe/iframe-extension.ts +62 -19
- package/src/components/TextEditor/extensions/image-group/ImageGroupNodeView.vue +1 -1
- package/src/components/TextEditor/extensions/image-group/ImageGroupUploadDialog.vue +4 -4
- package/src/components/TextEditor/extensions/suggestion/SuggestionList.vue +1 -1
- package/src/components/TextEditor/extensions/toc-node/TocNodeView.vue +1 -1
- package/src/components/TextEditor/hljs-github.css +197 -0
- package/src/components/TextEditor/index.ts +13 -0
- package/src/components/TextInput/TextInput.cy.ts +8 -3
- package/src/components/TextInput/TextInput.md +4 -0
- package/src/components/TextInput/TextInput.vue +11 -15
- package/src/components/TextInput/stories/Slots.vue +1 -1
- package/src/components/Textarea/Textarea.cy.ts +1 -1
- package/src/components/Textarea/Textarea.md +4 -0
- package/src/components/Textarea/Textarea.vue +11 -15
- package/src/components/ThemeSwitcher/ThemePreview.vue +74 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.api.md +82 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.cy.ts +101 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.md +70 -0
- package/src/components/ThemeSwitcher/ThemeSwitcher.vue +145 -0
- package/src/components/ThemeSwitcher/index.ts +2 -0
- package/src/components/ThemeSwitcher/previews/PreviewWindow.vue +74 -0
- package/src/components/ThemeSwitcher/stories/Branded.vue +12 -0
- package/src/components/ThemeSwitcher/stories/Default.vue +7 -0
- package/src/components/ThemeSwitcher/stories/Menu.vue +53 -0
- package/src/components/ThemeSwitcher/stories/Toggle.vue +25 -0
- package/src/components/ThemeSwitcher/types.ts +35 -0
- package/src/components/TimePicker/TimePicker.api.md +67 -15
- package/src/components/TimePicker/TimePicker.cy.ts +130 -14
- package/src/components/TimePicker/TimePicker.md +6 -0
- package/src/components/TimePicker/TimePicker.vue +74 -19
- package/src/components/TimePicker/stories/Labeling.vue +21 -0
- package/src/components/TimePicker/stories/Range.vue +1 -1
- package/src/components/TimePicker/stories/SizesAndVariants.vue +23 -0
- package/src/components/TimePicker/stories/TwentyFour.vue +1 -1
- package/src/components/TimePicker/types.ts +15 -3
- package/src/components/TimePicker/utils.ts +82 -21
- package/src/components/Toast/Toast.md +1 -1
- package/src/components/Toast/Toast.sanitize.test.ts +50 -0
- package/src/components/Toast/Toast.test.ts +5 -0
- package/src/components/Toast/Toast.vue +7 -7
- package/src/components/Toast/ToastProvider.vue +18 -9
- package/src/components/Toast/stories/CustomIcon.vue +1 -1
- package/src/components/Toast/toast.ts +32 -8
- package/src/components/Tooltip/Tooltip.api.md +43 -5
- package/src/components/Tooltip/Tooltip.cy.ts +48 -0
- package/src/components/Tooltip/Tooltip.md +10 -0
- package/src/components/Tooltip/Tooltip.vue +20 -5
- package/src/components/Tooltip/TooltipBubble.vue +2 -2
- package/src/components/Tooltip/TooltipProvider.vue +53 -0
- package/src/components/Tooltip/index.ts +1 -0
- package/src/components/Tooltip/stories/Group.vue +22 -0
- package/src/components/Tooltip/stories/Slots.vue +2 -2
- package/src/components/Tooltip/types.ts +1 -1
- package/src/components/Tree/Tree.api.md +76 -18
- package/src/components/Tree/Tree.md +144 -2
- package/src/components/Tree/Tree.vue +276 -97
- package/src/components/Tree/TreeItem.vue +263 -0
- package/src/components/Tree/index.ts +9 -1
- package/src/components/Tree/stories/DragDrop.vue +94 -0
- package/src/components/Tree/stories/Example.vue +11 -34
- package/src/components/Tree/stories/ExpandAll.vue +48 -0
- package/src/components/Tree/stories/Guides.vue +49 -0
- package/src/components/Tree/stories/ListView.vue +74 -0
- package/src/components/Tree/tree.cy.ts +218 -90
- package/src/components/Tree/types.ts +114 -24
- package/src/components/Tree/useTreeDragDrop.ts +203 -0
- package/src/components/Tree/useTreeKeyboard.ts +109 -0
- package/src/components/VueGridLayout/Layout.vue +1 -1
- package/src/components/shared/picker/PickerShell.vue +20 -15
- package/src/components/shared/popover/PopoverPanel.vue +67 -0
- package/src/components/shared/popover/popoverPanel.css +102 -0
- package/src/components/shared/selection/utils.ts +6 -6
- package/src/composables/useAutofocusOnOpen.ts +2 -0
- package/src/composables/useScreenSize.ts +46 -0
- package/src/composables/useScrollContainer.ts +108 -0
- package/src/composables/useShortcut.ts +3 -0
- package/src/data-fetching/docStore.test.ts +74 -0
- package/src/data-fetching/docStore.ts +18 -7
- package/src/data-fetching/useCall/types.ts +1 -0
- package/src/data-fetching/useCall/useCall.test.ts +43 -0
- package/src/data-fetching/useCall/useCall.ts +17 -4
- package/src/data-fetching/useDoc/useDoc.test.ts +61 -2
- package/src/data-fetching/useDoc/useDoc.ts +35 -15
- package/src/data-fetching/useDoctype/useDoctype.ts +6 -5
- package/src/data-fetching/useFrappeFetch.ts +17 -4
- package/src/data-fetching/useList/types.ts +1 -0
- package/src/data-fetching/useList/useList.test.ts +40 -0
- package/src/data-fetching/useList/useList.ts +20 -13
- package/src/data-fetching/useNewDoc/useNewDoc.ts +9 -5
- package/src/directives/focus.ts +3 -3
- package/src/index.ts +105 -74
- package/src/mocks/handlers.ts +8 -0
- package/src/molecules/editor/Editor.cy.ts +6 -1
- package/src/molecules/editor/EditorBubbleMenu.vue +1 -1
- package/src/molecules/editor/EditorContent.vue +50 -9
- package/src/molecules/editor/EditorFixedMenu.vue +2 -1
- package/src/molecules/editor/EditorFloatingMenu.vue +1 -1
- package/src/molecules/editor/EditorTableMenu.vue +303 -0
- package/src/molecules/editor/MenuItems.vue +81 -69
- package/src/molecules/editor/commands.ts +10 -2
- package/src/molecules/editor/components/AttachmentNodeView.vue +115 -0
- package/src/molecules/editor/components/EditorDropZone.vue +105 -0
- package/src/molecules/editor/components/EditorPopover.vue +89 -0
- package/src/molecules/editor/components/MediaNodeView.vue +156 -49
- package/src/molecules/editor/components/MediaResizeHandles.vue +38 -0
- package/src/molecules/editor/components/MediaToolbar.vue +108 -54
- package/src/molecules/editor/components/TableContextMenu.vue +144 -0
- package/src/molecules/editor/components/UploadProgressIndicator.vue +33 -0
- package/src/molecules/editor/components/VideoControls.vue +208 -0
- package/src/molecules/editor/components/font-color/ColorSwatchGrid.vue +3 -5
- package/src/molecules/editor/components/font-color/fontColorController.ts +28 -23
- package/src/molecules/editor/components/font-color/swatches.ts +1 -1
- package/src/molecules/editor/components/media-node-view-controller.ts +4 -4
- package/src/molecules/editor/components/media-node-view-utils.test.ts +0 -13
- package/src/molecules/editor/components/media-node-view-utils.ts +0 -10
- package/src/molecules/editor/components/table-color/tableCellColorController.ts +80 -0
- package/src/molecules/editor/components/table-size-picker/TableSizePicker.vue +92 -0
- package/src/molecules/editor/components/table-size-picker/tableSizePickerController.ts +40 -0
- package/src/molecules/editor/composables/useEditorFileDrop.ts +117 -0
- package/src/molecules/editor/composables/useFloatingPopup.ts +46 -2
- package/src/molecules/editor/composables/useNodeViewResize.test.ts +60 -11
- package/src/molecules/editor/composables/useNodeViewResize.ts +80 -21
- package/src/molecules/editor/composables/useTableCellColorState.ts +78 -0
- package/src/molecules/editor/composables/useWindowFileDragging.ts +68 -0
- package/src/molecules/editor/extensions/attachment/attachment-engine.ts +250 -0
- package/src/molecules/editor/extensions/attachment/attachment-extension.ts +248 -0
- package/src/molecules/editor/extensions/attachment/attachment.flow.test.ts +80 -0
- package/src/molecules/editor/extensions/attachment/attachment.roundtrip.test.ts +50 -0
- package/src/molecules/editor/extensions/attachment/index.ts +5 -0
- package/src/molecules/editor/extensions/code-block/CodeBlockComponent.css +16 -26
- package/src/molecules/editor/extensions/code-block/CodeBlockComponent.vue +107 -22
- package/src/molecules/editor/extensions/content-paste/content-paste-extension.ts +5 -3
- package/src/molecules/editor/extensions/iframe/IframeInsertDialog.vue +34 -7
- package/src/molecules/editor/extensions/iframe/IframeNodeView.vue +54 -48
- package/src/molecules/editor/extensions/iframe/iframe-allowlist.ts +33 -4
- package/src/molecules/editor/extensions/iframe/iframe-commands.ts +49 -5
- package/src/molecules/editor/extensions/iframe/iframe-embed-utils.ts +30 -0
- package/src/molecules/editor/extensions/iframe/iframe-extension.ts +4 -4
- package/src/molecules/editor/extensions/iframe/iframeInsertDialogController.ts +12 -1
- package/src/molecules/editor/extensions/iframe/index.ts +4 -4
- package/src/molecules/editor/extensions/iframe/useIframeDialog.ts +25 -13
- package/src/molecules/editor/extensions/image/image-extension.ts +29 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupGrid.vue +27 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupGridCell.vue +55 -11
- package/src/molecules/editor/extensions/image-group/ImageGroupNodeView.vue +64 -31
- package/src/molecules/editor/extensions/image-group/ImageGroupUploadDialog.vue +84 -40
- package/src/molecules/editor/extensions/image-group/image-group-extension.ts +4 -3
- package/src/molecules/editor/extensions/image-group/useImageGroupDialog.ts +116 -17
- package/src/molecules/editor/extensions/link/LinkEditorPopup.vue +214 -71
- package/src/molecules/editor/extensions/link/link-click-plugin.ts +10 -15
- package/src/molecules/editor/extensions/link/link-commands.ts +24 -3
- package/src/molecules/editor/extensions/link/link-extension.ts +11 -2
- package/src/molecules/editor/extensions/link/link-popup-controller.test.ts +49 -0
- package/src/molecules/editor/extensions/link/link-popup-controller.ts +67 -2
- package/src/molecules/editor/extensions/link/link-shortcut-plugin.ts +7 -2
- package/src/molecules/editor/extensions/media-drop/media-drop-extension.ts +177 -0
- package/src/molecules/editor/extensions/shared/media-dimensions.ts +36 -3
- package/src/molecules/editor/extensions/shared/media-node-ops.ts +17 -0
- package/src/molecules/editor/extensions/shared/media-plugin.test.ts +60 -0
- package/src/molecules/editor/extensions/shared/media-plugin.ts +7 -37
- package/src/molecules/editor/extensions/shared/media-upload-engine.test.ts +39 -2
- package/src/molecules/editor/extensions/shared/media-upload-engine.ts +104 -10
- package/src/molecules/editor/extensions/shared/media-upload-state.ts +55 -0
- package/src/molecules/editor/extensions/shared/media-upload-types.ts +52 -1
- package/src/molecules/editor/extensions/shared/suggestion-helpers.ts +20 -0
- package/src/molecules/editor/extensions/shared/suggestion-types.ts +7 -0
- package/src/molecules/editor/extensions/shared/upload-types.ts +1 -0
- package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.test.ts +38 -0
- package/src/molecules/editor/extensions/slash-commands/slash-commands-extension.ts +119 -55
- package/src/molecules/editor/extensions/suggestion/SuggestionList.vue +62 -22
- package/src/molecules/editor/extensions/suggestion/SuggestionListItem.vue +1 -1
- package/src/molecules/editor/extensions/suggestion/createSuggestionExtension.ts +4 -0
- package/src/molecules/editor/extensions/table/drag-scroll.ts +154 -0
- package/src/molecules/editor/extensions/table/table-cell-color.ts +188 -0
- package/src/molecules/editor/extensions/table/table-navigation.ts +560 -0
- package/src/molecules/editor/extensions/table/table-selection-overlay.ts +137 -0
- package/src/molecules/editor/extensions/toc-node/TocNodeView.vue +1 -1
- package/src/molecules/editor/extensions/toc-node/toc-render.ts +7 -3
- package/src/molecules/editor/extensions/video/video-extension.ts +72 -5
- package/src/molecules/editor/extensions.ts +176 -8
- package/src/molecules/editor/index.ts +9 -4
- package/src/molecules/editor/kits.test.ts +21 -1
- package/src/molecules/editor/kits.ts +56 -33
- package/src/molecules/editor/menu.test.ts +3 -1
- package/src/molecules/editor/menu.ts +180 -18
- package/src/molecules/editor/stories/Comment.vue +4 -4
- package/src/molecules/editor/stories/Inline.vue +1 -1
- package/src/molecules/editor/stories/Primitives.vue +1 -1
- package/src/molecules/editor/stories/RichText.vue +1 -1
- package/src/molecules/editor/style.css +80 -3
- package/src/molecules/editor/useEditor.test.ts +4 -0
- package/src/molecules/editor/useEditor.ts +7 -3
- package/src/molecules/list/List.cy.ts +626 -0
- package/src/molecules/list/List.vue +93 -0
- package/src/molecules/list/ListCell.vue +15 -0
- package/src/molecules/list/ListGroup.vue +40 -0
- package/src/molecules/list/ListHeader.vue +62 -0
- package/src/molecules/list/ListHeaderCell.vue +26 -0
- package/src/molecules/list/ListHeaderCellSort.vue +106 -0
- package/src/molecules/list/ListRow.vue +55 -0
- package/src/molecules/list/ListRowBase.vue +103 -0
- package/src/molecules/list/ListRows.vue +120 -0
- package/src/molecules/list/index.ts +24 -0
- package/src/molecules/list/list-context.ts +44 -0
- package/src/molecules/list/list.api.md +276 -0
- package/src/molecules/list/list.md +119 -0
- package/src/molecules/list/stories/Columns.vue +103 -0
- package/src/molecules/list/stories/Feed.vue +105 -0
- package/src/molecules/list/stories/Virtual.vue +31 -0
- package/src/molecules/list/style.css +173 -0
- package/src/molecules/list/types.ts +82 -0
- package/src/molecules/list/useVirtualRows.ts +80 -0
- package/src/resources/documentResource.d.ts +28 -0
- package/src/resources/documentResource.js +32 -4
- package/src/resources/documentResource.test.ts +111 -0
- package/src/resources/listResource.d.ts +33 -0
- package/src/resources/plugin.d.ts +7 -0
- package/src/resources/resources.d.ts +35 -0
- package/src/utils/{call.js → call.ts} +50 -9
- package/src/utils/config.ts +16 -1
- package/src/utils/confirmDialog.d.ts +17 -0
- package/src/utils/debounce.ts +6 -7
- package/src/utils/dialog.cy.ts +9 -9
- package/src/utils/fileSize.ts +36 -0
- package/src/utils/fileUploadHandler.ts +46 -6
- package/src/utils/frappeRequest.test.ts +79 -0
- package/src/utils/{frappeRequest.js → frappeRequest.ts} +76 -13
- package/src/utils/pageMeta.ts +1 -1
- package/src/utils/plugin.ts +58 -0
- package/src/utils/{request.js → request.ts} +28 -3
- package/src/utils/{socketio.js → socketio.ts} +11 -1
- package/src/utils/theme.ts +48 -57
- package/src/utils/useFileUpload.ts +80 -23
- package/tailwind/audit-token-drift.cjs +176 -0
- package/tailwind/colorPalette.js +69 -8
- package/tailwind/colors.json +1115 -397
- package/tailwind/figma-tokens-to-theme.js +364 -0
- package/tailwind/generated/colors.json +1217 -0
- package/tailwind/generated/effects.json +41 -0
- package/tailwind/generated/radius.json +20 -0
- package/tailwind/generated/typography.json +479 -0
- package/tailwind/migrate-tokens-v2.js +453 -0
- package/tailwind/migrate-tokens-v2.test.js +55 -0
- package/tailwind/plugin.js +220 -15
- package/tailwind/preset.js +15 -0
- package/tailwind/tokens.js +35 -37
- package/tsconfig.base.json +2 -2
- package/vitepress/components/Docs/CommandPalette.vue +214 -0
- package/vitepress/components/Docs/Demo.vue +60 -0
- package/vitepress/components/Docs/EmitsTable.vue +99 -0
- package/vitepress/components/Docs/MobileNavSheet.vue +148 -0
- package/vitepress/components/Docs/OnThisPage.vue +74 -0
- package/vitepress/components/Docs/PrevNextBtns.vue +58 -0
- package/vitepress/components/Docs/PropsTable.vue +185 -0
- package/vitepress/components/Docs/Search.vue +34 -0
- package/vitepress/components/Docs/Sidebar.vue +91 -0
- package/vitepress/components/Docs/SlotsTable.vue +99 -0
- package/vitepress/components/Docs/index.ts +13 -0
- package/vitepress/components/Docs/sidebarList.ts +33 -0
- package/vitepress/components/Layout.vue +58 -0
- package/vitepress/components/Navbar.vue +126 -0
- package/vitepress/components/index.ts +3 -0
- package/vitepress/composables/index.ts +3 -0
- package/vitepress/composables/useTheme.ts +39 -0
- package/vitepress/css/fonts/Inter-Italic.var.woff2 +0 -0
- package/vitepress/css/fonts/Inter.var.woff2 +0 -0
- package/vitepress/css/style.css +198 -0
- package/vitepress/index.d.ts +23 -0
- package/vitepress/index.node.ts +175 -0
- package/vitepress/index.ts +17 -0
- package/vitepress/plugins/colocatedComponentDocs.ts +164 -0
- package/vitepress/plugins/componentTransformer.ts +286 -0
- package/vitepress/plugins/index.ts +12 -0
- package/vitepress/state.ts +10 -0
- package/vitepress/theme/index.ts +21 -0
- package/frappe/telemetry/utils.ts +0 -15
- package/src/components/Badge/stories/Themes.vue +0 -10
- package/src/components/Badge/stories/Variants.vue +0 -10
- package/src/components/Button/stories/Icons.vue +0 -27
- package/src/components/Button/stories/Sizes.vue +0 -11
- package/src/components/Button/stories/Themes.vue +0 -10
- package/src/components/Button/stories/Variants.vue +0 -10
- package/src/components/Popover/stories/Hover.vue +0 -14
- package/src/components/Resource.vue +0 -21
- package/src/components/Sidebar/stories/Example.vue +0 -76
- package/src/components/Spinner.vue +0 -3
- package/src/components/TabButtons/TabButtons.story.vue +0 -22
- package/src/utils/plugin.js +0 -24
- /package/src/components/Dropdown/stories/{02_Shortcuts.vue → Shortcuts.vue} +0 -0
- /package/src/components/Dropdown/stories/{01_Simple.vue → Simple.vue} +0 -0
- /package/src/components/Dropdown/stories/{03_Submenus.vue → Submenus.vue} +0 -0
- /package/src/components/Dropdown/stories/{04_Switches.vue → Switches.vue} +0 -0
- /package/src/components/{Dropdown/DropdownRenderContentAsChild.vue → Menu/MenuRenderContentAsChild.vue} +0 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<LabelingWrapper
|
|
3
|
+
:enabled="hasLabeling"
|
|
4
|
+
:wrapper-class="['space-y-1', attrs.class]"
|
|
5
|
+
:wrapper-style="attrs.style as any"
|
|
6
|
+
>
|
|
7
|
+
<InputLabel
|
|
8
|
+
v-if="props.label || $slots.label"
|
|
9
|
+
:id="labelId"
|
|
10
|
+
:label="props.label"
|
|
11
|
+
:required="props.required"
|
|
12
|
+
>
|
|
13
|
+
<template v-if="$slots.label" #default="slotProps">
|
|
14
|
+
<slot name="label" v-bind="slotProps" />
|
|
15
|
+
</template>
|
|
16
|
+
</InputLabel>
|
|
17
|
+
<div
|
|
18
|
+
ref="el"
|
|
19
|
+
class="code-editor"
|
|
20
|
+
data-slot="control"
|
|
21
|
+
:class="hasLabeling ? null : (attrs.class as any)"
|
|
22
|
+
:style="hasLabeling ? null : (attrs.style as any)"
|
|
23
|
+
v-bind="dataAttrs"
|
|
24
|
+
/>
|
|
25
|
+
<InputDescription
|
|
26
|
+
v-if="showDescription || $slots.description"
|
|
27
|
+
:id="descriptionId"
|
|
28
|
+
:description="props.description"
|
|
29
|
+
>
|
|
30
|
+
<slot v-if="$slots.description" name="description" />
|
|
31
|
+
</InputDescription>
|
|
32
|
+
<InputError v-if="hasError" :id="errorMessageId" :lines="errorLines" />
|
|
33
|
+
</LabelingWrapper>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
// Writer primitive: a CodeMirror 6 editor, nothing else (no preview logic — that
|
|
38
|
+
// lives in the sibling `CodePreview`). CodeMirror is lazy-loaded in `onMounted`
|
|
39
|
+
// via dynamic `import()` so the static graph stays editor-free and each language
|
|
40
|
+
// tree-shakes into its own async chunk — apps that never render a code field pay
|
|
41
|
+
// no runtime cost (importing `frappe-ui/code-editor` pulls in no editor code
|
|
42
|
+
// until a field actually mounts).
|
|
43
|
+
import {
|
|
44
|
+
computed,
|
|
45
|
+
onBeforeUnmount,
|
|
46
|
+
onMounted,
|
|
47
|
+
ref,
|
|
48
|
+
useAttrs,
|
|
49
|
+
useSlots,
|
|
50
|
+
watch,
|
|
51
|
+
} from 'vue'
|
|
52
|
+
import type { EditorView } from '@codemirror/view'
|
|
53
|
+
import type { Compartment, Extension } from '@codemirror/state'
|
|
54
|
+
import { useInputLabeling } from '../../composables/useInputLabeling'
|
|
55
|
+
import InputLabel from '../InputLabeling/InputLabel.vue'
|
|
56
|
+
import InputDescription from '../InputLabeling/InputDescription.vue'
|
|
57
|
+
import InputError from '../InputLabeling/InputError.vue'
|
|
58
|
+
import LabelingWrapper from '../InputLabeling/LabelingWrapper.vue'
|
|
59
|
+
import { loadLanguage } from './languages'
|
|
60
|
+
import { buildBaseTheme, buildSyntaxHighlight } from './theme'
|
|
61
|
+
import type { CodeEditorEmits, CodeEditorProps } from './types'
|
|
62
|
+
|
|
63
|
+
defineOptions({ inheritAttrs: false })
|
|
64
|
+
|
|
65
|
+
const props = withDefaults(defineProps<CodeEditorProps>(), {
|
|
66
|
+
language: 'plain',
|
|
67
|
+
disabled: false,
|
|
68
|
+
placeholder: '',
|
|
69
|
+
variant: 'subtle',
|
|
70
|
+
size: 'md',
|
|
71
|
+
})
|
|
72
|
+
const emit = defineEmits<CodeEditorEmits>()
|
|
73
|
+
defineSlots<{
|
|
74
|
+
/** Overrides the rendered label content. Receives `{ required }`. */
|
|
75
|
+
label?: (props: { required: boolean }) => any
|
|
76
|
+
/** Overrides the rendered description content. */
|
|
77
|
+
description?: () => any
|
|
78
|
+
}>()
|
|
79
|
+
|
|
80
|
+
const attrs = useAttrs()
|
|
81
|
+
const slots = useSlots()
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
labelId,
|
|
85
|
+
descriptionId,
|
|
86
|
+
errorMessageId,
|
|
87
|
+
labelledBy,
|
|
88
|
+
describedBy,
|
|
89
|
+
hasError,
|
|
90
|
+
errorLines,
|
|
91
|
+
showDescription,
|
|
92
|
+
dataAttrs,
|
|
93
|
+
} = useInputLabeling(props, {
|
|
94
|
+
size: () => props.size,
|
|
95
|
+
variant: () => props.variant,
|
|
96
|
+
disabled: () => props.disabled,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Render the labeling chrome (and its wrapping div) only when something needs
|
|
100
|
+
// it — otherwise the editor mounts bare, preserving the primitive's footprint.
|
|
101
|
+
const hasLabeling = computed(() =>
|
|
102
|
+
Boolean(
|
|
103
|
+
props.label ||
|
|
104
|
+
slots.label ||
|
|
105
|
+
showDescription.value ||
|
|
106
|
+
slots.description ||
|
|
107
|
+
hasError.value,
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
const el = ref<HTMLElement | null>(null)
|
|
112
|
+
|
|
113
|
+
let view: EditorView | null = null
|
|
114
|
+
let languageCompartment: Compartment | null = null
|
|
115
|
+
let disabledCompartment: Compartment | null = null
|
|
116
|
+
let variantCompartment: Compartment | null = null
|
|
117
|
+
let sizeCompartment: Compartment | null = null
|
|
118
|
+
let placeholderCompartment: Compartment | null = null
|
|
119
|
+
// Carries the labeling ARIA onto CodeMirror's `contentDOM` (the focusable
|
|
120
|
+
// contenteditable). Setting it on the outer wrapper would miss the element a
|
|
121
|
+
// screen reader actually lands on, so it goes through `contentAttributes`.
|
|
122
|
+
let a11yCompartment: Compartment | null = null
|
|
123
|
+
// Imported CodeMirror modules, held so the watchers can reconfigure without a
|
|
124
|
+
// second dynamic import.
|
|
125
|
+
let cmView: typeof import('@codemirror/view') | null = null
|
|
126
|
+
let cmState: typeof import('@codemirror/state') | null = null
|
|
127
|
+
// Guards the `modelValue` watcher against the editor's own `updateListener`: when
|
|
128
|
+
// we dispatch an external change (e.g. the wrapper's JSON pretty-print) we must
|
|
129
|
+
// not echo it straight back out as fresh input.
|
|
130
|
+
let syncingFromProp = false
|
|
131
|
+
// Tracks whether content overflows the height cap so the `overflow` emit only
|
|
132
|
+
// fires on transitions, not on every keystroke. The observer watches the
|
|
133
|
+
// scroller box. The cap itself is pure CSS — consumers set `--cm-max-height` on
|
|
134
|
+
// the root (e.g. `style="--cm-max-height: 13.5rem"`), which the base theme reads
|
|
135
|
+
// as the editor's `max-height` (P10: styling via CSS hooks, not a style prop).
|
|
136
|
+
// This emit is the one piece a consumer can't derive from CSS — measuring the
|
|
137
|
+
// crossing needs the ResizeObserver below — so it stays on the public surface.
|
|
138
|
+
let overflowObserver: ResizeObserver | null = null
|
|
139
|
+
let lastOverflow = false
|
|
140
|
+
// `onMounted` is async (it awaits CodeMirror's dynamic imports). If the component
|
|
141
|
+
// unmounts mid-load, `onBeforeUnmount` runs while `view` is still null and its
|
|
142
|
+
// cleanup no-ops — then the resumed `onMounted` would create an orphan view +
|
|
143
|
+
// observer that nothing destroys. This flag lets the resume bail instead.
|
|
144
|
+
let destroyed = false
|
|
145
|
+
|
|
146
|
+
// Drive the gutter's horizontal-scroll shadow (see theme.ts). Two things: flag
|
|
147
|
+
// `cm-scrolled-x` when the content is scrolled under the sticky gutter so the
|
|
148
|
+
// shadow fades in, and publish `--cm-text-height` (the bottom of the last line)
|
|
149
|
+
// so the shadow overlay is capped to the actual code and doesn't run down into
|
|
150
|
+
// the empty min-height area below it. The scroller's `scroll` event doesn't
|
|
151
|
+
// bubble, so the scroll half can't ride CodeMirror's `domEventHandlers`; it's a
|
|
152
|
+
// plain listener on `scrollDOM`. The height half is refreshed wherever geometry
|
|
153
|
+
// changes (mount, edits, reflow, size) alongside `measureOverflow`.
|
|
154
|
+
function syncGutterShadow() {
|
|
155
|
+
if (!view) return
|
|
156
|
+
view.dom.classList.toggle('cm-scrolled-x', view.scrollDOM.scrollLeft > 0)
|
|
157
|
+
const textHeight = view.lineBlockAt(view.state.doc.length).bottom
|
|
158
|
+
view.dom.style.setProperty('--cm-text-height', `${textHeight}px`)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Emit when the scroller becomes (un)scrollable — i.e. content crosses the cap.
|
|
162
|
+
function measureOverflow() {
|
|
163
|
+
if (!view) return
|
|
164
|
+
const s = view.scrollDOM
|
|
165
|
+
const overflowing = s.scrollHeight > s.clientHeight + 1
|
|
166
|
+
if (overflowing === lastOverflow) return
|
|
167
|
+
lastOverflow = overflowing
|
|
168
|
+
emit('overflow', overflowing)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build the language `Compartment` contents for a key. For `json` we also wire
|
|
172
|
+
// the lint gutter + parse linter so invalid JSON is marked inline at its
|
|
173
|
+
// position — the field contract has no error channel, so validation surfaces
|
|
174
|
+
// here rather than in the field chrome.
|
|
175
|
+
async function buildLanguageExtension(key?: string): Promise<Extension> {
|
|
176
|
+
const exts: Extension[] = []
|
|
177
|
+
const lang = await loadLanguage(key)
|
|
178
|
+
if (lang) exts.push(lang)
|
|
179
|
+
if (key === 'json') {
|
|
180
|
+
const { jsonParseLinter } = await import('@codemirror/lang-json')
|
|
181
|
+
const { linter, lintGutter } = await import('@codemirror/lint')
|
|
182
|
+
exts.push(lintGutter(), linter(jsonParseLinter()))
|
|
183
|
+
}
|
|
184
|
+
return exts
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ARIA for the `contentDOM`, derived from the labeling props. Reconfigured by a
|
|
188
|
+
// watcher when the label/description/error wiring changes.
|
|
189
|
+
function buildA11y(): Extension {
|
|
190
|
+
if (!cmView) return []
|
|
191
|
+
const a: Record<string, string> = {}
|
|
192
|
+
if (labelledBy.value) a['aria-labelledby'] = labelledBy.value
|
|
193
|
+
if (describedBy.value) a['aria-describedby'] = describedBy.value
|
|
194
|
+
if (hasError.value) {
|
|
195
|
+
a['aria-invalid'] = 'true'
|
|
196
|
+
a['aria-errormessage'] = errorMessageId.value
|
|
197
|
+
}
|
|
198
|
+
if (props.required) a['aria-required'] = 'true'
|
|
199
|
+
return cmView.EditorView.contentAttributes.of(a)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Placeholder shown when the doc is empty. Empty string → no extension (a bare
|
|
203
|
+
// `placeholder('')` would render an empty hint box). Held in a compartment so a
|
|
204
|
+
// consumer can change the text at runtime without recreating the view.
|
|
205
|
+
function buildPlaceholder(text?: string): Extension {
|
|
206
|
+
if (!cmView || !text) return []
|
|
207
|
+
return cmView.placeholder(text)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// CodeMirror side of `disabled`: make the doc read-only and the content DOM
|
|
211
|
+
// non-editable. The greyed-out surface is owned by `buildVariant` (the variant
|
|
212
|
+
// compartment is the sole source of surface chrome), so disabling reconfigures
|
|
213
|
+
// both compartments — see the `disabled` watcher.
|
|
214
|
+
function buildDisabled(disabled: boolean): Extension {
|
|
215
|
+
if (!cmState || !cmView) return []
|
|
216
|
+
return [
|
|
217
|
+
cmState.EditorState.readOnly.of(disabled),
|
|
218
|
+
cmView.EditorView.editable.of(!disabled),
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Per-variant surface (background + border + focus treatment), mirroring
|
|
223
|
+
// frappe-ui's `subtle | outline` input variants so a code field sits
|
|
224
|
+
// flush with the TextInput/Textarea fields around it (`ghost` is intentionally
|
|
225
|
+
// not offered — a borderless code surface loses its editable affordance). The
|
|
226
|
+
// base `theme` owns
|
|
227
|
+
// everything else (typography, radius, gutters, tooltips) and deliberately does
|
|
228
|
+
// NOT set bg/border, so these fragments are the sole source of surface chrome —
|
|
229
|
+
// no precedence fight. Added after `theme` in the extensions array, so they win
|
|
230
|
+
// over `basicSetup`'s defaults the same way `theme` already does.
|
|
231
|
+
function buildVariant(v?: string, disabled?: boolean): Extension {
|
|
232
|
+
if (!cmView) return []
|
|
233
|
+
// Disabled surface, mirroring frappe-ui's TextInput/Textarea `disabled`
|
|
234
|
+
// variant: a flat greyed box with no hover/focus chrome, content dimmed so the
|
|
235
|
+
// whole field (including syntax-highlighted tokens) reads as inert. Keeps the
|
|
236
|
+
// base variant's border treatment (`outline` stays bordered, `subtle` goes
|
|
237
|
+
// transparent) so a disabled field keeps the footprint of its enabled self.
|
|
238
|
+
if (disabled) {
|
|
239
|
+
return cmView.EditorView.theme({
|
|
240
|
+
'&': {
|
|
241
|
+
backgroundColor: 'var(--surface-gray-1, #f8fafc)',
|
|
242
|
+
border:
|
|
243
|
+
v === 'outline'
|
|
244
|
+
? '1px solid var(--outline-gray-2, #e2e8f0)'
|
|
245
|
+
: '1px solid transparent',
|
|
246
|
+
},
|
|
247
|
+
// Gutter shares the disabled surface so scrolled content can't bleed
|
|
248
|
+
// through behind it (the gutter is sticky over the scroller).
|
|
249
|
+
'.cm-gutters': { backgroundColor: 'var(--surface-gray-1, #f8fafc)' },
|
|
250
|
+
'.cm-content': { opacity: '0.6' },
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
const ring = '0 0 0 2px var(--outline-gray-3, #cbd5e1)'
|
|
254
|
+
// Each variant paints `.cm-gutters` to match its `&` surface in the same
|
|
255
|
+
// state — the gutter is sticky over the scroller, so a transparent one would
|
|
256
|
+
// let horizontally-scrolled code bleed through behind the line numbers.
|
|
257
|
+
const fragments: Record<string, Record<string, Record<string, string>>> = {
|
|
258
|
+
subtle: {
|
|
259
|
+
'&': {
|
|
260
|
+
backgroundColor: 'var(--surface-gray-2, #f1f5f9)',
|
|
261
|
+
border: '1px solid var(--surface-gray-2, #e2e8f0)',
|
|
262
|
+
},
|
|
263
|
+
'.cm-gutters': { backgroundColor: 'var(--surface-gray-2, #f1f5f9)' },
|
|
264
|
+
// Hover mirrors frappe-ui's TextInput/Textarea `subtle` variant
|
|
265
|
+
// (`hover:border-outline-elevation-2 hover:bg-surface-gray-3`) so a code
|
|
266
|
+
// field reacts to hover the same way the inputs around it do. Declared
|
|
267
|
+
// before `&.cm-focused` so the focused surface wins when both apply.
|
|
268
|
+
'&:hover': {
|
|
269
|
+
backgroundColor: 'var(--surface-gray-3, #e2e8f0)',
|
|
270
|
+
borderColor: 'var(--outline-elevation-2, #94a3b8)',
|
|
271
|
+
},
|
|
272
|
+
'&:hover .cm-gutters': {
|
|
273
|
+
backgroundColor: 'var(--surface-gray-3, #e2e8f0)',
|
|
274
|
+
},
|
|
275
|
+
'&.cm-focused': {
|
|
276
|
+
backgroundColor: 'var(--surface-base, white)',
|
|
277
|
+
borderColor: 'var(--outline-gray-4, #94a3b8)',
|
|
278
|
+
boxShadow: ring,
|
|
279
|
+
},
|
|
280
|
+
'&.cm-focused .cm-gutters': {
|
|
281
|
+
backgroundColor: 'var(--surface-base, white)',
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
outline: {
|
|
285
|
+
'&': {
|
|
286
|
+
backgroundColor: 'var(--surface-base, white)',
|
|
287
|
+
border: '1px solid var(--outline-gray-2, #e2e8f0)',
|
|
288
|
+
},
|
|
289
|
+
'.cm-gutters': { backgroundColor: 'var(--surface-base, white)' },
|
|
290
|
+
'&.cm-focused': {
|
|
291
|
+
borderColor: 'var(--outline-gray-4, #94a3b8)',
|
|
292
|
+
boxShadow: ring,
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
return cmView.EditorView.theme(fragments[v ?? 'subtle'] ?? fragments.subtle)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Per-size font scale + content min-height, mirroring frappe-ui's `sm|md|lg|xl`
|
|
300
|
+
// input sizes. This compartment is the SOLE source of `&` font size and
|
|
301
|
+
// `.cm-content` min-height — the base `theme` deliberately omits both so there's
|
|
302
|
+
// no equal-specificity fight whose winner depends on StyleModule order.
|
|
303
|
+
function buildSize(s?: string): Extension {
|
|
304
|
+
if (!cmView) return []
|
|
305
|
+
const sizes: Record<string, { fontSize: string; minHeight: string }> = {
|
|
306
|
+
sm: { fontSize: '12px', minHeight: '3.5rem' },
|
|
307
|
+
md: { fontSize: '13px', minHeight: '4.5rem' },
|
|
308
|
+
lg: { fontSize: '14px', minHeight: '6rem' },
|
|
309
|
+
xl: { fontSize: '16px', minHeight: '7.5rem' },
|
|
310
|
+
}
|
|
311
|
+
const cfg = sizes[s ?? 'md'] ?? sizes.md
|
|
312
|
+
return cmView.EditorView.theme({
|
|
313
|
+
'&': { fontSize: cfg.fontSize },
|
|
314
|
+
'.cm-content': { minHeight: cfg.minHeight },
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
onMounted(async () => {
|
|
319
|
+
// SSR guard — no editor without a DOM.
|
|
320
|
+
if (typeof document === 'undefined' || !el.value) return
|
|
321
|
+
|
|
322
|
+
cmView = await import('@codemirror/view')
|
|
323
|
+
cmState = await import('@codemirror/state')
|
|
324
|
+
const { basicSetup } = await import('codemirror')
|
|
325
|
+
// `basicSetup` leaves Tab as a focus-move (CodeMirror's a11y default); bind it
|
|
326
|
+
// to indent/dedent so Tab behaves like an editor, not a form field. Escape
|
|
327
|
+
// still releases focus, so keyboard users can tab out via Esc-then-Tab.
|
|
328
|
+
const { indentWithTab } = await import('@codemirror/commands')
|
|
329
|
+
const { EditorView } = cmView
|
|
330
|
+
const { Compartment } = cmState
|
|
331
|
+
|
|
332
|
+
languageCompartment = new Compartment()
|
|
333
|
+
disabledCompartment = new Compartment()
|
|
334
|
+
variantCompartment = new Compartment()
|
|
335
|
+
sizeCompartment = new Compartment()
|
|
336
|
+
placeholderCompartment = new Compartment()
|
|
337
|
+
a11yCompartment = new Compartment()
|
|
338
|
+
|
|
339
|
+
// Base chrome theme + token-keyed syntax highlighting live in `./theme` (the
|
|
340
|
+
// ~200 lines of style literals would otherwise drown the editor logic). Both
|
|
341
|
+
// take the already-imported CodeMirror modules so no editor code leaks into the
|
|
342
|
+
// static graph.
|
|
343
|
+
const theme = buildBaseTheme(cmView)
|
|
344
|
+
const highlight = await buildSyntaxHighlight(cmState)
|
|
345
|
+
|
|
346
|
+
const updateListener = EditorView.updateListener.of((update) => {
|
|
347
|
+
if (update.docChanged && !syncingFromProp)
|
|
348
|
+
emit('update:modelValue', update.state.doc.toString())
|
|
349
|
+
// Content height may have changed — re-check the cap and the scroll-shadow
|
|
350
|
+
// overlay. A doc edit changes height, but so does folding/unfolding (a
|
|
351
|
+
// view-state effect, not a doc change) and any reflow. The capped/min-height
|
|
352
|
+
// scroller can keep its box size constant while content height shifts, so the
|
|
353
|
+
// ResizeObserver never fires for those — `geometryChanged` is what catches
|
|
354
|
+
// them (folding latching a stale Expand button was the bug this closes).
|
|
355
|
+
if (update.docChanged || update.geometryChanged) {
|
|
356
|
+
measureOverflow()
|
|
357
|
+
syncGutterShadow()
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Blur is the commit point (mirrors the typed-field contract).
|
|
362
|
+
const blurHandler = EditorView.domEventHandlers({
|
|
363
|
+
blur: () => {
|
|
364
|
+
if (view) emit('change', view.state.doc.toString())
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
// Escape releases focus. `indentWithTab` rebinds Tab to indent, which would
|
|
369
|
+
// otherwise trap keyboard users inside the editor with no way out (WCAG
|
|
370
|
+
// 2.1.2). Escape blurs the content DOM so they can Tab on to the next field —
|
|
371
|
+
// Esc-then-Tab, the standard CodeMirror keyboard-trap escape hatch.
|
|
372
|
+
const escapeHandler = EditorView.domEventHandlers({
|
|
373
|
+
keydown: (e, v) => {
|
|
374
|
+
if (e.key === 'Escape') {
|
|
375
|
+
v.contentDOM.blur()
|
|
376
|
+
return true
|
|
377
|
+
}
|
|
378
|
+
return false
|
|
379
|
+
},
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
const extensions: Extension[] = [
|
|
383
|
+
basicSetup,
|
|
384
|
+
cmView.keymap.of([indentWithTab]),
|
|
385
|
+
escapeHandler,
|
|
386
|
+
languageCompartment.of(await buildLanguageExtension(props.language)),
|
|
387
|
+
disabledCompartment.of(buildDisabled(!!props.disabled)),
|
|
388
|
+
a11yCompartment.of(buildA11y()),
|
|
389
|
+
updateListener,
|
|
390
|
+
blurHandler,
|
|
391
|
+
theme,
|
|
392
|
+
variantCompartment.of(buildVariant(props.variant, props.disabled)),
|
|
393
|
+
sizeCompartment.of(buildSize(props.size)),
|
|
394
|
+
placeholderCompartment.of(buildPlaceholder(props.placeholder)),
|
|
395
|
+
highlight,
|
|
396
|
+
]
|
|
397
|
+
|
|
398
|
+
// Bail if the component unmounted while the dynamic imports above were in
|
|
399
|
+
// flight — otherwise we'd create a view + observer that `onBeforeUnmount`
|
|
400
|
+
// already ran past and nothing would ever destroy.
|
|
401
|
+
if (destroyed || !el.value) return
|
|
402
|
+
|
|
403
|
+
view = new EditorView({
|
|
404
|
+
doc: props.modelValue ?? '',
|
|
405
|
+
extensions,
|
|
406
|
+
parent: el.value,
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
measureOverflow()
|
|
410
|
+
// Shadow the gutter while the content is scrolled horizontally. Listen on the
|
|
411
|
+
// scroller (the event doesn't bubble) and seed the initial state.
|
|
412
|
+
view.scrollDOM.addEventListener('scroll', syncGutterShadow, { passive: true })
|
|
413
|
+
syncGutterShadow()
|
|
414
|
+
// Catches overflow changes the doc-edit path misses: wrapping reflow on width
|
|
415
|
+
// change, and the cap being applied/removed (a consumer toggling
|
|
416
|
+
// `--cm-max-height` resizes the scroller, which fires this). Fires once on
|
|
417
|
+
// observe for the initial measurement.
|
|
418
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
419
|
+
overflowObserver = new ResizeObserver(() => {
|
|
420
|
+
measureOverflow()
|
|
421
|
+
syncGutterShadow()
|
|
422
|
+
})
|
|
423
|
+
overflowObserver.observe(view.scrollDOM)
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// Sync external `modelValue` changes into the view. This is also what makes the
|
|
428
|
+
// wrapper's JSON pretty-print visible: when it rewrites the value on commit, this
|
|
429
|
+
// dispatches the reformatted string back into the editor.
|
|
430
|
+
//
|
|
431
|
+
// Rather than replacing the whole doc (which would reset the caret to the end,
|
|
432
|
+
// collapse the selection, jump the scroll, and abort IME composition), we diff
|
|
433
|
+
// off the common prefix/suffix and change only the differing middle. CodeMirror
|
|
434
|
+
// then maps the existing selection through that change, so an unchanged caret
|
|
435
|
+
// stays put — important when a consumer transforms the value live, not just on
|
|
436
|
+
// the unfocused blur/pretty-print path.
|
|
437
|
+
watch(
|
|
438
|
+
() => props.modelValue,
|
|
439
|
+
(val) => {
|
|
440
|
+
if (!view) return
|
|
441
|
+
const current = view.state.doc.toString()
|
|
442
|
+
const next = val ?? ''
|
|
443
|
+
if (next === current) return
|
|
444
|
+
|
|
445
|
+
// Longest common prefix.
|
|
446
|
+
let from = 0
|
|
447
|
+
const max = Math.min(current.length, next.length)
|
|
448
|
+
while (from < max && current[from] === next[from]) from++
|
|
449
|
+
// Longest common suffix (not overlapping the prefix on either side).
|
|
450
|
+
let curEnd = current.length
|
|
451
|
+
let nextEnd = next.length
|
|
452
|
+
while (
|
|
453
|
+
curEnd > from &&
|
|
454
|
+
nextEnd > from &&
|
|
455
|
+
current[curEnd - 1] === next[nextEnd - 1]
|
|
456
|
+
) {
|
|
457
|
+
curEnd--
|
|
458
|
+
nextEnd--
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
syncingFromProp = true
|
|
462
|
+
view.dispatch({
|
|
463
|
+
changes: { from, to: curEnd, insert: next.slice(from, nextEnd) },
|
|
464
|
+
})
|
|
465
|
+
syncingFromProp = false
|
|
466
|
+
},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
// Swap the language (and JSON linter) without recreating the view. The extension
|
|
470
|
+
// build is async (it dynamic-imports the `@codemirror/lang-*` package), so a fast
|
|
471
|
+
// A→B switch races two builds; the monotonic `langSeq` token drops a stale build
|
|
472
|
+
// whose import resolves after a newer one, so the latest `props.language` always
|
|
473
|
+
// wins regardless of import-resolution order.
|
|
474
|
+
let langSeq = 0
|
|
475
|
+
watch(
|
|
476
|
+
() => props.language,
|
|
477
|
+
async (key) => {
|
|
478
|
+
const seq = ++langSeq
|
|
479
|
+
const ext = await buildLanguageExtension(key)
|
|
480
|
+
if (!view || !languageCompartment || seq !== langSeq) return
|
|
481
|
+
view.dispatch({
|
|
482
|
+
effects: languageCompartment.reconfigure(ext),
|
|
483
|
+
})
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
// Toggle disabled without recreating the view: reconfigure the read-only/editable
|
|
488
|
+
// extension and re-derive the surface (the variant compartment owns the greyed
|
|
489
|
+
// disabled chrome) in a single dispatch.
|
|
490
|
+
watch(
|
|
491
|
+
() => props.disabled,
|
|
492
|
+
(disabled) => {
|
|
493
|
+
if (!view || !disabledCompartment || !variantCompartment) return
|
|
494
|
+
view.dispatch({
|
|
495
|
+
effects: [
|
|
496
|
+
disabledCompartment.reconfigure(buildDisabled(!!disabled)),
|
|
497
|
+
variantCompartment.reconfigure(buildVariant(props.variant, disabled)),
|
|
498
|
+
],
|
|
499
|
+
})
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
// Swap the surface variant without recreating the view.
|
|
504
|
+
watch(
|
|
505
|
+
() => props.variant,
|
|
506
|
+
(v) => {
|
|
507
|
+
if (!view || !variantCompartment) return
|
|
508
|
+
view.dispatch({
|
|
509
|
+
effects: variantCompartment.reconfigure(buildVariant(v, props.disabled)),
|
|
510
|
+
})
|
|
511
|
+
},
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
// Swap the size scale without recreating the view, then re-measure overflow
|
|
515
|
+
// (the new min-height/font size can change whether content overflows).
|
|
516
|
+
watch(
|
|
517
|
+
() => props.size,
|
|
518
|
+
(s) => {
|
|
519
|
+
if (!view || !sizeCompartment) return
|
|
520
|
+
view.dispatch({ effects: sizeCompartment.reconfigure(buildSize(s)) })
|
|
521
|
+
measureOverflow()
|
|
522
|
+
syncGutterShadow()
|
|
523
|
+
},
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
// Swap the placeholder text without recreating the view.
|
|
527
|
+
watch(
|
|
528
|
+
() => props.placeholder,
|
|
529
|
+
(text) => {
|
|
530
|
+
if (!view || !placeholderCompartment) return
|
|
531
|
+
view.dispatch({
|
|
532
|
+
effects: placeholderCompartment.reconfigure(buildPlaceholder(text)),
|
|
533
|
+
})
|
|
534
|
+
},
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
// Re-apply ARIA when the label/description/error wiring changes.
|
|
538
|
+
watch(
|
|
539
|
+
() => [labelledBy.value, describedBy.value, hasError.value, props.required],
|
|
540
|
+
() => {
|
|
541
|
+
if (!view || !a11yCompartment) return
|
|
542
|
+
view.dispatch({ effects: a11yCompartment.reconfigure(buildA11y()) })
|
|
543
|
+
},
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
onBeforeUnmount(() => {
|
|
547
|
+
destroyed = true
|
|
548
|
+
overflowObserver?.disconnect()
|
|
549
|
+
overflowObserver = null
|
|
550
|
+
if (view) {
|
|
551
|
+
view.scrollDOM.removeEventListener('scroll', syncGutterShadow)
|
|
552
|
+
view.destroy()
|
|
553
|
+
view = null
|
|
554
|
+
}
|
|
555
|
+
})
|
|
556
|
+
</script>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*
|
|
4
|
+
* CodePreview is the sanitize-before-render half of the pair, pure enough to
|
|
5
|
+
* mount in jsdom. The CodeEditor writer lazy-loads CodeMirror and measures
|
|
6
|
+
* layout on mount (which jsdom can't render), so its behaviour is covered in
|
|
7
|
+
* CodeEditor.cy.ts instead.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest'
|
|
10
|
+
import { createApp } from 'vue'
|
|
11
|
+
import CodePreview from './CodePreview.vue'
|
|
12
|
+
|
|
13
|
+
describe('CodePreview', () => {
|
|
14
|
+
it('renders sanitized markdown', () => {
|
|
15
|
+
const host = document.createElement('div')
|
|
16
|
+
const app = createApp(CodePreview, {
|
|
17
|
+
modelValue: '# Title',
|
|
18
|
+
language: 'markdown',
|
|
19
|
+
})
|
|
20
|
+
app.mount(host)
|
|
21
|
+
expect(host.innerHTML).toContain('<h1')
|
|
22
|
+
expect(host.textContent).toContain('Title')
|
|
23
|
+
app.unmount()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders sanitized html', () => {
|
|
27
|
+
const host = document.createElement('div')
|
|
28
|
+
const app = createApp(CodePreview, {
|
|
29
|
+
modelValue: '<p>hi</p><script>alert(1)</script>',
|
|
30
|
+
language: 'html',
|
|
31
|
+
})
|
|
32
|
+
app.mount(host)
|
|
33
|
+
expect(host.innerHTML).toContain('<p>hi</p>')
|
|
34
|
+
// The script tag is stripped by DOMPurify.
|
|
35
|
+
expect(host.innerHTML).not.toContain('alert(1)')
|
|
36
|
+
app.unmount()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders nothing for non-preview languages', () => {
|
|
40
|
+
const host = document.createElement('div')
|
|
41
|
+
const app = createApp(CodePreview, {
|
|
42
|
+
modelValue: 'const a = 1;',
|
|
43
|
+
language: 'javascript',
|
|
44
|
+
})
|
|
45
|
+
app.mount(host)
|
|
46
|
+
expect(host.textContent).toBe('')
|
|
47
|
+
app.unmount()
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Display-only, sanitized render. Mirrors the sanitize-before-render
|
|
3
|
+
convention (DOMPurify is a frappe-ui dep). The container always renders for
|
|
4
|
+
a preview-capable language (even when empty) so the consumer's
|
|
5
|
+
min-height/border holds instead of collapsing; non-preview languages render
|
|
6
|
+
nothing. -->
|
|
7
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
8
|
+
<div
|
|
9
|
+
v-if="isPreviewLanguage"
|
|
10
|
+
class="prose prose-sm max-w-none text-ink-gray-8"
|
|
11
|
+
v-html="sanitized"
|
|
12
|
+
/>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
// Preview primitive — separate from the writer. Renders read-only sanitized
|
|
17
|
+
// output for the two languages that have a meaningful preview:
|
|
18
|
+
// markdown → marked() → DOMPurify.sanitize → v-html
|
|
19
|
+
// html → DOMPurify.sanitize → v-html
|
|
20
|
+
// Any other language renders nothing (the wrapper only mounts this for fieldtypes
|
|
21
|
+
// that want a preview). No emit — display only.
|
|
22
|
+
import { computed } from 'vue'
|
|
23
|
+
import DOMPurify from 'dompurify'
|
|
24
|
+
import { marked } from 'marked'
|
|
25
|
+
import type { CodePreviewProps } from './types'
|
|
26
|
+
|
|
27
|
+
const props = defineProps<CodePreviewProps>()
|
|
28
|
+
|
|
29
|
+
const isPreviewLanguage = computed(
|
|
30
|
+
() => props.language === 'markdown' || props.language === 'html',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const sanitized = computed(() => {
|
|
34
|
+
const src = props.modelValue ?? ''
|
|
35
|
+
if (!src) return ''
|
|
36
|
+
if (props.language === 'markdown') {
|
|
37
|
+
// `marked.parse` is sync for string input (no async extensions here).
|
|
38
|
+
return DOMPurify.sanitize(marked.parse(src) as string)
|
|
39
|
+
}
|
|
40
|
+
if (props.language === 'html') {
|
|
41
|
+
return DOMPurify.sanitize(src)
|
|
42
|
+
}
|
|
43
|
+
return ''
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Code-editor primitives: a CodeMirror 6 writer (`CodeEditor`) and a sanitized
|
|
2
|
+
// read-only `CodePreview`, kept as two independent pieces. CodeMirror is
|
|
3
|
+
// lazy-loaded inside `CodeEditor`, so importing this barrel pulls in no editor
|
|
4
|
+
// code until a field actually mounts.
|
|
5
|
+
//
|
|
6
|
+
// Published from frappe-ui under its own `frappe-ui/code-editor` subpath (not the
|
|
7
|
+
// root barrel) so the CodeMirror dependency only loads for apps that opt in —
|
|
8
|
+
// the same isolation pattern as `frappe-ui/editor`.
|
|
9
|
+
export { default as CodeEditor } from './CodeEditor.vue'
|
|
10
|
+
export { default as CodePreview } from './CodePreview.vue'
|
|
11
|
+
export { loadLanguage } from './languages'
|
|
12
|
+
export type {
|
|
13
|
+
CodeLanguage,
|
|
14
|
+
CodeEditorProps,
|
|
15
|
+
CodeEditorEmits,
|
|
16
|
+
CodePreviewProps,
|
|
17
|
+
} from './types'
|