├── src ├── js │ ├── helpers │ │ ├── types.ts │ │ ├── apexcharts │ │ │ └── interfaces.ts │ │ └── clipboard │ │ │ └── index.ts │ ├── spa │ │ ├── types.ts │ │ └── interfaces.ts │ ├── static │ │ ├── types.ts │ │ └── interfaces.ts │ ├── utils │ │ ├── types.ts │ │ └── interfaces.ts │ ├── plugins │ │ ├── collapse │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── combobox │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── dropdown │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── select │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── stepper │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── tabs │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── accordion │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── base-plugin │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── index.ts │ │ ├── copy-markup │ │ │ ├── types.ts │ │ │ └── interfaces.ts │ │ ├── datatable │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── file-upload │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── input-number │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── pin-input │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── scrollspy │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── toggle-count │ │ │ ├── types.ts │ │ │ └── interfaces.ts │ │ ├── tree-view │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── remove-element │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── strong-password │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── toggle-password │ │ │ ├── types.ts │ │ │ └── interfaces.ts │ │ ├── accessibility-manager │ │ │ ├── types.ts │ │ │ └── interfaces.ts │ │ ├── tooltip │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ ├── carousel │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ │ ├── overlay │ │ │ ├── types.ts │ │ │ ├── interfaces.ts │ │ │ └── variants.css │ │ └── range-slider │ │ │ ├── types.ts │ │ │ ├── variants.css │ │ │ └── interfaces.ts │ ├── types.ts │ ├── interfaces.ts │ ├── constants.ts │ └── index.ts ├── components │ ├── collapse.css │ ├── navbar.css │ ├── label.css │ ├── footer.css │ ├── carousel.css │ ├── skeleton.css │ ├── accordion.css │ ├── breadcrumbs.css │ ├── filter.css │ ├── kbd.css │ ├── status.css │ ├── customOptions.css │ ├── drawer.css │ ├── fileinput.css │ ├── dropdown.css │ ├── indicator.css │ ├── stat.css │ ├── radialprogress.css │ ├── tooltip.css │ ├── diff.css │ ├── alert.css │ ├── pinInput.css │ ├── link.css │ ├── modal.css │ ├── swap.css │ ├── divider.css │ ├── chat.css │ ├── progress.css │ ├── badge.css │ ├── avatar.css │ ├── stack.css │ └── mockup.css ├── base │ ├── custombase.css │ ├── scrollbar.css │ ├── properties.css │ ├── rootcolor.css │ ├── svg.css │ └── reset.css ├── vendor │ ├── datatables.css │ ├── apexcharts.css │ ├── raty.css │ ├── waves.css │ └── notyf.css ├── utilities │ ├── glass.css │ ├── typography.css │ └── radius.css └── themes │ ├── claude.css │ ├── marshmallow.css │ ├── pastel.css │ ├── perplexity.css │ ├── spotify.css │ ├── vscode.css │ ├── black.css │ ├── slack.css │ ├── valorant.css │ ├── ghibli.css │ ├── mintlify.css │ ├── gourmet.css │ ├── light.css │ ├── shadcn.css │ ├── dark.css │ ├── corporate.css │ ├── luxury.css │ └── soft.css ├── .github ├── FUNDING.yml ├── workflows │ ├── issue-reply.hbs │ ├── handle-issue.yml │ ├── write-release-notes.yml │ └── release-new-version.yml └── ISSUE_TEMPLATE │ ├── config.yml │ ├── documentation-issue.yml │ └── bug-report.yml ├── .prettierignore ├── functions ├── themeOrder.d.ts ├── breakpoints.js ├── themeOrder.js ├── plugin.js ├── createDirectoryBasedOnFileNames.js ├── replaceApplyTrueWithEmptyObject.js ├── copyFile.js ├── removeFiles.js ├── getDirectoriesWithTargetFile.js ├── variables.css ├── getFileNames.js ├── variables.d.ts ├── cleanCss.js ├── createPluginFiles.js ├── generatePlugins.js ├── generateThemeFiles.js ├── variables.js ├── minify.js ├── generateThemes.js ├── themePlugin.js ├── compileAndExtractStyles.js ├── copyFile.test.js ├── generateChunks.js ├── cssToJs.js ├── cleanCss.test.js ├── generateThemesObject.js ├── generateImports.js ├── themes.test.js ├── generateChunks.test.js ├── validatecss.test.js ├── plugin.test.js ├── contrast.test.js ├── createDirectoryBasedOnFileNames.test.js ├── extractClasses.js ├── packCss.js ├── pluginOptionsHandler.js ├── extractClasses.test.js └── filesExist.test.js ├── .editorconfig ├── tsconfig.mjs.json ├── tsconfig.json ├── .prettierrc ├── dts-config.cjs ├── .gitignore ├── variants.css ├── index.js ├── webpack.config.cjs ├── webpack.config.mjs.cjs └── CONTRIBUTING.md /src/js/helpers/types.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/spa/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/static/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/utils/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: themeselection 2 | -------------------------------------------------------------------------------- /src/js/plugins/collapse/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/combobox/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/dropdown/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/select/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/stepper/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/tabs/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/accordion/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/base-plugin/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/copy-markup/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/datatable/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/file-upload/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/input-number/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/pin-input/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/scrollspy/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/toggle-count/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/tree-view/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | CHANGELOG.md 3 | dist 4 | -------------------------------------------------------------------------------- /src/js/plugins/remove-element/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/strong-password/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/toggle-password/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/accessibility-manager/types.ts: -------------------------------------------------------------------------------- 1 | // no types 2 | -------------------------------------------------------------------------------- /src/js/plugins/tooltip/types.ts: -------------------------------------------------------------------------------- 1 | export type TTooltipOptionsScope = 'parent' | 'window' 2 | -------------------------------------------------------------------------------- /functions/themeOrder.d.ts: -------------------------------------------------------------------------------- 1 | declare const themeOrder: readonly string[] 2 | export default themeOrder 3 | -------------------------------------------------------------------------------- /src/components/collapse.css: -------------------------------------------------------------------------------- 1 | .collapse:not(td):not(tr):not(colgroup) { 2 | @apply visible; 3 | } 4 | -------------------------------------------------------------------------------- /src/js/types.ts: -------------------------------------------------------------------------------- 1 | export interface ICollectionItem { 2 | id: string | number 3 | element: T 4 | } 5 | -------------------------------------------------------------------------------- /src/js/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICollectionItem { 2 | id: string | number 3 | element: T 4 | } 5 | -------------------------------------------------------------------------------- /src/js/plugins/carousel/types.ts: -------------------------------------------------------------------------------- 1 | export type TCarouselOptionsSlidesQty = { 2 | [key: string]: number 3 | } 4 | -------------------------------------------------------------------------------- /src/js/plugins/overlay/types.ts: -------------------------------------------------------------------------------- 1 | export type TOverlayOptionsAutoCloseEqualityType = 'less-than' | 'more-than' 2 | -------------------------------------------------------------------------------- /src/base/custombase.css: -------------------------------------------------------------------------------- 1 | button:not(:disabled), 2 | [role="button"]:not(:disabled) { 3 | cursor: pointer; 4 | } 5 | -------------------------------------------------------------------------------- /src/base/scrollbar.css: -------------------------------------------------------------------------------- 1 | :root { 2 | scrollbar-color: color-mix(in oklch, currentColor 20%, #0000) #0000; 3 | } 4 | -------------------------------------------------------------------------------- /src/js/plugins/remove-element/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant removing { 2 | &.removing { 3 | @slot; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/js/plugins/scrollspy/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant scrollspy-active { 2 | &.active { 3 | @slot; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/js/plugins/base-plugin/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IBasePlugin { 2 | el: E 3 | options?: O 4 | events?: {} 5 | } 6 | -------------------------------------------------------------------------------- /src/base/properties.css: -------------------------------------------------------------------------------- 1 | @property --radialprogress { 2 | syntax: ""; 3 | inherits: true; 4 | initial-value: 0%; 5 | } 6 | -------------------------------------------------------------------------------- /src/js/plugins/range-slider/types.ts: -------------------------------------------------------------------------------- 1 | export type TRangeSliderOptionsFormatterType = 'integer' | 'thousandsSeparatorAndDecimalPoints' | null 2 | -------------------------------------------------------------------------------- /functions/breakpoints.js: -------------------------------------------------------------------------------- 1 | export default { 2 | sm: '640px', 3 | md: '768px', 4 | lg: '1024px', 5 | xl: '1280px', 6 | '2xl': '1536px' 7 | } 8 | -------------------------------------------------------------------------------- /src/js/plugins/tooltip/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ITooltip { 2 | options?: {} 3 | 4 | show(): void 5 | hide(): void 6 | destroy(): void 7 | } 8 | -------------------------------------------------------------------------------- /src/base/rootcolor.css: -------------------------------------------------------------------------------- 1 | :root, 2 | [data-theme] { 3 | background-color: var(--root-bg, var(--color-base-100)); 4 | color: var(--color-base-content); 5 | } 6 | -------------------------------------------------------------------------------- /src/js/plugins/collapse/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICollapse { 2 | options?: {} 3 | 4 | show(): void 5 | hide(): void 6 | destroy(): void 7 | } 8 | -------------------------------------------------------------------------------- /src/js/spa/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ISpaCollectionItem { 2 | key: string 3 | fn: { 4 | autoInit: () => void 5 | } 6 | collection: string 7 | } 8 | -------------------------------------------------------------------------------- /src/js/plugins/pin-input/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant pin-input-active { 2 | &.active { 3 | @slot; 4 | } 5 | 6 | .active & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/file-upload/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant file-upload-complete { 2 | &.complete { 3 | @slot; 4 | } 5 | 6 | .complete & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/tabs/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant active-tab { 2 | &[data-tab].active { 3 | @slot; 4 | } 5 | 6 | [data-tab].active & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/input-number/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant input-number-disabled { 2 | &.disabled { 3 | @slot; 4 | } 5 | 6 | .disabled & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/range-slider/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant range-slider-disabled { 2 | &.disabled { 3 | @slot; 4 | } 5 | 6 | .disabled & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/tooltip/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant tooltip-shown { 2 | &.tooltip-content.show { 3 | @slot; 4 | } 5 | 6 | .tooltip.show & { 7 | @slot; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/vendor/datatables.css: -------------------------------------------------------------------------------- 1 | .dt-layout-row:has(.dt-search), 2 | .dt-layout-row:has(.dt-length), 3 | .dt-layout-row:has(.dt-paging), 4 | .dt-scroll-body thead { 5 | display: none !important; 6 | } 7 | -------------------------------------------------------------------------------- /src/js/utils/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IMenuSearchHistory { 2 | historyIndex: number 3 | 4 | addHistory(index: number): void 5 | existsInHistory(index: number): boolean 6 | clearHistory(): void 7 | } 8 | -------------------------------------------------------------------------------- /src/js/plugins/pin-input/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IPinInputOptions { 2 | availableCharsRE?: RegExp 3 | } 4 | 5 | export interface IPinInput { 6 | options?: IPinInputOptions 7 | 8 | destroy(): void 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/scrollspy/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IScrollspyOptions { 2 | ignoreScrollUp?: boolean 3 | } 4 | 5 | export interface IScrollspy { 6 | options?: IScrollspyOptions 7 | 8 | destroy(): void 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/remove-element/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IRemoveElementOptions { 2 | removeTargetAnimationClass: string 3 | } 4 | 5 | export interface IRemoveElement { 6 | options?: IRemoveElementOptions 7 | 8 | destroy(): void 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/input-number/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IInputNumberOptions { 2 | min?: number 3 | max?: number 4 | step?: number 5 | } 6 | 7 | export interface IInputNumber { 8 | options?: IInputNumberOptions 9 | 10 | destroy(): void 11 | } 12 | -------------------------------------------------------------------------------- /src/components/navbar.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | @apply bg-base-100 flex w-full items-center px-6 py-3; 3 | } 4 | 5 | .navbar-start { 6 | @apply flex w-1/2 justify-start; 7 | } 8 | .navbar-center { 9 | @apply shrink-0; 10 | } 11 | .navbar-end { 12 | @apply flex w-1/2 justify-end; 13 | } 14 | -------------------------------------------------------------------------------- /src/js/plugins/collapse/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant collapse-open { 2 | &.collapse.open { 3 | @slot; 4 | } 5 | 6 | &.collapse-toggle.open { 7 | @slot; 8 | } 9 | 10 | .collapse.open > & { 11 | @slot; 12 | } 13 | 14 | .collapse-toggle.open > & { 15 | @slot; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/plugins/dropdown/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IDropdown { 2 | options?: {} 3 | 4 | open(): void 5 | close(isAnimated: boolean): void 6 | forceClearState(): void 7 | destroy(): void 8 | } 9 | 10 | export interface IHTMLElementFloatingUI extends HTMLElement { 11 | _floatingUI: any 12 | } 13 | -------------------------------------------------------------------------------- /src/js/static/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IStaticMethods { 2 | getClassProperty(el: HTMLElement, prop?: string, val?: string): string 3 | afterTransition(el: HTMLElement, cb: Function): void 4 | autoInit(collection?: string | string[]): void 5 | cleanCollection(collection?: string | string[]): void 6 | } 7 | -------------------------------------------------------------------------------- /src/js/plugins/copy-markup/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICopyMarkupOptions { 2 | targetSelector: string 3 | wrapperSelector: string 4 | limit?: number 5 | } 6 | 7 | export interface ICopyMarkup { 8 | options?: ICopyMarkupOptions 9 | 10 | delete(target: HTMLElement): void 11 | destroy(): void 12 | } 13 | -------------------------------------------------------------------------------- /src/js/plugins/toggle-password/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ITogglePasswordOptions { 2 | target: string | string[] | HTMLInputElement | HTMLInputElement[] 3 | } 4 | 5 | export interface ITogglePassword { 6 | options?: ITogglePasswordOptions 7 | 8 | show(): void 9 | hide(): void 10 | destroy(): void 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /functions/themeOrder.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'light', 3 | 'dark', 4 | 'black', 5 | 'claude', 6 | 'corporate', 7 | 'ghibli', 8 | 'gourmet', 9 | 'luxury', 10 | 'mintlify', 11 | 'pastel', 12 | 'perplexity', 13 | 'shadcn', 14 | 'slack', 15 | 'soft', 16 | 'spotify', 17 | 'valorant', 18 | 'vscode' 19 | ] 20 | -------------------------------------------------------------------------------- /src/js/plugins/toggle-count/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IToggleCountOptions { 2 | target: string | HTMLInputElement 3 | min: number 4 | max: number 5 | duration: number 6 | } 7 | 8 | export interface IToggleCount { 9 | options?: IToggleCountOptions 10 | 11 | countUp(): void 12 | countDown(): void 13 | destroy(): void 14 | } 15 | -------------------------------------------------------------------------------- /src/js/plugins/file-upload/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { DropzoneOptions } from 'dropzone' 2 | 3 | export interface IFileUploadOptions extends DropzoneOptions { 4 | extensions?: {} 5 | autoHideTrigger?: boolean 6 | singleton?: boolean 7 | } 8 | 9 | export interface IFileUpload { 10 | options?: IFileUploadOptions 11 | 12 | destroy(): void 13 | } 14 | -------------------------------------------------------------------------------- /src/js/plugins/datatable/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant datatable-ordering-asc { 2 | &.dt-ordering-asc { 3 | @slot; 4 | } 5 | 6 | .dt-ordering-asc & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant datatable-ordering-desc { 12 | &.dt-ordering-desc { 13 | @slot; 14 | } 15 | 16 | .dt-ordering-desc & { 17 | @slot; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/label.css: -------------------------------------------------------------------------------- 1 | .label-text { 2 | @apply text-base-content block cursor-pointer p-1 text-sm; 3 | } 4 | 5 | .helper-text { 6 | @apply text-base-content/80 block p-1 text-xs select-none; 7 | } 8 | 9 | .success-message { 10 | @apply text-success hidden p-1 text-sm select-none; 11 | } 12 | 13 | .error-message { 14 | @apply text-error hidden p-1 text-sm select-none; 15 | } 16 | -------------------------------------------------------------------------------- /functions/plugin.js: -------------------------------------------------------------------------------- 1 | export const plugin = { 2 | withOptions: (pluginFunction, configFunction = () => ({})) => { 3 | const optionsFunction = options => { 4 | const handler = pluginFunction(options) 5 | const config = configFunction(options) 6 | return { handler, config } 7 | } 8 | optionsFunction.__isOptionsFunction = true 9 | return optionsFunction 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/js/plugins/tabs/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ITabsOnChangePayload { 2 | el: HTMLElement 3 | tabsId: string 4 | prev: string 5 | current: string 6 | } 7 | 8 | export interface ITabsOptions { 9 | eventType: 'click' | 'hover' 10 | preventNavigationResolution: string | number | null 11 | } 12 | 13 | export interface ITabs { 14 | options?: ITabsOptions 15 | 16 | destroy(): void 17 | } 18 | -------------------------------------------------------------------------------- /src/js/plugins/tree-view/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant tree-view-selected { 2 | &[data-tree-view-item].selected { 3 | @slot; 4 | } 5 | 6 | [data-tree-view-item].selected > & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant tree-view-disabled { 12 | &[data-tree-view-item].disabled { 13 | @slot; 14 | } 15 | 16 | [data-tree-view-item].disabled > & { 17 | @slot; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /functions/createDirectoryBasedOnFileNames.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import path from 'node:path' 3 | 4 | export const createDirectoryBasedOnFileNames = async (fileName, fileExtension, distDir) => { 5 | const componentName = path.basename(fileName, fileExtension) 6 | const componentDir = path.join(distDir, componentName) 7 | await fs.mkdir(componentDir, { recursive: true }) 8 | return componentDir 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/carousel/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant carousel-active { 2 | &.active { 3 | @slot; 4 | } 5 | 6 | .active & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant carousel-disabled { 12 | &.disabled { 13 | @slot; 14 | } 15 | 16 | .disabled & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant carousel-dragging { 22 | &.dragging { 23 | @slot; 24 | } 25 | 26 | .dragging & { 27 | @slot; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/vendor/apexcharts.css: -------------------------------------------------------------------------------- 1 | .apexcharts-tooltip-series-group.apexcharts-active { 2 | @apply !pb-0; 3 | } 4 | 5 | .apexcharts-tooltip.apexcharts-theme-light, 6 | .apexcharts-tooltip.apexcharts-theme-dark { 7 | @apply !bg-base-100 !border-none !shadow; 8 | } 9 | 10 | .apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title, 11 | .apexcharts-tooltip.apexcharts-theme-dark .apexcharts-tooltip-title { 12 | @apply !bg-base-100 !border-base-content/20; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "./dist", 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "module": "ESNext", 8 | "target": "ES6", 9 | "allowJs": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "include": ["src/js/**/*", "./global.d.ts"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /src/vendor/raty.css: -------------------------------------------------------------------------------- 1 | @keyframes rating-jump { 2 | 0%, 3 | 100% { 4 | transform: translateY(0) scale(1); 5 | } 6 | 50% { 7 | transform: translateY(-0.3em) scale(1.1); 8 | } 9 | } 10 | 11 | .raty-jump { 12 | animation: rating-jump 0.5s ease-in-out; 13 | } 14 | 15 | .raty-cancel { 16 | @apply ms-2; 17 | } 18 | 19 | .cancel-off-png { 20 | @apply text-error; 21 | } 22 | 23 | .star-off-png:before { 24 | content: "\f005"; 25 | opacity: 0.2; 26 | } 27 | -------------------------------------------------------------------------------- /src/js/plugins/strong-password/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IStrongPasswordOptions { 2 | target: string | HTMLInputElement 3 | hints?: string 4 | stripClasses?: string 5 | minLength?: number 6 | mode?: string 7 | popoverSpace?: number 8 | checksExclude?: string[] 9 | specialCharactersSet?: string 10 | } 11 | 12 | export interface IStrongPassword { 13 | options?: IStrongPasswordOptions 14 | 15 | recalculateDirection(): void 16 | destroy(): void 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "outDir": "./dist/", 5 | "sourceMap": true, 6 | "noImplicitAny": true, 7 | "module": "commonjs", 8 | "target": "es2015", // Class extend requires using "es2015" instead of "es5" 9 | "allowJs": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true 12 | }, 13 | "include": ["src/js/**/*", "global.d.ts"], 14 | "exclude": ["node_modules", "**/*.spec.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /src/components/footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | @apply grid w-full grid-flow-row place-items-start gap-x-4 gap-y-10 md:grid-flow-col; 3 | & > * { 4 | @apply grid place-items-start gap-3; 5 | } 6 | &.footer-center { 7 | @apply place-items-center text-center md:grid-flow-row-dense; 8 | & > * { 9 | @apply place-items-center; 10 | } 11 | } 12 | :where(.link) { 13 | @apply font-normal; 14 | } 15 | } 16 | 17 | .footer-title { 18 | @apply text-base-content mb-1 font-medium; 19 | } 20 | -------------------------------------------------------------------------------- /functions/replaceApplyTrueWithEmptyObject.js: -------------------------------------------------------------------------------- 1 | export const replaceApplyTrueWithEmptyObject = obj => { 2 | const stack = [obj] 3 | 4 | while (stack.length > 0) { 5 | const currentObj = stack.pop() 6 | 7 | for (const [key, value] of Object.entries(currentObj)) { 8 | if (typeof value === 'object' && value !== null) { 9 | stack.push(value) 10 | } 11 | 12 | if (key.startsWith('@apply') && value === true) { 13 | currentObj[key] = {} 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/plugins/select/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant selected { 2 | &.selected { 3 | @slot; 4 | } 5 | 6 | .selected & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant select-disabled { 12 | &.disabled { 13 | @slot; 14 | } 15 | 16 | .disabled & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant select-active { 22 | &.active { 23 | @slot; 24 | } 25 | 26 | .active & { 27 | @slot; 28 | } 29 | } 30 | 31 | @custom-variant select-opened { 32 | &.opened { 33 | @slot; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /functions/copyFile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | 4 | export const copyFile = async (from, to, newName = null) => { 5 | try { 6 | const destDir = path.dirname(to) 7 | await fs.mkdir(destDir, { recursive: true }) 8 | 9 | let destPath = to 10 | if (newName) { 11 | destPath = path.join(destDir, newName) 12 | } 13 | 14 | await fs.copyFile(from, destPath) 15 | } catch (error) { 16 | throw new Error(`Error copying file from ${from} to ${to}: ${error.message}`) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/js/plugins/accordion/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IAccordionTreeViewStaticOptions {} 2 | 3 | export interface IAccordionTreeView { 4 | el: HTMLElement | null 5 | options?: IAccordionTreeViewStaticOptions 6 | listeners?: { el: HTMLElement; listener: (evt: Event) => void }[] 7 | } 8 | 9 | export interface IAccordionOptions {} 10 | 11 | export interface IAccordion { 12 | options?: IAccordionOptions 13 | 14 | toggleClick(evt: Event): void 15 | show(): void 16 | hide(): void 17 | update(): void 18 | destroy(): void 19 | } 20 | -------------------------------------------------------------------------------- /src/js/plugins/overlay/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IOverlayOptions { 2 | hiddenClass?: string | null 3 | emulateScrollbarSpace?: boolean 4 | isClosePrev?: boolean 5 | backdropClasses?: string | null 6 | backdropParent?: string | HTMLElement | Document 7 | backdropExtraClasses?: string | null 8 | moveOverlayToBody?: number | null 9 | } 10 | 11 | export interface IOverlay { 12 | options?: IOverlayOptions 13 | 14 | open(cb: Function | null): void 15 | close(forceClose: boolean, cb: Function | null): void 16 | destroy(): void 17 | } 18 | -------------------------------------------------------------------------------- /src/js/plugins/datatable/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { Config } from 'datatables.net' 2 | 3 | interface IDataTablePagingOptions { 4 | pageBtnClasses?: string 5 | } 6 | 7 | interface IDataTableRowSelectingOptions { 8 | selectAllSelector?: string 9 | individualSelector?: string 10 | } 11 | export interface IDataTableOptions extends Config { 12 | rowSelectingOptions?: IDataTableRowSelectingOptions 13 | pagingOptions?: IDataTablePagingOptions 14 | } 15 | 16 | export interface IDataTable { 17 | options?: IDataTableOptions 18 | 19 | destroy(): void 20 | } 21 | -------------------------------------------------------------------------------- /src/js/plugins/combobox/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant combo-box-active { 2 | &.active { 3 | @slot; 4 | } 5 | 6 | .active & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant combo-box-has-value { 12 | &.has-value { 13 | @slot; 14 | } 15 | 16 | .has-value & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant combo-box-selected { 22 | &.selected { 23 | @slot; 24 | } 25 | 26 | .selected & { 27 | @slot; 28 | } 29 | } 30 | 31 | @custom-variant combo-box-tab-active { 32 | &.active { 33 | @slot; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/js/plugins/strong-password/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant password-active { 2 | &.active { 3 | @slot; 4 | } 5 | 6 | .active & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant strong-password { 12 | &.passed { 13 | @slot; 14 | } 15 | 16 | .passed & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant strong-password-accepted { 22 | &.accepted { 23 | @slot; 24 | } 25 | 26 | .accepted & { 27 | @slot; 28 | } 29 | } 30 | 31 | @custom-variant strong-password-active { 32 | &.active { 33 | @slot; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /functions/removeFiles.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import { resolve } from 'path' 3 | export const removeFiles = async (items = []) => { 4 | const removePromises = items.map(async item => { 5 | const itemPath = resolve(item) 6 | 7 | try { 8 | const stats = await fs.lstat(itemPath) 9 | if (stats.isDirectory()) { 10 | await fs.rmdir(itemPath, { recursive: true }) 11 | } else { 12 | await fs.unlink(itemPath) 13 | } 14 | } catch (error) { 15 | if (error.code !== 'ENOENT') { 16 | throw error 17 | } 18 | } 19 | }) 20 | 21 | await Promise.all(removePromises) 22 | } 23 | -------------------------------------------------------------------------------- /src/utilities/glass.css: -------------------------------------------------------------------------------- 1 | .glass { 2 | border: none; 3 | backdrop-filter: blur(var(--glass-blur, 40px)); 4 | background-color: #0000; 5 | background-image: 6 | linear-gradient(135deg, oklch(100% 0 0 / var(--glass-opacity, 30%)) 0%, oklch(0% 0 0 / 0%) 100%), 7 | linear-gradient( 8 | var(--glass-reflect-degree, 100deg), 9 | oklch(100% 0 0 / var(--glass-reflect-opacity, 5%)) 25%, 10 | oklch(0% 0 0 / 0%) 25% 11 | ); 12 | box-shadow: 13 | 0 0 0 1px oklch(100% 0 0 / var(--glass-border-opacity, 20%)) inset, 14 | 0 0 0 2px oklch(0% 0 0 / 5%); 15 | text-shadow: 0 1px oklch(0% 0 0 / var(--glass-text-shadow-opacity, 5%)); 16 | } 17 | -------------------------------------------------------------------------------- /src/js/plugins/tree-view/interfaces.ts: -------------------------------------------------------------------------------- 1 | export type ITreeViewOptionsControlBy = 'checkbox' | 'button' 2 | 3 | export interface ITreeViewItem { 4 | id: string 5 | value: string 6 | isDir: boolean 7 | path: string 8 | isSelected?: boolean 9 | } 10 | 11 | export interface ITreeViewOptions { 12 | items: ITreeViewItem[] | null 13 | controlBy?: ITreeViewOptionsControlBy 14 | autoSelectChildren?: boolean 15 | isIndeterminate?: boolean 16 | } 17 | 18 | export interface ITreeView { 19 | options?: ITreeViewOptions 20 | 21 | update(): void 22 | getSelectedItems(): ITreeViewItem[] 23 | changeItemProp(id: string, prop: string, val: any): void 24 | destroy(): void 25 | } 26 | -------------------------------------------------------------------------------- /src/js/plugins/dropdown/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant dropdown-open { 2 | &.dropdown-menu.open { 3 | @slot; 4 | } 5 | 6 | .dropdown.open > & { 7 | @slot; 8 | } 9 | 10 | .dropdown.open > .dropdown-toggle & { 11 | @slot; 12 | } 13 | 14 | .dropdown.open > .dropdown-menu > & { 15 | @slot; 16 | } 17 | } 18 | 19 | @custom-variant dropdown-item-disabled { 20 | .dropdown.open > .dropdown-menu &.disabled { 21 | @slot; 22 | } 23 | } 24 | 25 | @custom-variant dropdown-item-checked { 26 | .dropdown.open .dropdown-menu [aria-checked="true"] & { 27 | @slot; 28 | } 29 | 30 | .dropdown.open .dropdown-menu &[aria-checked="true"] { 31 | @slot; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/js/plugins/overlay/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant overlay-open { 2 | &.open { 3 | @slot; 4 | } 5 | 6 | .open & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant overlay-layout-open { 12 | &.overlay-body-open { 13 | @slot; 14 | } 15 | 16 | .overlay-body-open & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant overlay-minified { 22 | &.minified { 23 | @slot; 24 | } 25 | 26 | .minified & { 27 | @slot; 28 | } 29 | 30 | .overlay-minified & { 31 | @slot; 32 | } 33 | } 34 | 35 | @custom-variant overlay-backdrop-open { 36 | &.overlay-backdrop { 37 | @slot; 38 | } 39 | 40 | .overlay-backdrop & { 41 | @slot; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /functions/getDirectoriesWithTargetFile.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | 4 | export const getDirectoriesWithTargetFile = async (directory, targetFile) => { 5 | const files = await fs.readdir(directory) 6 | const filteredDirs = [] 7 | 8 | for (const file of files) { 9 | const filePath = path.join(directory, file) 10 | const stats = await fs.stat(filePath) 11 | 12 | if (stats.isDirectory()) { 13 | try { 14 | await fs.access(path.join(filePath, targetFile)) 15 | filteredDirs.push(file) 16 | } finally { 17 | // File doesn't exist, skip this directory 18 | } 19 | } 20 | } 21 | 22 | return filteredDirs 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/issue-reply.hbs: -------------------------------------------------------------------------------- 1 |
2 | Hi @{{inputs.user}} 5 | 6 |

Thank you for your support in helping us improve FlyonUI!

7 | 8 |

We’ve received your submission and will respond within few business days. Our team handles issues one at a time, and we’ll be reviewing yours as soon as possible.

9 | 10 |

In the meantime, any additional details or a reproducible example would be greatly appreciated and will help us resolve the issue more efficiently.

11 | 12 |

Thank you for your patience and understanding!

13 |
14 | -------------------------------------------------------------------------------- /src/js/plugins/stepper/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IStepperOptions { 2 | currentIndex?: number 3 | isCompleted?: boolean 4 | mode?: string 5 | } 6 | 7 | export interface IStepper { 8 | options?: IStepperOptions 9 | 10 | setProcessedNavItem(n?: number): void 11 | unsetProcessedNavItem(n?: number): void 12 | goToNext(): void 13 | goToFinish(): void 14 | disableButtons(): void 15 | enableButtons(): void 16 | setErrorNavItem(n?: number): void 17 | destroy(): void 18 | } 19 | 20 | export interface IStepperItem { 21 | index: number 22 | isFinal: boolean 23 | isCompleted: boolean 24 | isSkip: boolean 25 | isOptional?: boolean 26 | isDisabled?: boolean 27 | isProcessed?: boolean 28 | isInvalid?: boolean 29 | el: HTMLElement 30 | } 31 | -------------------------------------------------------------------------------- /functions/variables.css: -------------------------------------------------------------------------------- 1 | @theme { 2 | --color-base-100: ; 3 | --color-base-200: ; 4 | --color-base-300: ; 5 | --color-base-content: ; 6 | --color-primary: ; 7 | --color-primary-content: ; 8 | --color-secondary: ; 9 | --color-secondary-content: ; 10 | --color-accent: ; 11 | --color-accent-content: ; 12 | --color-neutral: ; 13 | --color-neutral-content: ; 14 | --color-info: ; 15 | --color-info-content: ; 16 | --color-success: ; 17 | --color-success-content: ; 18 | --color-warning: ; 19 | --color-warning-content: ; 20 | --color-error: ; 21 | --color-error-content: ; 22 | --radius-selector: ; 23 | --radius-field: ; 24 | --radius-box: ; 25 | --size-selector: ; 26 | --size-field: ; 27 | --border: ; 28 | --depth: ; 29 | --noise: ; 30 | } 31 | -------------------------------------------------------------------------------- /functions/getFileNames.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import path from 'path' 3 | 4 | export const getFileNames = async (dir, extension, recursive = true) => { 5 | let fileNames = [] 6 | const files = await fs.readdir(dir, { withFileTypes: true }) 7 | 8 | for (const file of files) { 9 | const filePath = path.join(dir, file.name) 10 | 11 | if (file.isDirectory() && recursive) { 12 | const subDirFiles = await getFileNames(filePath, extension, recursive) 13 | fileNames = fileNames.concat(subDirFiles) 14 | } else if (file.isFile() && file.name.endsWith(extension)) { 15 | // Extract the file name without extension 16 | const fileName = path.basename(file.name, extension) 17 | fileNames.push(fileName) 18 | } 19 | } 20 | 21 | return fileNames 22 | } 23 | -------------------------------------------------------------------------------- /src/js/plugins/carousel/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { TCarouselOptionsSlidesQty } from './types' 2 | 3 | export interface ICarouselOptions { 4 | currentIndex: number 5 | loadingClasses?: string | string[] 6 | dotsItemClasses?: string 7 | mode?: 'default' | 'scroll-nav' 8 | isAutoHeight?: boolean 9 | isAutoPlay?: boolean 10 | isCentered?: boolean 11 | isDraggable?: boolean 12 | isInfiniteLoop?: boolean 13 | isRTL?: boolean 14 | isSnap?: boolean 15 | hasSnapSpacers?: boolean 16 | slidesQty?: TCarouselOptionsSlidesQty | number 17 | speed?: number 18 | updateDelay?: number 19 | } 20 | 21 | export interface ICarousel { 22 | options?: ICarouselOptions 23 | 24 | recalculateWidth(): void 25 | goToPrev(): void 26 | goToNext(): void 27 | goTo(i: number): void 28 | destroy(): void 29 | } 30 | -------------------------------------------------------------------------------- /src/js/plugins/range-slider/interfaces.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'nouislider' 2 | 3 | import { TRangeSliderOptionsFormatterType } from './types' 4 | 5 | export interface IRangeSliderCssClassesObject { 6 | [key: string]: any 7 | } 8 | 9 | export interface IRangeSliderOptionsFormatterOptions { 10 | type?: TRangeSliderOptionsFormatterType 11 | prefix?: string 12 | postfix?: string 13 | } 14 | 15 | export interface IRangeSliderOptions extends Options { 16 | disabled?: boolean 17 | wrapper?: HTMLElement 18 | currentValue?: HTMLElement[] 19 | formatter?: IRangeSliderOptionsFormatterOptions | TRangeSliderOptionsFormatterType 20 | icons?: { 21 | handle?: string 22 | } 23 | } 24 | 25 | export interface IRangeSlider { 26 | options?: IRangeSliderOptions 27 | 28 | destroy(): void 29 | } 30 | -------------------------------------------------------------------------------- /src/components/carousel.css: -------------------------------------------------------------------------------- 1 | .carousel { 2 | @apply relative w-full overflow-hidden rounded-2xl; 3 | } 4 | .carousel-body { 5 | @apply flex flex-nowrap; 6 | } 7 | 8 | .carousel-prev, 9 | .carousel-next { 10 | @apply absolute inset-y-0 my-auto inline-flex h-max cursor-pointer items-center justify-center; 11 | } 12 | 13 | .carousel-prev.disabled, 14 | .carousel-next.disabled { 15 | @apply pointer-events-none; 16 | } 17 | 18 | .carousel-prev { 19 | @apply rounded-s-2xl; 20 | } 21 | .carousel-next { 22 | @apply rounded-e-2xl; 23 | } 24 | 25 | .carousel-box { 26 | @apply bg-neutral/30 h-1 w-10 cursor-pointer; 27 | } 28 | 29 | .carousel-dot { 30 | @apply bg-neutral/30 size-3 cursor-pointer rounded-full; 31 | } 32 | 33 | .carousel-body, 34 | .carousel-slide { 35 | @apply transition-transform duration-700; 36 | } 37 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "bracketSameLine": false, 5 | "htmlWhitespaceSensitivity": "ignore", 6 | "insertPragma": false, 7 | "printWidth": 120, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "requirePragma": false, 11 | "semi": false, 12 | "singleQuote": false, 13 | "tabWidth": 2, 14 | "trailingComma": "none", 15 | "useTabs": false, 16 | "endOfLine": "lf", 17 | "embeddedLanguageFormatting": "auto", 18 | "overrides": [ 19 | { 20 | "files": [ 21 | "*.js", 22 | "*.ts", 23 | "*.jsx", 24 | "*.tsx" 25 | ], 26 | "options": { 27 | "singleQuote": true, 28 | "jsxSingleQuote": true 29 | } 30 | } 31 | ], 32 | "plugins": [ 33 | "prettier-plugin-tailwindcss" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /functions/variables.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'daisyui/functions/variables' { 2 | const variables: { 3 | colors: { 4 | 'base-100': string 5 | 'base-200': string 6 | 'base-300': string 7 | 'base-content': string 8 | primary: string 9 | 'primary-content': string 10 | secondary: string 11 | 'secondary-content': string 12 | accent: string 13 | 'accent-content': string 14 | neutral: string 15 | 'neutral-content': string 16 | info: string 17 | 'info-content': string 18 | success: string 19 | 'success-content': string 20 | warning: string 21 | 'warning-content': string 22 | error: string 23 | 'error-content': string 24 | } 25 | borderRadius: { 26 | selector: string 27 | field: string 28 | box: string 29 | } 30 | } 31 | export default variables 32 | } 33 | -------------------------------------------------------------------------------- /functions/cleanCss.js: -------------------------------------------------------------------------------- 1 | export const cleanCss = cssContent => { 2 | // Precompile regular expressions for better performance 3 | const emptyFallbackRegex = /var\((--[^,)]+),\s*\)/g 4 | const spacingWidthFallbackRegex = /var\((--(spacing|width)[\w-]*),\s*((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/g 5 | const spacingVarRegex = /var\(--spacing\)/g 6 | 7 | // Remove empty fallbacks 8 | cssContent = cssContent.replace(emptyFallbackRegex, 'var($1)') 9 | 10 | // Remove spacing, width css variable if there's a fallback value 11 | cssContent = cssContent.replace(spacingWidthFallbackRegex, (match, variable, prefix, fallback) => { 12 | // If there's no actual fallback value, return the original match 13 | return fallback.trim() ? fallback.trim() : match 14 | }) 15 | 16 | // Replace all `var(--spacing)` with `0.25rem` 17 | cssContent = cssContent.replace(spacingVarRegex, '0.25rem') 18 | 19 | return cssContent 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🔎 Search in Discussions 4 | url: https://github.com/themeselection/flyonui/discussions?discussions_q= 5 | about: First see if there's an existing topic in Discussions. 6 | - name: 🔎 Search in Issues 7 | url: https://github.com/themeselection/flyonui/issues?q=is%3Aissue 8 | about: If you found a bug, first see if there's an existing issue about it. 9 | - name: ❓ Ask a new question in Discussions 10 | url: https://github.com/themeselection/flyonui/discussions/new?category=q-a 11 | about: If you have a new question and you couldn't find answers. 12 | - name: 💡 Ideas / Feature request 13 | url: https://github.com/themeselection/flyonui/discussions/new?category=ideas-request-new-feature 14 | about: If you want to suggest a new idea or if you want a new feature to be added to flyonui, let's talk about it in Discussions forum 15 | -------------------------------------------------------------------------------- /functions/createPluginFiles.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs' 2 | import path from 'path' 3 | 4 | export const createPluginFiles = async (type, componentDir, jsContent, fileName) => { 5 | const types = { 6 | base: 'addBase', 7 | component: 'addComponents', 8 | utility: 'addUtilities' 9 | } 10 | 11 | // create object.js 12 | const objectJsPath = path.join(componentDir, 'object.js') 13 | await fs.writeFile(objectJsPath, `export default ${jsContent};`) 14 | 15 | // create index.js 16 | const indexJsPath = path.join(componentDir, 'index.js') 17 | const indexJsContent = `import ${fileName} from './object.js'; 18 | import { addPrefix } from '../../functions/addPrefix.js'; 19 | 20 | export default ({ ${types[type]}, prefix = '' }) => { 21 | const prefixed${fileName} = addPrefix(${fileName}, prefix); 22 | ${types[type]}({ ...prefixed${fileName} }); 23 | }; 24 | ` 25 | await fs.writeFile(indexJsPath, indexJsContent) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/skeleton.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | @apply bg-base-200 rounded-box; 3 | will-change: background-position; 4 | background-image: linear-gradient(105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100%); 5 | background-size: 200% auto; 6 | background-repeat: no-repeat; 7 | background-position-x: -50%; 8 | } 9 | 10 | /* Define the animation properties in the skeleton-animated class */ 11 | .skeleton-animated { 12 | @apply motion-reduce:[transition-duration:15s]; 13 | animation: skeleton 1.8s ease-in-out infinite; 14 | } 15 | 16 | .skeleton-striped { 17 | background-image: repeating-linear-gradient( 18 | 45deg, 19 | var(--color-base-200), 20 | var(--color-base-200) 2px, 21 | var(--color-base-100) 3px, 22 | var(--color-base-100) 6px 23 | ); 24 | } 25 | 26 | @keyframes skeleton { 27 | 0% { 28 | background-position: 150%; 29 | } 30 | 31 | 100% { 32 | background-position: -50%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/accordion.css: -------------------------------------------------------------------------------- 1 | .accordion-toggle { 2 | @apply text-base-content w-full cursor-pointer px-5 py-4 text-lg font-medium disabled:pointer-events-none disabled:opacity-50; 3 | } 4 | 5 | .accordion-shadow :where(.accordion-item) { 6 | @apply bg-base-100 rounded-box shadow-base-300/20 my-2 shadow-sm; 7 | 8 | .accordion-item:first-of-type { 9 | @apply mt-0; 10 | } 11 | .accordion-item:last-of-type { 12 | @apply mb-0; 13 | } 14 | 15 | & > *:first-child { 16 | @apply rounded-t-box; 17 | } 18 | & > *:last-child { 19 | @apply rounded-b-box; 20 | } 21 | } 22 | 23 | .accordion:where(.accordion-bordered) { 24 | @apply border-base-content/25 divide-base-content/25 rounded-box bg-base-100 divide-y border; 25 | } 26 | 27 | /* Tree view space */ 28 | 29 | .tree-view-space { 30 | @apply before:bg-base-content/40 relative ms-4 ps-4 before:absolute before:start-0 before:top-0 before:-ms-0.5 before:h-full before:w-0.5; 31 | } 32 | -------------------------------------------------------------------------------- /functions/generatePlugins.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { getFileNames } from './getFileNames.js' 3 | import { cssToJs } from './cssToJs.js' 4 | import { createDirectoryBasedOnFileNames } from './createDirectoryBasedOnFileNames.js' 5 | import { createPluginFiles } from './createPluginFiles.js' 6 | 7 | export const generatePlugins = async ({ type, srcDir, distDir, exclude = [] }) => { 8 | await fs.mkdir(distDir, { recursive: true }) 9 | const cssFiles = await getFileNames(srcDir, '.css') 10 | const filteredCssFiles = cssFiles.filter(file => !exclude.includes(file)) 11 | 12 | await Promise.all( 13 | filteredCssFiles.map(async cssFile => { 14 | const [jsContent, componentDir] = await Promise.all([ 15 | cssToJs(`${srcDir}/${cssFile}.css`), 16 | createDirectoryBasedOnFileNames(cssFile, '.css', distDir) 17 | ]) 18 | 19 | await createPluginFiles(type, componentDir, jsContent, cssFile) 20 | }) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/js/plugins/accordion/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant accordion-item-active { 2 | &.accordion-item.active { 3 | @slot; 4 | } 5 | 6 | .accordion-item.active > & { 7 | @slot; 8 | } 9 | 10 | .accordion-item.active > .accordion-toggle & { 11 | @slot; 12 | } 13 | 14 | .accordion-item.active > .accordion-heading > .accordion-toggle & { 15 | @slot; 16 | } 17 | 18 | &.accordion-toggle { 19 | .accordion-item.active > & { 20 | @slot; 21 | } 22 | } 23 | 24 | &.accordion-toggle { 25 | .accordion-item.active > .accordion-heading > & { 26 | @slot; 27 | } 28 | } 29 | 30 | &.accordion-force-active { 31 | .accordion-item.active & { 32 | @slot; 33 | } 34 | } 35 | } 36 | 37 | @custom-variant accordion-selected { 38 | &.selected { 39 | .accordion-item & { 40 | @slot; 41 | } 42 | } 43 | } 44 | 45 | @custom-variant accordion-outside-active { 46 | &.active { 47 | @slot; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/breadcrumbs.css: -------------------------------------------------------------------------------- 1 | .breadcrumbs { 2 | @apply max-w-full overflow-x-auto; 3 | } 4 | 5 | :where(.breadcrumbs > ul), 6 | :where(.breadcrumbs > ol) { 7 | @apply flex items-center px-1 py-2 whitespace-nowrap; 8 | min-height: min-content; 9 | } 10 | 11 | .breadcrumbs-separator { 12 | @apply text-base-content/50 mx-2 inline-flex shrink-0 items-center justify-center; 13 | } 14 | 15 | :where(.breadcrumbs > ul > li), 16 | :where(.breadcrumbs > ol > li) { 17 | @apply inline-flex items-center; 18 | } 19 | 20 | :where(.breadcrumbs > ul > li > a), 21 | :where(.breadcrumbs > ol > li > a) { 22 | @apply hover:text-primary inline-flex items-center gap-1; 23 | &:focus { 24 | @apply outline-none; 25 | } 26 | &:focus-visible { 27 | outline: 2px solid currentColor; 28 | outline-offset: 2px; 29 | } 30 | } 31 | 32 | :where(.breadcrumbs > ul > li:last-child), 33 | :where(.breadcrumbs > ol > li:last-child) { 34 | @apply text-base-content cursor-text font-medium; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/filter.css: -------------------------------------------------------------------------------- 1 | .filter { 2 | @apply flex flex-wrap; 3 | input[type="radio"] { 4 | @apply w-auto; 5 | } 6 | input { 7 | @apply overflow-hidden opacity-100; 8 | scale: 1; 9 | border-width: 0; 10 | transition: 11 | margin 0.1s, 12 | opacity 0.3s, 13 | padding 0.3s, 14 | border-width 0.1s; 15 | &:not(:last-child) { 16 | @apply me-1; 17 | } 18 | &.filter-reset { 19 | @apply aspect-square; 20 | &::after { 21 | content: "×"; 22 | } 23 | } 24 | } 25 | &:not(:has(input:checked:not(.filter-reset))) { 26 | .filter-reset, 27 | input[type="reset"] { 28 | scale: 0; 29 | border-width: 0; 30 | @apply mx-0 w-0 px-0 opacity-0; 31 | } 32 | } 33 | &:has(input:checked:not(.filter-reset)) { 34 | input:not(:checked, .filter-reset, input[type="reset"]) { 35 | scale: 0; 36 | border-width: 0; 37 | @apply mx-0 w-0 px-0 opacity-0; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/kbd.css: -------------------------------------------------------------------------------- 1 | .kbd { 2 | @apply bg-base-200/20 rounded-field inline-flex items-center justify-center text-sm; 3 | padding-left: 0.5em; 4 | padding-right: 0.5em; 5 | border: var(--border) solid color-mix(in oklab, var(--color-base-content) 25%, #0000); 6 | border-bottom-width: calc(var(--border) + 1px); 7 | --size: calc(var(--size-selector, 0.25rem) * 6.5); 8 | height: var(--size); 9 | min-width: var(--size); 10 | } 11 | 12 | /* kbd sizes */ 13 | .kbd-xs { 14 | --size: calc(var(--size-selector, 0.25rem) * 4.5); 15 | font-size: 0.625rem; 16 | } 17 | 18 | .kbd-sm { 19 | @apply text-xs; 20 | --size: calc(var(--size-selector, 0.25rem) * 5.5); 21 | } 22 | 23 | .kbd-md { 24 | @apply text-sm; 25 | --size: calc(var(--size-selector, 0.25rem) * 6.5); 26 | } 27 | 28 | .kbd-lg { 29 | @apply text-base; 30 | --size: calc(var(--size-selector, 0.25rem) * 7.5); 31 | } 32 | 33 | .kbd-xl { 34 | @apply text-lg; 35 | --size: calc(var(--size-selector, 0.25rem) * 8.5); 36 | } 37 | -------------------------------------------------------------------------------- /src/base/svg.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fx-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='a'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.34' numOctaves='4' stitchTiles='stitch'%3E%3C/feTurbulence%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23a)' opacity='0.2'%3E%3C/rect%3E%3C/svg%3E"); 3 | } 4 | .chat { 5 | --mask-chat: url("data:image/svg+xml,%3csvg width='13' height='13' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='M0 11.5004C0 13.0004 2 13.0004 2 13.0004H12H13V0.00036329L12.5 0C12.5 0 11.977 2.09572 11.8581 2.50033C11.6075 3.35237 10.9149 4.22374 9 5.50036C6 7.50036 0 10.0004 0 11.5004Z'/%3e%3c/svg%3e"); 6 | } 7 | .tooltip { 8 | --mask-tooltip: url("data:image/svg+xml,%3Csvg width='10' height='4' viewBox='0 0 8 4' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0.500009 1C3.5 1 3.00001 4 5.00001 4C7 4 6.5 1 9.5 1C10 1 10 0.499897 10 0H0C-1.99338e-08 0.5 0 1 0.500009 1Z' fill='black'/%3E%3C/svg%3E%0A"); 9 | } 10 | -------------------------------------------------------------------------------- /src/js/plugins/stepper/variants.css: -------------------------------------------------------------------------------- 1 | @custom-variant stepper-active { 2 | &.active { 3 | @slot; 4 | } 5 | 6 | .active & { 7 | @slot; 8 | } 9 | } 10 | 11 | @custom-variant stepper-success { 12 | &.is-valid { 13 | @slot; 14 | } 15 | 16 | .is-valid & { 17 | @slot; 18 | } 19 | } 20 | 21 | @custom-variant stepper-completed { 22 | &.completed { 23 | @slot; 24 | } 25 | 26 | .completed & { 27 | @slot; 28 | } 29 | } 30 | 31 | @custom-variant stepper-error { 32 | &.is-invalid { 33 | @slot; 34 | } 35 | 36 | .is-invalid & { 37 | @slot; 38 | } 39 | } 40 | 41 | @custom-variant stepper-processed { 42 | &.processed { 43 | @slot; 44 | } 45 | 46 | .processed & { 47 | @slot; 48 | } 49 | } 50 | 51 | @custom-variant stepper-disabled { 52 | &.disabled { 53 | @slot; 54 | } 55 | 56 | .disabled & { 57 | @slot; 58 | } 59 | } 60 | 61 | @custom-variant stepper-skipped { 62 | &.skipped { 63 | @slot; 64 | } 65 | 66 | .skipped & { 67 | @slot; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/js/plugins/base-plugin/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* 4 | * HSBasePlugin 5 | * @version: 3.2.3 6 | * @author: Preline Labs Ltd. 7 | * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) 8 | * Copyright 2024 Preline Labs Ltd. 9 | */ 10 | 11 | import { IBasePlugin } from './interfaces' 12 | 13 | export default class HSBasePlugin implements IBasePlugin { 14 | constructor( 15 | public el: E, 16 | public options: O, 17 | public events?: any 18 | ) { 19 | this.el = el 20 | this.options = options 21 | this.events = {} 22 | } 23 | 24 | public createCollection(collection: any[], element: any) { 25 | collection.push({ 26 | id: element?.el?.id || collection.length + 1, 27 | element 28 | }) 29 | } 30 | 31 | public fireEvent(evt: string, payload: any = null) { 32 | if (this.events.hasOwnProperty(evt)) return this.events[evt](payload) 33 | } 34 | 35 | public on(evt: string, cb: Function) { 36 | this.events[evt] = cb 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/status.css: -------------------------------------------------------------------------------- 1 | .status { 2 | @apply bg-neutral text-neutral inline-block aspect-square size-2.5 rounded-full bg-center bg-no-repeat align-middle; 3 | background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / calc(var(--depth) * 0.5)), #0000); 4 | box-shadow: 0 2px 3px -1px color-mix(in oklab, currentColor calc(var(--depth) * 100%), #0000); 5 | } 6 | 7 | .status-primary { 8 | @apply bg-primary text-primary; 9 | } 10 | 11 | .status-secondary { 12 | @apply bg-secondary text-secondary; 13 | } 14 | 15 | .status-accent { 16 | @apply bg-accent text-accent; 17 | } 18 | 19 | .status-info { 20 | @apply bg-info text-info; 21 | } 22 | 23 | .status-success { 24 | @apply bg-success text-success; 25 | } 26 | 27 | .status-warning { 28 | @apply bg-warning text-warning; 29 | } 30 | 31 | .status-error { 32 | @apply bg-error text-error; 33 | } 34 | 35 | .status-xs { 36 | @apply size-1.5; 37 | } 38 | 39 | .status-sm { 40 | @apply size-2; 41 | } 42 | 43 | .status-md { 44 | @apply size-2.5; 45 | } 46 | 47 | .status-lg { 48 | @apply size-3; 49 | } 50 | 51 | .status-xl { 52 | @apply size-3.5; 53 | } 54 | -------------------------------------------------------------------------------- /src/js/plugins/accessibility-manager/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IAccessibilityKeyboardHandlers { 2 | onEnter?: () => void 3 | onEsc?: () => void 4 | onSpace?: () => void 5 | onArrow?: (event: KeyboardEvent) => void 6 | onTab?: () => void 7 | onShiftTab?: () => void 8 | onHome?: () => void 9 | onEnd?: () => void 10 | onFirstLetter?: (key: string) => void 11 | [key: string]: ((...args: any[]) => void) | undefined 12 | } 13 | 14 | export interface IAccessibilityComponent { 15 | wrapper: HTMLElement 16 | handlers: IAccessibilityKeyboardHandlers 17 | isOpened: boolean 18 | name: string 19 | selector: string 20 | context?: HTMLElement 21 | isRegistered: boolean 22 | } 23 | 24 | export interface HSAccessibilityObserver { 25 | registerComponent( 26 | wrapper: HTMLElement, 27 | handlers: IAccessibilityKeyboardHandlers, 28 | isActive?: boolean, 29 | name?: string, 30 | selector?: string, 31 | context?: HTMLElement 32 | ): IAccessibilityComponent 33 | addAllowedKeybinding(key: string): void 34 | removeAllowedKeybinding(key: string): void 35 | getAllowedKeybindings(): string[] 36 | } 37 | -------------------------------------------------------------------------------- /src/themes/claude.css: -------------------------------------------------------------------------------- 1 | color-scheme: "light"; 2 | --color-base-100: oklch(98% 0.01 93.48); 3 | --color-base-200: oklch(94.95% 0.01 87.47); 4 | --color-base-300: oklch(0.8685 0.0109 100.85); 5 | --color-base-content: oklch(34% 0.03 94.42); 6 | --color-primary: oklch(62% 0.14 39.15); 7 | --color-primary-content: oklch(100% 0 0); 8 | --color-secondary: oklch(69% 0.16 290.41); 9 | --color-secondary-content: oklch(14% 0 0); 10 | --color-accent: oklch(42.621% 0.074 224.389); 11 | --color-accent-content: oklch(88.524% 0.014 224.389); 12 | --color-neutral: oklch(20% 0 0); 13 | --color-neutral-content: oklch(98% 0 0); 14 | --color-info: oklch(56% 0.13 42.95); 15 | --color-info-content: oklch(100% 0 0); 16 | --color-success: oklch(60% 0.1 120.29); 17 | --color-success-content: oklch(100% 0 0); 18 | --color-warning: oklch(85% 0.12 80.65); 19 | --color-warning-content: oklch(0% 0 0); 20 | --color-error: oklch(64% 0.25 19.69); 21 | --color-error-content: oklch(100% 0 0); 22 | --radius-selector: 1rem; 23 | --radius-field: 1rem; 24 | --radius-box: 1rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/marshmallow.css: -------------------------------------------------------------------------------- 1 | color-scheme: "light"; 2 | --color-base-100: oklch(1 0 0); 3 | --color-base-200: oklch(0.9726 0.0086 264.52); 4 | --color-base-300: oklch(0.8047 0 0); 5 | --color-base-content: oklch(0.22 0 0); 6 | --color-primary: oklch(0.8 0.14 348.82); 7 | --color-primary-content: oklch(0 0 0); 8 | --color-secondary: oklch(0.7481 0.1406 97.42); 9 | --color-secondary-content: oklch(0 0 0); 10 | --color-accent: oklch(0.83 0.09 247.96); 11 | --color-accent-content: oklch(0 0 0); 12 | --color-neutral: oklch(0.8324 0.0531 231.64); 13 | --color-neutral-content: oklch(0.22 0 0); 14 | --color-info: oklch(89% 0.057 293.283); 15 | --color-info-content: oklch(54% 0.281 293.009); 16 | --color-success: oklch(84% 0.238 128.85); 17 | --color-success-content: oklch(0 0 0); 18 | --color-warning: oklch(0.7987 0.1642 73.09); 19 | --color-warning-content: oklch(0 0 0); 20 | --color-error: oklch(70% 0.191 22.216); 21 | --color-error-content: oklch(0 0 0); 22 | --radius-selector: 0.25rem; 23 | --radius-field: 0.25rem; 24 | --radius-box: 0.25rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /functions/generateThemeFiles.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { getFileNames } from './getFileNames.js' 4 | 5 | export const wrapContent = (themeName, content) => { 6 | if (themeName === 'light') { 7 | return `:root,:root:has(input.theme-controller[value=${themeName}]:checked),[data-theme="${themeName}"] { 8 | ${content}} 9 | ` 10 | } 11 | 12 | return `:root:has(input.theme-controller[value=${themeName}]:checked),[data-theme="${themeName}"] { 13 | ${content}} 14 | ` 15 | } 16 | 17 | export const generateThemeFiles = async ({ srcDir, distDir }) => { 18 | const themeNames = await getFileNames(srcDir, '.css') 19 | 20 | const tasks = themeNames.map(async themeName => { 21 | const srcPath = path.join(srcDir, `${themeName}.css`) 22 | const distPath = path.join(distDir, `${themeName}.css`) 23 | 24 | const content = await fs.readFile(srcPath, 'utf-8') 25 | const wrappedContent = wrapContent(themeName, content) 26 | 27 | await fs.mkdir(path.dirname(distPath), { recursive: true }) 28 | await fs.writeFile(distPath, wrappedContent) 29 | }) 30 | 31 | await Promise.all(tasks) 32 | } 33 | -------------------------------------------------------------------------------- /src/themes/pastel.css: -------------------------------------------------------------------------------- 1 | color-scheme: "dark"; 2 | --color-base-100: oklch(0.28 0.0292 308.75); 3 | --color-base-200: oklch(0.2182 0.0098 52.95); 4 | --color-base-300: oklch(0.3467 0.0053 301.25); 5 | --color-base-content: oklch(98% 0.003 247.858); 6 | --color-primary: oklch(79% 0.12 295.97); 7 | --color-primary-content: oklch(100% 0 0); 8 | --color-secondary: oklch(91% 0.05 306.07); 9 | --color-secondary-content: oklch(45% 0.03 257.68); 10 | --color-accent: oklch(72% 0.2 210); 11 | --color-accent-content: oklch(37% 0.03 259.73); 12 | --color-neutral: oklch(1 0 0); 13 | --color-neutral-content: oklch(14% 0.004 49.25); 14 | --color-info: oklch(50% 0.25 255.25); 15 | --color-info-content: oklch(100% 0 0); 16 | --color-success: oklch(69% 0.17 162.48); 17 | --color-success-content: oklch(100% 0 0); 18 | --color-warning: oklch(79% 0.184 86.047); 19 | --color-warning-content: oklch(0% 0 0); 20 | --color-error: oklch(64% 0.246 16.439); 21 | --color-error-content: oklch(100% 0 0); 22 | --radius-selector: 2rem; 23 | --radius-field: 2rem; 24 | --radius-box: 2rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/perplexity.css: -------------------------------------------------------------------------------- 1 | color-scheme: "light"; 2 | --color-base-100: oklch(95.36% 0.011 196.98); 3 | --color-base-200: oklch(0.9393 0.0096 196.99); 4 | --color-base-300: oklch(85.94% 0.009 258.34); 5 | --color-base-content: oklch(23.72% 0.034 208.26); 6 | --color-primary: oklch(72% 0.12 210.36); 7 | --color-primary-content: oklch(100% 0 0); 8 | --color-secondary: oklch(66.11% 0.123 228.63); 9 | --color-secondary-content: oklch(98% 0 0); 10 | --color-accent: oklch(67% 0.182 276.935); 11 | --color-accent-content: oklch(25% 0.09 281.288); 12 | --color-neutral: oklch(14% 0 0); 13 | --color-neutral-content: oklch(100% 0 0); 14 | --color-info: oklch(58% 0.1 218.29); 15 | --color-info-content: oklch(100% 0 0); 16 | --color-success: oklch(73% 0.14 118.63); 17 | --color-success-content: oklch(100% 0 0); 18 | --color-warning: oklch(85% 0.16 72.74); 19 | --color-warning-content: oklch(0% 0 0); 20 | --color-error: oklch(64% 0.21 25.39); 21 | --color-error-content: oklch(100% 0 0); 22 | --radius-selector: 1rem; 23 | --radius-field: 1rem; 24 | --radius-box: 1rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/spotify.css: -------------------------------------------------------------------------------- 1 | color-scheme: "dark"; 2 | --color-base-100: oklch(0.1964 0.0168 268.77); 3 | --color-base-200: oklch(9.3% 0.007 145); 4 | --color-base-300: oklch(55% 0.046 257.417); 5 | --color-base-content: oklch(98% 0.003 247.858); 6 | --color-primary: oklch(70% 0.18 153.85); 7 | --color-primary-content: oklch(1 0 0); 8 | --color-secondary: oklch(98% 0.031 120.757); 9 | --color-secondary-content: oklch(27% 0.072 132.109); 10 | --color-accent: oklch(45% 0.05 250.05); 11 | --color-accent-content: oklch(95% 0.01 238.46); 12 | --color-neutral: oklch(1 0 0); 13 | --color-neutral-content: oklch(14% 0 0); 14 | --color-info: oklch(60% 0.1 269.83); 15 | --color-info-content: oklch(95% 0.01 238.46); 16 | --color-success: oklch(72% 0.12 201.79); 17 | --color-success-content: oklch(95% 0.01 238.46); 18 | --color-warning: oklch(80% 0.1 100.65); 19 | --color-warning-content: oklch(0% 0 0); 20 | --color-error: oklch(64% 0.25 19.69); 21 | --color-error-content: oklch(95% 0.01 238.46); 22 | --radius-selector: 1rem; 23 | --radius-field: 1rem; 24 | --radius-box: 1rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/vscode.css: -------------------------------------------------------------------------------- 1 | color-scheme: "dark"; 2 | --color-base-100: oklch(0.18 0.02 271.27); 3 | --color-base-200: oklch(0.22 0.02 271.67); 4 | --color-base-300: oklch(0.28 0.03 270.91); 5 | --color-base-content: oklch(0.994 0 0); 6 | --color-primary: oklch(0.71 0.15 239.15); 7 | --color-primary-content: oklch(0.94 0.03 232.39); 8 | --color-secondary: oklch(88% 0.059 254.128); 9 | --color-secondary-content: oklch(0 0 0); 10 | --color-accent: oklch(0.5636 0.0173 273.66); 11 | --color-accent-content: oklch(86% 0.022 252.894); 12 | --color-neutral: oklch(86% 0.022 252.894); 13 | --color-neutral-content: oklch(14% 0 0); 14 | --color-info: oklch(0.6 0.1 269.83); 15 | --color-info-content: oklch(0.9 0.01 238.47); 16 | --color-success: oklch(0.7 0.15 159.83); 17 | --color-success-content: oklch(0.9 0.01 238.47); 18 | --color-warning: oklch(0.8 0.1 100.65); 19 | --color-warning-content: oklch(0 0 0); 20 | --color-error: oklch(0.64 0.25 19.69); 21 | --color-error-content: oklch(0.9 0.01 238.47); 22 | --radius-selector: 0rem; 23 | --radius-field: 0rem; 24 | --radius-box: 0rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/components/customOptions.css: -------------------------------------------------------------------------------- 1 | /* custom option */ 2 | .custom-soft-option, 3 | .custom-option { 4 | @apply rounded-box w-full cursor-pointer overflow-hidden border p-4; 5 | border: var(--border) solid var(--input-color, color-mix(in oklab, var(--color-base-content) 40%, #0000)); 6 | 7 | &:hover { 8 | border-color: var(--input-color, color-mix(in oklab, var(--color-base-content) 40%, #0000)); 9 | outline: 1px solid var(--input-color, color-mix(in oklab, var(--color-base-content) 40%, #0000)); 10 | } 11 | 12 | & > .label-text { 13 | @apply p-0; 14 | } 15 | } 16 | 17 | .custom-option { 18 | &:has(:checked) { 19 | border-color: var(--input-color, var(--color-primary)); 20 | outline: 1px solid var(--input-color, var(--color-primary)); 21 | } 22 | } 23 | 24 | .custom-soft-option { 25 | background-color: color-mix(in oklab, var(--input-color, var(--color-neutral)) 5%, #0000); 26 | &:has(:checked) { 27 | border-color: var(--input-color, var(--color-primary)); 28 | background-color: color-mix(in oklab, var(--input-color, var(--color-primary)) 10%, #0000); 29 | outline: 1px solid var(--input-color, var(--color-primary)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /functions/variables.js: -------------------------------------------------------------------------------- 1 | export default { 2 | colors: { 3 | 'base-100': 'var(--color-base-100)', 4 | 'base-200': 'var(--color-base-200)', 5 | 'base-300': 'var(--color-base-300)', 6 | 'base-content': 'var(--color-base-content)', 7 | primary: 'var(--color-primary)', 8 | 'primary-content': 'var(--color-primary-content)', 9 | secondary: 'var(--color-secondary)', 10 | 'secondary-content': 'var(--color-secondary-content)', 11 | accent: 'var(--color-accent)', 12 | 'accent-content': 'var(--color-accent-content)', 13 | neutral: 'var(--color-neutral)', 14 | 'neutral-content': 'var(--color-neutral-content)', 15 | info: 'var(--color-info)', 16 | 'info-content': 'var(--color-info-content)', 17 | success: 'var(--color-success)', 18 | 'success-content': 'var(--color-success-content)', 19 | warning: 'var(--color-warning)', 20 | 'warning-content': 'var(--color-warning-content)', 21 | error: 'var(--color-error)', 22 | 'error-content': 'var(--color-error-content)' 23 | }, 24 | borderRadius: { 25 | selector: 'var(--radius-selector)', 26 | field: 'var(--radius-field)', 27 | box: 'var(--radius-box)' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/themes/black.css: -------------------------------------------------------------------------------- 1 | color-scheme: dark; 2 | --color-base-100: oklch(23.2% 0.006 285.95); 3 | --color-base-200: oklch(14% 0.005 285.823); 4 | --color-base-300: oklch(14% 0 0); 5 | --color-base-content: oklch(96% 0.001 286.375); 6 | --color-primary: oklch(58% 0.233 277.117); 7 | --color-primary-content: oklch(98% 0.022 95.277); 8 | --color-secondary: oklch(60% 0.118 184.704); 9 | --color-secondary-content: oklch(98% 0.014 180.72); 10 | --color-accent: oklch(0.51 0.27 271.36); 11 | --color-accent-content: oklch(100% 0 0); 12 | --color-neutral: oklch(98% 0 0); 13 | --color-neutral-content: oklch(37% 0.044 257.287); 14 | --color-info: oklch(58% 0.158 241.966); 15 | --color-info-content: oklch(97% 0.013 236.62); 16 | --color-success: oklch(64% 0.2 131.684); 17 | --color-success-content: oklch(98% 0.031 120.757); 18 | --color-warning: oklch(55% 0.163 48.998); 19 | --color-warning-content: oklch(98% 0.026 102.212); 20 | --color-error: oklch(63% 0.237 25.331); 21 | --color-error-content: oklch(97% 0.013 17.38); 22 | --radius-selector: 2rem; 23 | --radius-field: 0.25rem; 24 | --radius-box: 0.5rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 2px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/slack.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(97.31% 0 0); 3 | --color-base-200: oklch(93.7% 0 0); 4 | --color-base-300: oklch(38.87% 0.0052 301.28); 5 | --color-base-content: oklch(21% 0.006 56.043); 6 | --color-primary: oklch(30.65% 0.108 327.06); 7 | --color-primary-content: oklch(97% 0.014 254.604); 8 | --color-secondary: oklch(0% 0 0); 9 | --color-secondary-content: oklch(100% 0 0); 10 | --color-accent: oklch(55% 0.016 285.938); 11 | --color-accent-content: oklch(98% 0 0); 12 | --color-neutral: oklch(14% 0.004 49.25); 13 | --color-neutral-content: oklch(98% 0.001 106.423); 14 | --color-info: oklch(76.8% 0.131 223.2); 15 | --color-info-content: oklch(45% 0.085 224.283); 16 | --color-success: oklch(69.11% 0.142 160.24); 17 | --color-success-content: oklch(39% 0.095 152.535); 18 | --color-warning: oklch(79.66% 0.152 82.65); 19 | --color-warning-content: oklch(47% 0.137 46.201); 20 | --color-error: oklch(58.8% 0.222 11.49); 21 | --color-error-content: oklch(97% 0.014 343.198); 22 | --radius-selector: 2rem; 23 | --radius-field: 0.25rem; 24 | --radius-box: 0.5rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /dts-config.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const pluginsDir = './src/js/plugins' 4 | const helpersDir = './src/js/helpers' 5 | const distDir = './dist' 6 | const excludePlugins = ['base-plugin'] 7 | 8 | const outputConfig = { noBanner: true } 9 | 10 | const config = { 11 | compilationOptions: { 12 | preferredConfigPath: './tsconfig.json' 13 | }, 14 | entries: [ 15 | { 16 | filePath: './src/js/index.ts', 17 | outFile: './dist/index.d.ts', 18 | output: outputConfig 19 | }, 20 | ...fs 21 | .readdirSync(pluginsDir) 22 | .map(pluginName => writeFile(pluginsDir, pluginName)) 23 | .filter(Boolean), 24 | ...fs 25 | .readdirSync(helpersDir) 26 | .map(pluginName => writeFile(helpersDir, pluginName, 'helper-')) 27 | .filter(Boolean) 28 | ] 29 | } 30 | 31 | function writeFile(dir, plugin, prefix = '') { 32 | if (!fs.lstatSync(`${dir}/${plugin}`).isDirectory() || excludePlugins.includes(plugin)) { 33 | return null 34 | } 35 | 36 | return { 37 | filePath: `${dir}/${plugin}/index.ts`, 38 | outFile: `${distDir}/${prefix}${plugin}.d.ts`, 39 | output: outputConfig 40 | } 41 | } 42 | 43 | module.exports = config 44 | -------------------------------------------------------------------------------- /src/components/drawer.css: -------------------------------------------------------------------------------- 1 | .drawer { 2 | @apply bg-base-100 shadow-base-300/20 fixed z-80 flex size-full max-w-96 flex-col justify-between shadow-md; 3 | } 4 | 5 | .drawer-header { 6 | @apply flex items-center justify-between p-6; 7 | } 8 | 9 | .drawer-title { 10 | @apply text-base-content text-lg font-medium; 11 | } 12 | 13 | .drawer-body { 14 | @apply grow overflow-x-hidden overflow-y-auto px-6 text-base [&::-webkit-scrollbar]:w-2; 15 | } 16 | 17 | .drawer-body:where(:last-child) { 18 | @apply pb-6; 19 | } 20 | 21 | .drawer-footer { 22 | @apply flex items-center justify-end gap-3 p-6; 23 | } 24 | 25 | .drawer-start { 26 | @apply start-0 end-auto top-0 -translate-x-full transform transition-all duration-300 ease-in-out rtl:translate-x-full; 27 | } 28 | .drawer-end { 29 | @apply start-auto end-0 top-0 translate-x-full transform transition-all duration-300 ease-in-out rtl:-translate-x-full; 30 | } 31 | .drawer-top { 32 | @apply inset-x-0 top-0 max-h-60 max-w-none -translate-y-full transform transition-all duration-300 ease-in-out; 33 | } 34 | .drawer-bottom { 35 | @apply inset-x-0 bottom-0 max-h-60 max-w-none translate-y-full transform transition-all duration-300 ease-in-out; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/fileinput.css: -------------------------------------------------------------------------------- 1 | .input::file-selector-button { 2 | @apply bg-primary text-primary-content me-4 inline-flex h-full shrink-0 cursor-pointer flex-wrap items-center justify-center border-0 px-4 text-center text-base leading-none font-semibold uppercase no-underline select-none; 3 | --input-color: var(--color-primary); 4 | &.input-sm { 5 | @apply text-sm; 6 | } 7 | &.input-md { 8 | @apply text-base; 9 | } 10 | &.input-lg { 11 | @apply text-lg; 12 | } 13 | &.input-xl { 14 | @apply text-xl; 15 | } 16 | } 17 | /* File Input sizes */ 18 | .input-xs::file-selector-button { 19 | @apply text-xs; 20 | } 21 | .input-sm::file-selector-button { 22 | @apply text-sm; 23 | } 24 | .input-md::file-selector-button { 25 | @apply text-base; 26 | } 27 | .input-lg::file-selector-button { 28 | @apply text-lg; 29 | } 30 | .input-xl::file-selector-button { 31 | @apply text-xl; 32 | } 33 | 34 | .input[type="file"] { 35 | @apply overflow-hidden ps-0; 36 | } 37 | 38 | .input-floating .input::file-selector-button { 39 | @apply bg-base-100 text-base-content; 40 | border-inline-end: var(--border) solid color-mix(in oklab, var(--color-base-content) 40%, #0000); 41 | outline: none; 42 | } 43 | -------------------------------------------------------------------------------- /src/themes/valorant.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(99.14% 0.0044 359.99); 3 | --color-base-200: oklch(96.72% 0.0163 12.78); 4 | --color-base-300: oklch(27.67% 0.0779 19.29); 5 | --color-base-content: oklch(21% 0.006 56.043); 6 | --color-primary: oklch(66.77% 0.2199 21.34); 7 | --color-primary-content: oklch(97% 0.013 17.38); 8 | --color-secondary: oklch(30.12% 0 0); 9 | --color-secondary-content: oklch(100% 0 0); 10 | --color-accent: oklch(55% 0.016 285.938); 11 | --color-accent-content: oklch(98% 0 0); 12 | --color-neutral: oklch(20.89% 0.0248 249.09); 13 | --color-neutral-content: oklch(98% 0.001 106.423); 14 | --color-info: oklch(58% 0.158 241.966); 15 | --color-info-content: oklch(98% 0.019 200.873); 16 | --color-success: oklch(76.82% 0.1855 152.24); 17 | --color-success-content: oklch(39% 0.095 152.535); 18 | --color-warning: oklch(80.16% 0.1705 73.27); 19 | --color-warning-content: oklch(47% 0.137 46.201); 20 | --color-error: oklch(67.08% 0.2165 25.19); 21 | --color-error-content: oklch(97% 0.014 343.198); 22 | --radius-selector: 0rem; 23 | --radius-field: 0rem; 24 | --radius-box: 0rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 1; 30 | -------------------------------------------------------------------------------- /src/themes/ghibli.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(0.94 0.0262 82.38); 3 | --color-base-200: oklch(0.91 0.0326 80.99); 4 | --color-base-300: oklch(0.34 0.0131 81.73 / 20%); 5 | --color-base-content: oklch(41% 0.077 79.04); 6 | --color-primary: oklch(0.62 0.0868 111.8); 7 | --color-primary-content: oklch(97% 0.009 106.57); 8 | --color-secondary: oklch(44% 0.043 257.281); 9 | --color-secondary-content: oklch(92% 0.013 255.508); 10 | --color-accent: oklch(70% 0.14 182.503); 11 | --color-accent-content: oklch(98% 0.014 180.72); 12 | --color-neutral: oklch(41% 0.025 282.21); 13 | --color-neutral-content: oklch(98% 0.001 286.38); 14 | --color-info: oklch(62% 0.214 259.815); 15 | --color-info-content: oklch(97% 0.014 254.604); 16 | --color-success: oklch(64% 0.2 131.684); 17 | --color-success-content: oklch(98% 0.031 120.757); 18 | --color-warning: oklch(68% 0.162 75.834); 19 | --color-warning-content: oklch(28% 0.066 53.813); 20 | --color-error: oklch(63% 0.237 25.331); 21 | --color-error-content: oklch(97% 0.013 17.38); 22 | --radius-selector: 0.25rem; 23 | --radius-field: 1rem; 24 | --radius-box: 2rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 0; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /.github/workflows/handle-issue.yml: -------------------------------------------------------------------------------- 1 | name: "🧑🏻‍💻 Handle issue" 2 | 3 | on: 4 | issues: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | handle_new_issue: 9 | if: github.event.action == 'opened' 10 | runs-on: ubuntu-latest 11 | name: Handle new issue 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Greet 16 | uses: badsyntax/github-action-issue-comment@master 17 | if: github.event_name == 'issues' 18 | with: 19 | id: "auto-reply" 20 | action: "create-clean" 21 | template: ".github/workflows/issue-reply.hbs" 22 | issue-number: ${{ github.event.issue.number }} 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | template-inputs: | 25 | { 26 | "user": "${{ github.event.issue.user.login }}" 27 | } 28 | 29 | # Find duplicates 30 | - name: Find duplicates 31 | uses: wow-actions/potential-duplicates@v1.0.9 32 | with: 33 | GITHUB_TOKEN: ${{ github.token }} 34 | label: "" 35 | comment: > 36 | Potential duplicates: {{#issues}} 37 | - #{{ number }} _({{ accuracy }}% Match)_ 38 | {{/issues}} 39 | -------------------------------------------------------------------------------- /src/themes/mintlify.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(98.69% 0.007 145.52); 3 | --color-base-200: oklch(95.89% 0.019 145.43); 4 | --color-base-300: oklch(48.75% 0.0326 146.64); 5 | --color-base-content: oklch(21% 0.006 56.043); 6 | --color-primary: oklch(62% 0.194 149.214); 7 | --color-primary-content: oklch(98% 0.018 155.826); 8 | --color-secondary: oklch(84.42% 0.172 84.934); 9 | --color-secondary-content: oklch(47% 0.137 46.201); 10 | --color-accent: oklch(55% 0.016 285.938); 11 | --color-accent-content: oklch(98% 0 0); 12 | --color-neutral: oklch(26% 0.007 34.298); 13 | --color-neutral-content: oklch(97% 0.001 106.424); 14 | --color-info: oklch(65.52% 0.111 212.17); 15 | --color-info-content: oklch(39% 0.07 227.392); 16 | --color-success: oklch(64.01% 0.175 146.74); 17 | --color-success-content: oklch(26% 0.065 152.934); 18 | --color-warning: oklch(77.03% 0.174 64.05); 19 | --color-warning-content: oklch(47% 0.137 46.201); 20 | --color-error: oklch(59.15% 0.202 21.24); 21 | --color-error-content: oklch(97% 0.014 343.198); 22 | --radius-selector: 2rem; 23 | --radius-field: 1rem; 24 | --radius-box: 0.5rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/gourmet.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(99.45% 0.002 67.8); 3 | --color-base-200: oklch(98.19% 0.008 61.46); 4 | --color-base-300: oklch(47% 0.0131 54.28); 5 | --color-base-content: oklch(32.38% 0.009 67.5); 6 | --color-primary: oklch(70.49% 0.187 47.6); 7 | --color-primary-content: oklch(97.96% 0.016 73.68); 8 | --color-secondary: oklch(55.79% 0.022 301.91); 9 | --color-secondary-content: oklch(95.05% 0.003 308.43); 10 | --color-accent: oklch(65.59% 0.212 354.31); 11 | --color-accent-content: oklch(97.14% 0.014 343.2); 12 | --color-neutral: oklch(32.38% 0.009 67.5); 13 | --color-neutral-content: oklch(99.45% 0.002 67.8); 14 | --color-info: oklch(68.47% 0.148 237.32); 15 | --color-info-content: oklch(97.71% 0.012 236.62); 16 | --color-success: oklch(69.59% 0.149 162.48); 17 | --color-success-content: oklch(97.93% 0.021 166.11); 18 | --color-warning: oklch(57.59% 0.247 287.24); 19 | --color-warning-content: oklch(96.57% 0.017 289.61); 20 | --color-error: oklch(65.39% 0.222 25.86); 21 | --color-error-content: oklch(98.76% 0.009 67.73); 22 | --radius-selector: 2rem; 23 | --radius-field: 2rem; 24 | --radius-box: 0.75rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/light.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(100% 0 0); 3 | --color-base-200: oklch(97.8% 0.005 297.73); 4 | --color-base-300: oklch(37.01% 0.0069 297.49); 5 | --color-base-content: oklch(37.57% 0.022 281.8); 6 | --color-primary: oklch(57.59% 0.247 287.24); 7 | --color-primary-content: oklch(96.57% 0.017 289.61); 8 | --color-secondary: oklch(55.79% 0.022 301.91); 9 | --color-secondary-content: oklch(97.64% 0.001 286.38); 10 | --color-accent: oklch(62.31% 0.188 259.81); 11 | --color-accent-content: oklch(97.05% 0.014 254.6); 12 | --color-neutral: oklch(37.57% 0.0222 281.8); 13 | --color-neutral-content: oklch(0.98 0.0013 286.38); 14 | --color-info: oklch(71.48% 0.126 215.22); 15 | --color-info-content: oklch(98.41% 0.019 200.87); 16 | --color-success: oklch(73.11% 0.217 147.04); 17 | --color-success-content: oklch(98.1% 0.014 174.18); 18 | --color-warning: oklch(79.87% 0.164 73.09); 19 | --color-warning-content: oklch(99.34% 0.011 95.16); 20 | --color-error: oklch(65.39% 0.222 25.86); 21 | --color-error-content: oklch(98.76% 0.009 67.73); 22 | --radius-selector: 0.25rem; 23 | --radius-field: 0.375rem; 24 | --radius-box: 0.5rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 0; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/shadcn.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(100% 0 0); 3 | --color-base-200: oklch(96.74% 0.0013 286.38); 4 | --color-base-300: oklch(0% 0 0); 5 | --color-base-content: oklch(21.03% 0.0059 285.89); 6 | --color-primary: oklch(27.39% 0.0055 286.03); 7 | --color-primary-content: oklch(98.51% 0 0); 8 | --color-secondary: oklch(55.79% 0.0221 301.91); 9 | --color-secondary-content: oklch(97.64% 0.0013 286.38); 10 | --color-accent: oklch(62.31% 0.188 259.81); 11 | --color-accent-content: oklch(97.05% 0.0142 254.6); 12 | --color-neutral: oklch(40.66% 0.0251 282.21); 13 | --color-neutral-content: oklch(97.64% 0.0013 286.38); 14 | --color-info: oklch(71.48% 0.1257 215.22); 15 | --color-info-content: oklch(98.41% 0.0189 200.87); 16 | --color-success: oklch(69.59% 0.1491 162.48); 17 | --color-success-content: oklch(97.93% 0.0207 166.11); 18 | --color-warning: oklch(70.49% 0.1867 47.6); 19 | --color-warning-content: oklch(97.96% 0.0158 73.68); 20 | --color-error: oklch(65.39% 0.2221 25.86); 21 | --color-error-content: oklch(98.76% 0.0085 67.73); 22 | --radius-selector: 0.125rem; 23 | --radius-field: 0.25rem; 24 | --radius-box: 0.375rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/js/plugins/combobox/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IComboBoxOptions { 2 | gap?: number 3 | viewport?: string | HTMLElement | null 4 | preventVisibility?: boolean 5 | minSearchLength?: number 6 | apiUrl?: string | null 7 | apiDataPart?: string | null 8 | apiQuery?: string | null 9 | apiSearchQuery?: string | null 10 | apiSearchPath?: string | null 11 | apiSearchDefaultPath?: string | null 12 | apiHeaders?: {} 13 | apiGroupField?: string | null 14 | outputItemTemplate?: string | null 15 | outputEmptyTemplate?: string | null 16 | outputLoaderTemplate?: string | null 17 | groupingType?: 'default' | 'tabs' | null 18 | groupingTitleTemplate?: string | null 19 | tabsWrapperTemplate?: string | null 20 | preventSelection?: boolean 21 | preventAutoPosition?: boolean 22 | preventClientFiltering?: boolean 23 | isOpenOnFocus?: boolean 24 | keepOriginalOrder?: boolean 25 | preserveSelectionOnEmpty?: boolean 26 | } 27 | 28 | export interface IComboBox { 29 | options?: IComboBoxOptions 30 | 31 | getCurrentData(): {} | {}[] 32 | open(): void 33 | close(): void 34 | recalculateDirection(): void 35 | destroy(): void 36 | } 37 | 38 | export interface IComboBoxItemAttr { 39 | valueFrom: string 40 | attr: string 41 | } 42 | -------------------------------------------------------------------------------- /src/themes/dark.css: -------------------------------------------------------------------------------- 1 | color-scheme: dark; 2 | --color-base-100: oklch(31.23% 0.026 301.24); 3 | --color-base-200: oklch(25.39% 0.025 298.72); 4 | --color-base-300: oklch(23.44% 0.0194 298.63); 5 | --color-base-content: oklch(84.98% 0.014 295.28); 6 | --color-primary: oklch(53.93% 0.271 286.75); 7 | --color-primary-content: oklch(88.22% 0.062 290.17); 8 | --color-secondary: oklch(49.12% 0.021 303.05); 9 | --color-secondary-content: oklch(88.83% 0.007 304.23); 10 | --color-accent: oklch(54.61% 0.215 262.88); 11 | --color-accent-content: oklch(88.23% 0.057 254.13); 12 | --color-neutral: oklch(65.75% 0.022 294.95); 13 | --color-neutral-content: oklch(18.51% 0.017 301.92); 14 | --color-info: oklch(60.89% 0.111 221.72); 15 | --color-info-content: oklch(91.67% 0.077 205.04); 16 | --color-success: oklch(67.35% 0.201 146.84); 17 | --color-success-content: oklch(91.38% 0.069 168.24); 18 | --color-warning: oklch(72.59% 0.152 69.05); 19 | --color-warning-content: oklch(96.07% 0.057 93.2); 20 | --color-error: oklch(59.54% 0.208 26.28); 21 | --color-error-content: oklch(93.4% 0.039 54.86); 22 | --radius-selector: 0.25rem; 23 | --radius-field: 0.375rem; 24 | --radius-box: 0.5rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 0; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/corporate.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(99.43% 0.001 286.38); 3 | --color-base-200: oklch(97.33% 0.008 271.33); 4 | --color-base-300: oklch(48.52% 0.0225 279.09); 5 | --color-base-content: oklch(42.06% 0.049 276.53); 6 | --color-primary: oklch(62.31% 0.188 259.81); 7 | --color-primary-content: oklch(97.05% 0.014 254.6); 8 | --color-secondary: oklch(55.79% 0.022 301.91); 9 | --color-secondary-content: oklch(95.05% 0.003 308.43); 10 | --color-accent: oklch(65.59% 0.212 354.31); 11 | --color-accent-content: oklch(97.14% 0.014 343.2); 12 | --color-neutral: oklch(0.447 0.0732 274.26); 13 | --color-neutral-content: oklch(82.15% 0.055 277.12); 14 | --color-info: oklch(70.38% 0.123 182.5); 15 | --color-info-content: oklch(98.36% 0.014 180.72); 16 | --color-success: oklch(76.81% 0.204 130.85); 17 | --color-success-content: oklch(98.57% 0.031 120.76); 18 | --color-warning: oklch(79.52% 0.162 86.05); 19 | --color-warning-content: oklch(98.73% 0.026 102.21); 20 | --color-error: oklch(64.5% 0.215 16.44); 21 | --color-error-content: oklch(96.94% 0.015 12.42); 22 | --radius-selector: 0.125rem; 23 | --radius-field: 0.25rem; 24 | --radius-box: 0.375rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/luxury.css: -------------------------------------------------------------------------------- 1 | color-scheme: dark; 2 | --color-base-100: oklch(24.97% 0.0235 60.71); 3 | --color-base-200: oklch(20.19% 0.0212 56.18); 4 | --color-base-300: oklch(13.67% 0.0208 68.4); 5 | --color-base-content: oklch(89.79% 0.0323 68.91); 6 | --color-primary: oklch(68.75% 0.0948 67.23); 7 | --color-primary-content: oklch(92.24% 0.0235 69.59); 8 | --color-secondary: oklch(55.79% 0.0221 301.91); 9 | --color-secondary-content: oklch(97.64% 0.0013 286.38); 10 | --color-accent: oklch(64.45% 0.1596 354.64); 11 | --color-accent-content: oklch(89.57% 0.0416 348.79); 12 | --color-neutral: oklch(0.51 0.0449 70.29); 13 | --color-neutral-content: oklch(16.74% 0.0325 59.58); 14 | --color-info: oklch(55.75% 0.0746 234.15); 15 | --color-info-content: oklch(87.94% 0.0196 230.73); 16 | --color-success: oklch(52.65% 0.0537 188.98); 17 | --color-success-content: oklch(84.44% 0.0185 192.54); 18 | --color-warning: oklch(57.37% 0.149 296.3); 19 | --color-warning-content: oklch(88.26% 0.0392 300.25); 20 | --color-error: oklch(50.81% 0.097 4.98); 21 | --color-error-content: oklch(87.99% 0.021 358.74); 22 | --radius-selector: 0.1875rem; 23 | --radius-field: 0.375rem; 24 | --radius-box: 0.5625rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/themes/soft.css: -------------------------------------------------------------------------------- 1 | color-scheme: light; 2 | --color-base-100: oklch(98.8% 0.0069 304.24); 3 | --color-base-200: oklch(96.95% 0.0114 308.33); 4 | --color-base-300: oklch(40.47% 0.0142 308.07); 5 | --color-base-content: oklch(32.61% 0.0705 305.29); 6 | --color-primary: oklch(62.68% 0.2325 303.9); 7 | --color-primary-content: oklch(97.68% 0.0142 308.3); 8 | --color-secondary: oklch(55.79% 0.0221 301.91); 9 | --color-secondary-content: oklch(97.64% 0.0013 286.38); 10 | --color-accent: oklch(62.31% 0.188 259.81); 11 | --color-accent-content: oklch(97.05% 0.0142 254.6); 12 | --color-neutral: oklch(32.61% 0.0705 305.29); 13 | --color-neutral-content: oklch(99.54% 0.0028 308.43); 14 | --color-info: oklch(71.48% 0.1257 215.22); 15 | --color-info-content: oklch(98.41% 0.0189 200.87); 16 | --color-success: oklch(69.59% 0.1491 162.48); 17 | --color-success-content: oklch(97.93% 0.0207 166.11); 18 | --color-warning: oklch(70.49% 0.1867 47.6); 19 | --color-warning-content: oklch(97.96% 0.0158 73.68); 20 | --color-error: oklch(65.39% 0.2221 25.86); 21 | --color-error-content: oklch(98.76% 0.0085 67.73); 22 | --radius-selector: 0.5rem; 23 | --radius-field: 0.75rem; 24 | --radius-box: 1rem; 25 | --size-selector: 0.25rem; 26 | --size-field: 0.25rem; 27 | --border: 1px; 28 | --depth: 1; 29 | --noise: 0; 30 | -------------------------------------------------------------------------------- /src/components/dropdown.css: -------------------------------------------------------------------------------- 1 | .dropdown-menu { 2 | @apply bg-base-100 rounded-box shadow-base-300/20 z-10 mt-2 space-y-1 p-2 text-base text-nowrap opacity-0 shadow-md transition-[opacity,margin] duration-300; 3 | } 4 | 5 | .dropdown-item { 6 | @apply text-base-content rounded-field clear-both flex w-full items-center gap-x-2 bg-transparent px-4 py-2.5 no-underline decoration-0; 7 | text-align: inherit; 8 | &:hover:not(.dropdown-active), 9 | &:focus:not(.dropdown-active), 10 | &:focus-within:not(.dropdown-active), 11 | &:focus-visible:not(.dropdown-active) { 12 | @apply bg-neutral/10 outline-none; 13 | } 14 | 15 | &.dropdown-disabled, 16 | &:disabled, 17 | &[disabled] { 18 | @apply bg-neutral/5 text-base-content/50 pointer-events-none; 19 | } 20 | } 21 | 22 | .dropdown-active { 23 | @apply bg-primary/10 text-primary; 24 | } 25 | 26 | .dropdown-header, 27 | .dropdown-footer { 28 | @apply text-base-content/80 border-base-content/20 static flex items-center px-4 py-2.5; 29 | } 30 | 31 | .dropdown-header { 32 | @apply rounded-t-box -m-2 mb-2 border-b; 33 | } 34 | 35 | .dropdown-footer { 36 | @apply rounded-b-box -mx-2 !-mb-2 border-t; 37 | } 38 | 39 | .dropdown-title { 40 | @apply text-base-content/50 block px-5 py-1.5 text-xs font-medium uppercase; 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-issue.yml: -------------------------------------------------------------------------------- 1 | name: 📕 Documentation Issue 2 | description: If you found an issue on flyonui.com website 3 | title: "docs: " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please first search in [existing issues](https://github.com/themeselection/flyonui/issues?q=is%3Aissue) and [discussion forum](https://github.com/themeselection/flyonui/discussions) and make sure this issue is not addressed before. 9 | - type: input 10 | id: page 11 | attributes: 12 | label: On which page do you see this issue? 13 | placeholder: "example: https://flyonui.com/" 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: description 18 | attributes: 19 | label: Describe the issue 20 | description: Explain the issue, including the circumstances under which it occurs, and provide screenshots if possible. 21 | validations: 22 | required: true 23 | 24 | - type: dropdown 25 | id: browsers 26 | attributes: 27 | label: What browsers are you seeing the problem on? 28 | multiple: true 29 | options: 30 | - All browsers 31 | - Chrome 32 | - Chrome Android 33 | - Safari 34 | - Safari iOS 35 | - Firefox 36 | - Edge 37 | - Other 38 | -------------------------------------------------------------------------------- /functions/minify.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { transform } from 'lightningcss' 4 | 5 | export const minify = async filePath => { 6 | if (!fs.existsSync(filePath)) { 7 | return 8 | } 9 | const css = await fs.promises.readFile(filePath, 'utf8') 10 | try { 11 | const { code } = transform({ 12 | filename: filePath, 13 | code: Buffer.from(css), 14 | minify: true 15 | }) 16 | const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) 17 | const modifiedCode = 18 | `${atob('Lyoh')} ${decodeURIComponent('%F0%9F%9A%80')} ${atob('Zmx5b251aQ==')} ${packageJson.version} ${atob('LSBNSVQgTGljZW5zZSAqLw==')} ` + 19 | code 20 | await fs.promises.writeFile(filePath, modifiedCode) 21 | } catch (error) { 22 | throw new Error(`${filePath}:${error?.loc?.line}: ${error.message}`) 23 | } 24 | } 25 | 26 | export const minifyCssInDirectory = async directories => { 27 | await Promise.all( 28 | directories.map(async dir => { 29 | const directory = path.join(dir) 30 | const files = fs 31 | .readdirSync(directory) 32 | .filter(file => path.extname(file).toLowerCase() === '.css') 33 | .map(file => path.join(directory, file)) 34 | 35 | await Promise.all(files.map(minify)) 36 | }) 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /package 2 | /base 3 | /colors 4 | /components 5 | /utilities 6 | /theme 7 | 8 | /imports.js 9 | 10 | /flyonui.css 11 | /chunks.css 12 | /themes.css 13 | /dist 14 | 15 | 16 | # Dependency directories 17 | node_modules/ 18 | jspm_packages/ 19 | yarn.lock 20 | pnpm-lock.yaml 21 | package-lock.json 22 | bun.lock 23 | 24 | # System Files 25 | .DS_Store 26 | Thumbs.db 27 | 28 | # Runtime data 29 | pids 30 | *.pid 31 | *.seed 32 | *.pid.lock 33 | 34 | # Directory for instrumented libs generated by jscoverage/JSCover 35 | lib-cov 36 | 37 | # Coverage directory used by tools like istanbul 38 | coverage 39 | 40 | # nyc test coverage 41 | .nyc_output 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # IDEs and editors 47 | .idea 48 | .project 49 | .classpath 50 | .c9/ 51 | *.launch 52 | .settings/ 53 | *.sublime-workspace 54 | 55 | # Logs 56 | logs 57 | *.log 58 | npm-debug.log* 59 | yarn-debug.log* 60 | yarn-error.log* 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | 80 | # misc 81 | .sass-cache 82 | connect.lock 83 | typings 84 | .unlighthouse 85 | -------------------------------------------------------------------------------- /src/components/indicator.css: -------------------------------------------------------------------------------- 1 | .indicator { 2 | @apply relative inline-flex; 3 | width: max-content; 4 | 5 | :where(.indicator-item) { 6 | z-index: 1; 7 | @apply absolute whitespace-nowrap; 8 | top: var(--indicator-t, 0); 9 | bottom: var(--indicator-b, auto); 10 | inset-inline-start: var(--indicator-s, auto); 11 | inset-inline-end: var(--indicator-e, 0); 12 | translate: var(--indicator-x, 50%) var(--indicator-y, -50%); 13 | [dir="rtl"] & { 14 | --indicator-x: -50%; 15 | } 16 | } 17 | } 18 | 19 | .indicator-start { 20 | --indicator-s: 0; 21 | --indicator-e: auto; 22 | --indicator-x: -50%; 23 | [dir="rtl"] & { 24 | --indicator-x: 50%; 25 | } 26 | } 27 | 28 | .indicator-center { 29 | --indicator-s: 50%; 30 | --indicator-e: 50%; 31 | --indicator-x: -50%; 32 | [dir="rtl"] & { 33 | --indicator-x: 50%; 34 | } 35 | } 36 | 37 | .indicator-end { 38 | --indicator-s: auto; 39 | --indicator-e: 0; 40 | --indicator-x: 50%; 41 | [dir="rtl"] & { 42 | --indicator-x: -50%; 43 | } 44 | } 45 | 46 | .indicator-bottom { 47 | --indicator-t: auto; 48 | --indicator-b: 0; 49 | --indicator-y: 50%; 50 | } 51 | 52 | .indicator-middle { 53 | --indicator-t: 50%; 54 | --indicator-b: 50%; 55 | --indicator-y: -50%; 56 | } 57 | 58 | .indicator-top { 59 | --indicator-t: 0; 60 | --indicator-b: auto; 61 | --indicator-y: -50%; 62 | } 63 | -------------------------------------------------------------------------------- /src/components/stat.css: -------------------------------------------------------------------------------- 1 | .stats { 2 | @apply rounded-box bg-base-100 border-base-content/20 inline-grid grid-flow-col divide-x divide-y-0 overflow-x-auto border-solid; 3 | box-shadow: var( 4 | --card-shadow, 5 | 0 1px 3px 0 color-mix(in oklab, var(--color-base-300) 20%, #0000), 6 | 0 1px 2px -1px color-mix(in oklab, var(--color-base-300) 20%, #0000) 7 | ); 8 | border-width: var(--card-border, 0px); 9 | 10 | &:where(.stats-border) { 11 | border: var(--border) solid color-mix(in oklab, var(--color-base-content) 20%, transparent); 12 | } 13 | } 14 | 15 | .stat { 16 | @apply inline-grid w-full gap-x-4 gap-y-1 px-6 py-4; 17 | grid-template-columns: repeat(1, 1fr); 18 | border-color: inherit; 19 | } 20 | .stat-figure { 21 | @apply col-start-2 row-span-3 row-start-1 place-self-center justify-self-end; 22 | } 23 | .stat-title { 24 | @apply text-base-content/80 col-start-1 whitespace-nowrap; 25 | } 26 | .stat-value { 27 | @apply text-base-content col-start-1 text-3xl font-semibold whitespace-nowrap; 28 | } 29 | .stat-desc { 30 | @apply text-base-content/50 col-start-1 text-xs whitespace-nowrap; 31 | } 32 | .stat-actions { 33 | @apply col-start-1 mt-3 whitespace-nowrap; 34 | } 35 | 36 | .stats-horizontal { 37 | @apply grid-flow-col divide-x divide-y-0 overflow-x-auto; 38 | } 39 | 40 | .stats-vertical { 41 | @apply grid-flow-row divide-x-0 divide-y overflow-y-auto; 42 | } 43 | -------------------------------------------------------------------------------- /functions/generateThemes.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { getFileNames } from './getFileNames.js' 4 | import themeOrder from './themeOrder.js' 5 | 6 | const readFileContent = async filePath => { 7 | return await fs.readFile(filePath, 'utf8') 8 | } 9 | 10 | const readAllThemeCSS = async () => { 11 | // Get all file names in the ./theme folder with the .css extension 12 | const themeDirs = await getFileNames('./theme', '.css', false) 13 | 14 | // Read the content of each theme CSS file and store in an object 15 | const themeContents = {} 16 | await Promise.all( 17 | themeDirs.map(async themeDir => { 18 | const content = await readFileContent(path.join('./theme', `${themeDir}.css`)) 19 | themeContents[themeDir] = content 20 | }) 21 | ) 22 | 23 | // Sort themes according to the specified order 24 | const sortedThemeContents = themeOrder.filter(theme => themeDirs.includes(theme)).map(theme => themeContents[theme]) 25 | 26 | return sortedThemeContents.join('\n') 27 | } 28 | 29 | export const generateThemes = async outputFile => { 30 | try { 31 | // Read all theme CSS files 32 | const themeContent = await readAllThemeCSS() 33 | 34 | // Write the combined theme content to the output file 35 | await fs.writeFile(outputFile, themeContent) 36 | } catch (error) { 37 | throw new Error('Error generating themes:', error) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/vendor/waves.css: -------------------------------------------------------------------------------- 1 | .waves-effect { 2 | display: inline-flex; 3 | } 4 | 5 | .waves-effect .waves-ripple { 6 | background: radial-gradient( 7 | var(--wave-color) 0, 8 | var(--wave-color) 40%, 9 | var(--wave-color) 50%, 10 | var(--wave-color) 60%, 11 | var(--wave-color) 70% 12 | ); 13 | } 14 | 15 | @layer components { 16 | .waves { 17 | --wave-color: color-mix(in oklab, var(--color-neutral) 20%, #0000); 18 | .waves-ripple { 19 | background-color: var(--wave-color); 20 | } 21 | 22 | &.waves-primary .waves-ripple { 23 | --wave-color: color-mix(in oklab, var(--color-primary) 20%, #0000); 24 | } 25 | &.waves-secondary .waves-ripple { 26 | --wave-color: color-mix(in oklab, var(--color-secondary) 20%, #0000); 27 | } 28 | &.waves-accent .waves-ripple { 29 | --wave-color: color-mix(in oklab, var(--color-accent) 20%, #0000); 30 | } 31 | &.waves-info .waves-ripple { 32 | --wave-color: color-mix(in oklab, var(--color-info) 20%, #0000); 33 | } 34 | &.waves-success .waves-ripple { 35 | --wave-color: color-mix(in oklab, var(--color-success) 20%, #0000); 36 | } 37 | &.waves-warning .waves-ripple { 38 | --wave-color: color-mix(in oklab, var(--color-warning) 20%, #0000); 39 | } 40 | &.waves-error .waves-ripple { 41 | --wave-color: color-mix(in oklab, var(--color-error) 20%, #0000); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /variants.css: -------------------------------------------------------------------------------- 1 | /* Preline */ 2 | @import "./src/js/plugins/dropdown/variants.css"; 3 | @import "./src/js/plugins/remove-element/variants.css"; 4 | @import "./src/js/plugins/tooltip/variants.css"; 5 | @import "./src/js/plugins/accordion/variants.css"; 6 | @import "./src/js/plugins/tree-view/variants.css"; 7 | @import "./src/js/plugins/collapse/variants.css"; 8 | @import "./src/js/plugins/tabs/variants.css"; 9 | @import "./src/js/plugins/overlay/variants.css"; 10 | @import "./src/js/plugins/scrollspy/variants.css"; 11 | @import "./src/js/plugins/carousel/variants.css"; 12 | @import "./src/js/plugins/select/variants.css"; 13 | @import "./src/js/plugins/input-number/variants.css"; 14 | @import "./src/js/plugins/pin-input/variants.css"; 15 | @import "./src/js/plugins/strong-password/variants.css"; 16 | @import "./src/js/plugins/stepper/variants.css"; 17 | @import "./src/js/plugins/combobox/variants.css"; 18 | @import "./src/js/plugins/datatable/variants.css"; 19 | @import "./src/js/plugins/range-slider/variants.css"; 20 | @import "./src/js/plugins/file-upload/variants.css"; 21 | 22 | /* Sortable.js */ 23 | @custom-variant dragged { 24 | &.dragged { 25 | @slot; 26 | } 27 | } 28 | 29 | /* States */ 30 | @custom-variant success { 31 | &.success { 32 | @slot; 33 | } 34 | 35 | .success & { 36 | @slot; 37 | } 38 | } 39 | 40 | @custom-variant error { 41 | &.error { 42 | @slot; 43 | } 44 | 45 | .error & { 46 | @slot; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/js/constants.ts: -------------------------------------------------------------------------------- 1 | export const POSITIONS: any = { 2 | auto: 'auto', 3 | 'auto-start': 'auto-start', 4 | 'auto-end': 'auto-end', 5 | top: 'top', 6 | 'top-start': 'top-start', 7 | 'top-end': 'top-end', 8 | bottom: 'bottom', 9 | 'bottom-start': 'bottom-start', 10 | 'bottom-end': 'bottom-end', 11 | right: 'right', 12 | 'right-start': 'right-start', 13 | 'right-end': 'right-end', 14 | left: 'left', 15 | 'left-start': 'left-start', 16 | 'left-end': 'left-end' 17 | } 18 | 19 | export const DROPDOWN_ACCESSIBILITY_KEY_SET = [ 20 | 'Escape', 21 | 'ArrowUp', 22 | 'ArrowDown', 23 | 'ArrowRight', 24 | 'ArrowLeft', 25 | 'Home', 26 | 'End', 27 | 'Enter' 28 | ] 29 | 30 | export const OVERLAY_ACCESSIBILITY_KEY_SET = ['Escape', 'Tab'] 31 | 32 | export const TABS_ACCESSIBILITY_KEY_SET = ['ArrowUp', 'ArrowLeft', 'ArrowDown', 'ArrowRight', 'Home', 'End'] 33 | 34 | export const SELECT_ACCESSIBILITY_KEY_SET = [ 35 | 'ArrowUp', 36 | 'ArrowLeft', 37 | 'ArrowDown', 38 | 'ArrowRight', 39 | 'Home', 40 | 'End', 41 | 'Escape', 42 | 'Enter', 43 | 'Space', 44 | 'Tab' 45 | ] 46 | 47 | export const COMBO_BOX_ACCESSIBILITY_KEY_SET = [ 48 | 'ArrowUp', 49 | 'ArrowLeft', 50 | 'ArrowDown', 51 | 'ArrowRight', 52 | 'Home', 53 | 'End', 54 | 'Escape', 55 | 'Enter' 56 | ] 57 | 58 | export const BREAKPOINTS: any = { 59 | xs: 0, 60 | sm: 640, 61 | md: 768, 62 | lg: 1024, 63 | xl: 1280, 64 | '2xl': 1536 65 | } 66 | -------------------------------------------------------------------------------- /src/components/radialprogress.css: -------------------------------------------------------------------------------- 1 | .radial-progress { 2 | @apply text-base-content relative box-content inline-grid place-content-center rounded-full font-medium; 3 | vertical-align: middle; 4 | height: var(--size); 5 | width: var(--size); 6 | --value: 0; 7 | --size: 5rem; 8 | --thickness: calc(var(--size) / 10); 9 | --radialprogress: calc(var(--value) * 1%); 10 | transition: --radialprogress 0.3s linear; 11 | 12 | &:before { 13 | @apply absolute inset-0 rounded-full; 14 | content: ""; 15 | background: 16 | radial-gradient(farthest-side, currentColor 98%, #0000) top/var(--thickness) var(--thickness) no-repeat, 17 | conic-gradient( 18 | currentColor var(--radialprogress), 19 | color-mix(in oklab, var(--color-base-content) 20%, transparent) 0 20 | ); 21 | 22 | -webkit-mask: radial-gradient( 23 | farthest-side, 24 | #0000 calc(100% - var(--thickness)), 25 | #000 calc(100% + 0.5px - var(--thickness)) 26 | ); 27 | mask: radial-gradient( 28 | farthest-side, 29 | #0000 calc(100% - var(--thickness)), 30 | #000 calc(100% + 0.5px - var(--thickness)) 31 | ); 32 | } 33 | 34 | &:after { 35 | @apply absolute rounded-full bg-current; 36 | transition: transform 0.3s linear; 37 | content: ""; 38 | inset: calc(50% - var(--thickness) / 2); 39 | transform: rotate(calc(var(--value) * 3.6deg - 90deg)) translate(calc(var(--size) / 2 - 50%)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/tooltip.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | @apply inline-block text-center; 3 | } 4 | 5 | .tooltip-content { 6 | @apply invisible absolute inline-block p-2 opacity-0 transition-opacity; 7 | width: max-content; 8 | z-index: 20; 9 | } 10 | 11 | .tooltip-body { 12 | --tooltip-color: var(--color-neutral); 13 | --tooltip-text-color: var(--color-neutral-content); 14 | @apply rounded-selector shadow-base-300/20 px-3 py-1 text-sm leading-5 shadow-md; 15 | background-color: var(--tooltip-color); 16 | color: var(--tooltip-text-color); 17 | } 18 | 19 | .tooltip-primary { 20 | --tooltip-color: var(--color-primary); 21 | --tooltip-text-color: var(--color-primary-content); 22 | } 23 | .tooltip-secondary { 24 | --tooltip-color: var(--color-secondary); 25 | --tooltip-text-color: var(--color-secondary-content); 26 | } 27 | .tooltip-accent { 28 | --tooltip-color: var(--color-accent); 29 | --tooltip-text-color: var(--color-accent-content); 30 | } 31 | .tooltip-info { 32 | --tooltip-color: var(--color-info); 33 | --tooltip-text-color: var(--color-info-content); 34 | } 35 | .tooltip-success { 36 | --tooltip-color: var(--color-success); 37 | --tooltip-text-color: var(--color-success-content); 38 | } 39 | .tooltip-warning { 40 | --tooltip-color: var(--color-warning); 41 | --tooltip-text-color: var(--color-warning-content); 42 | } 43 | .tooltip-error { 44 | --tooltip-color: var(--color-error); 45 | --tooltip-text-color: var(--color-error-content); 46 | } 47 | -------------------------------------------------------------------------------- /functions/themePlugin.js: -------------------------------------------------------------------------------- 1 | import { plugin } from '../functions/plugin.js' 2 | import allThemes from './object.js' 3 | 4 | export default plugin.withOptions((options = {}) => { 5 | return ({ addBase }) => { 6 | const { 7 | name = 'custom-theme', 8 | default: isDefault = false, 9 | prefersdark = false, 10 | 'color-scheme': colorScheme = 'normal', 11 | root = ':root', 12 | ...customThemeTokens 13 | } = options 14 | 15 | let selector = `${root}:has(input.theme-controller[value=${name}]:checked),[data-theme="${name}"]` 16 | if (isDefault) { 17 | selector = `:where(${root}),${selector}` 18 | } 19 | 20 | // Merge custom theme with built-in theme if it exists 21 | let themeTokens = { ...customThemeTokens } 22 | if (allThemes[name]) { 23 | const builtinTheme = allThemes[name] 24 | themeTokens = { 25 | ...builtinTheme, 26 | ...customThemeTokens, 27 | 'color-scheme': colorScheme || builtinTheme.colorScheme 28 | } 29 | } 30 | 31 | const baseStyles = { 32 | [selector]: { 33 | 'color-scheme': themeTokens['color-scheme'] || colorScheme, 34 | ...themeTokens 35 | } 36 | } 37 | 38 | if (prefersdark) { 39 | addBase({ 40 | '@media (prefers-color-scheme: dark)': { 41 | [root]: baseStyles[selector] // Use the configurable root option here 42 | } 43 | }) 44 | } 45 | 46 | addBase(baseStyles) 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /functions/compileAndExtractStyles.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { promises as fs } from 'node:fs' 3 | import { compile } from 'tailwindcss' 4 | 5 | export async function loadThemes() { 6 | const [defaultTheme, theme] = await Promise.all([ 7 | fs.readFile(path.join(import.meta.dirname, '../node_modules/tailwindcss/theme.css'), 'utf-8'), 8 | fs.readFile(path.join(import.meta.dirname, './variables.css'), 'utf-8') 9 | ]) 10 | return { defaultTheme, theme } 11 | } 12 | 13 | export async function compileAndExtractStyles(styleContent, defaultTheme, theme) { 14 | const compiledContent = ( 15 | await compile(` 16 | @layer theme{${defaultTheme}${theme}} 17 | @layer wrapperStart{${styleContent}} 18 | @layer wrapperEnd 19 | `) 20 | ).build([]) 21 | 22 | const startIndex = compiledContent.indexOf('@layer wrapperStart') 23 | const endIndex = compiledContent.indexOf('@layer wrapperEnd') 24 | 25 | if (startIndex === -1 || endIndex === -1) { 26 | throw new Error('Failed to find wrapper layers in compiled content') 27 | } 28 | 29 | const openingBraceIndex = compiledContent.indexOf('{', startIndex) 30 | const closingBraceIndex = compiledContent.lastIndexOf('}', endIndex) 31 | 32 | if (openingBraceIndex === -1 || closingBraceIndex === -1 || openingBraceIndex >= closingBraceIndex) { 33 | throw new Error('Invalid wrapper structure in compiled content') 34 | } 35 | 36 | return compiledContent.substring(openingBraceIndex + 1, closingBraceIndex).trim() 37 | } 38 | -------------------------------------------------------------------------------- /src/js/helpers/apexcharts/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ApexOptions } from 'apexcharts' 2 | 3 | export interface IBuildTooltipHelperOptions { 4 | title: string 5 | valuePrefix: string 6 | isValueDivided: boolean 7 | valuePostfix: string 8 | hasTextLabel: boolean 9 | invertGroup: boolean 10 | labelDivider: string 11 | wrapperClasses: string 12 | wrapperExtClasses: string 13 | seriesClasses: string 14 | seriesExtClasses: string 15 | titleClasses: string 16 | titleExtClasses: string 17 | markerClasses: string 18 | markerExtClasses: string 19 | valueClasses: string 20 | valueExtClasses: string 21 | labelClasses: string 22 | labelExtClasses: string 23 | hasCategory?: boolean 24 | thousandsShortName?: string 25 | } 26 | 27 | export interface IBuildTooltipHelperSingleOptions { 28 | valuePrefix: string 29 | valuePostfix: string 30 | divider: string 31 | wrapperClasses: string 32 | wrapperExtClasses: string 33 | markerClasses: string 34 | markerStyles: string 35 | markerExtClasses: string 36 | valueClasses: string 37 | valueExtClasses: string 38 | } 39 | 40 | export interface IChartProps { 41 | dataPointIndex: number 42 | seriesIndex: number 43 | series: [][] 44 | ctx: { 45 | opts: ApexOptions 46 | } 47 | } 48 | 49 | export interface IChartPropsSeries { 50 | name: string 51 | altValue?: string 52 | data: number[] 53 | } 54 | 55 | export interface IChartDonutProps { 56 | series: IChartPropsSeries[] 57 | seriesIndex: number 58 | w: { 59 | globals: ApexOptions 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/diff.css: -------------------------------------------------------------------------------- 1 | .diff { 2 | @apply relative grid w-full overflow-hidden; 3 | container-type: inline-size; 4 | grid-template-columns: auto 1fr; 5 | } 6 | 7 | .diff-resizer { 8 | @apply top-1/2 z-1 h-1.5 w-[25rem] max-w-[calc(100cqi-1rem)] min-w-4 translate-x-[0.335rem] -translate-y-[0.18rem] scale-y-400 resize-x overflow-hidden opacity-0; 9 | clip-path: inset(calc(100% - 0.75rem) 0 0 calc(100% - 0.75rem)); 10 | } 11 | 12 | .diff-resizer, 13 | .diff-item-1, 14 | .diff-item-2 { 15 | @apply relative col-start-1 row-start-1; 16 | } 17 | 18 | .diff-item-1:after { 19 | @apply bg-primary/40 border-base-100 outline-base-content/5 shadow-base-300/20 pointer-events-none absolute end-px top-1/2 bottom-0 z-1 size-5 rotate-45 border-2 shadow-sm outline -outline-offset-2 backdrop-blur content-['']; 20 | translate: 50% -50%; 21 | backdrop-filter: blur(8px); 22 | } 23 | 24 | .diff-item-2 { 25 | @apply border-base-100 overflow-hidden border-e-2; 26 | } 27 | 28 | .diff-item-1 > *, 29 | .diff-item-2 > * { 30 | @apply pointer-events-none absolute start-0 top-0 bottom-0 h-full w-[100cqi] max-w-none object-cover object-center; 31 | } 32 | 33 | [dir="rtl"] .diff-resizer { 34 | transform-origin: 0 100%; 35 | translate: -0.29rem 0.37rem; 36 | clip-path: inset(calc(100% - 0.75rem) calc(100% - 0.75rem) 0 0); 37 | } 38 | 39 | [dir="rtl"] .diff-item-1:after { 40 | right: auto; 41 | left: 1px; 42 | translate: -50% -50%; 43 | } 44 | 45 | [dir="rtl"] .diff-item-1 > *, 46 | [dir="rtl"] .diff-item-2 > * { 47 | left: auto; 48 | right: 0; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/alert.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | @apply rounded-box text-neutral-content relative w-full p-4; 3 | text-align: start; 4 | background-size: auto, calc(var(--noise) * 100%); 5 | background-image: none, var(--fx-noise); 6 | background-color: var(--alert-bg); 7 | border: var(--border) solid var(--alert-border); 8 | box-shadow: 0 0.1875rem 0 -0.125rem oklch(100% 0 0 / calc(var(--depth) * 0.08)) inset; 9 | --alert-bg: var(--alert-color, var(--color-neutral)); 10 | --alert-border: var(--alert-color, var(--color-neutral)); 11 | 12 | &.alert-outline { 13 | color: var(--alert-color, var(--color-neutral)); 14 | --alert-bg: transparent; 15 | box-shadow: none; 16 | background-image: none; 17 | } 18 | 19 | &.alert-soft { 20 | color: var(--alert-color, var(--color-neutral)); 21 | --alert-bg: color-mix(in oklab, var(--alert-color, var(--color-neutral)) 10%, var(--color-base-100)); 22 | box-shadow: none; 23 | background-image: none; 24 | } 25 | } 26 | 27 | /* alert colors */ 28 | .alert-primary { 29 | @apply text-primary-content; 30 | --alert-color: var(--color-primary); 31 | } 32 | 33 | .alert-info { 34 | @apply text-info-content; 35 | --alert-color: var(--color-info); 36 | } 37 | 38 | .alert-success { 39 | @apply text-success-content; 40 | --alert-color: var(--color-success); 41 | } 42 | 43 | .alert-warning { 44 | @apply text-warning-content; 45 | --alert-color: var(--color-warning); 46 | } 47 | 48 | .alert-error { 49 | @apply text-error-content; 50 | --alert-color: var(--color-error); 51 | } 52 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { version } from './package.json' 2 | import { pluginOptionsHandler } from './functions/pluginOptionsHandler.js' 3 | import { plugin } from './functions/plugin.js' 4 | import variables from './functions/variables.js' 5 | import themesObject from './theme/object.js' 6 | import { base, components, utilities } from './imports.js' 7 | 8 | export default plugin.withOptions( 9 | options => { 10 | return ({ addBase, addComponents, addUtilities }) => { 11 | const { include, exclude, prefix = '' } = pluginOptionsHandler(options, addBase, themesObject, version) 12 | 13 | const shouldIncludeItem = name => { 14 | if (include && exclude) { 15 | return include.includes(name) && !exclude.includes(name) 16 | } 17 | if (include) { 18 | return include.includes(name) 19 | } 20 | if (exclude) { 21 | return !exclude.includes(name) 22 | } 23 | return true 24 | } 25 | 26 | Object.entries(base).forEach(([name, item]) => { 27 | if (!shouldIncludeItem(name)) return 28 | item({ addBase, prefix }) 29 | }) 30 | 31 | Object.entries(components).forEach(([name, item]) => { 32 | if (!shouldIncludeItem(name)) return 33 | item({ addComponents, prefix }) 34 | }) 35 | 36 | Object.entries(utilities).forEach(([name, item]) => { 37 | if (!shouldIncludeItem(name)) return 38 | item({ addUtilities, prefix }) 39 | }) 40 | } 41 | }, 42 | () => ({ 43 | theme: { 44 | extend: variables 45 | } 46 | }) 47 | ) 48 | -------------------------------------------------------------------------------- /functions/copyFile.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, mock } from 'bun:test' 2 | import path from 'node:path' 3 | import { copyFile } from './copyFile' 4 | import fs from 'fs/promises' 5 | 6 | // Mock the fs functions 7 | const mockMkdir = mock(async () => {}) 8 | const mockCopyFile = mock(async () => {}) 9 | 10 | fs.mkdir = mockMkdir 11 | fs.copyFile = mockCopyFile 12 | 13 | test('copyFile copies file to destination', async () => { 14 | const from = '/path/to/source/file.txt' 15 | const to = '/path/to/destination/file.txt' 16 | 17 | await copyFile(from, to) 18 | 19 | expect(mockMkdir).toHaveBeenCalledWith(path.dirname(to), { recursive: true }) 20 | expect(mockCopyFile).toHaveBeenCalledWith(from, to) 21 | }) 22 | 23 | test('copyFile copies file to destination with new name', async () => { 24 | const from = '/path/to/source/file.txt' 25 | const to = '/path/to/destination/file.txt' 26 | const newName = 'newFileName.txt' 27 | 28 | await copyFile(from, to, newName) 29 | 30 | const expectedDestPath = path.join(path.dirname(to), newName) 31 | expect(mockMkdir).toHaveBeenCalledWith(path.dirname(to), { recursive: true }) 32 | expect(mockCopyFile).toHaveBeenCalledWith(from, expectedDestPath) 33 | }) 34 | 35 | test('copyFile throws error if copying fails', async () => { 36 | const from = '/path/to/source/file.txt' 37 | const to = '/path/to/destination/file.txt' 38 | 39 | const mockError = new Error('Mock copy error') 40 | mockCopyFile.mockRejectedValueOnce(mockError) 41 | 42 | await expect(copyFile(from, to)).rejects.toThrow(`Error copying file from ${from} to ${to}:`) 43 | }) 44 | -------------------------------------------------------------------------------- /.github/workflows/write-release-notes.yml: -------------------------------------------------------------------------------- 1 | name: "📝 Write Release Notes" 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | flyonuiversion: 7 | type: string 8 | 9 | jobs: 10 | write-release-notes: 11 | timeout-minutes: 5 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 10 18 | clean: false 19 | 20 | - name: Read package version from workflow input 21 | id: package-version 22 | run: | 23 | version=${{ inputs.flyonuiversion }} 24 | echo "version=$version" >> $GITHUB_OUTPUT 25 | 26 | # Check if the version follows semantic versioning (e.g., 1.0.0) 27 | if [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 28 | echo "$version is a stable version" 29 | echo "is-stable=true" >> $GITHUB_OUTPUT 30 | else 31 | echo "$version is not a stable version" 32 | echo "is-stable=false" >> $GITHUB_OUTPUT 33 | fi 34 | 35 | - name: Add release notes to GitHub 36 | uses: softprops/action-gh-release@v2 37 | if: steps.package-version.outputs.is-stable == 'true' 38 | with: 39 | tag_name: v${{ steps.package-version.outputs.version }} 40 | body: | 41 | 📜 Read changelog: https://flyonui.com/changelog/ 42 | 43 | 📦 Install this update: 44 | ```bash 45 | npm i -D flyonui@${{ steps.package-version.outputs.version }} 46 | ``` 47 | 48 | 🙏 Thank you for using flyonui! 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /src/utilities/typography.css: -------------------------------------------------------------------------------- 1 | :root .prose { 2 | --tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, #0000); 3 | --tw-prose-headings: var(--color-base-content); 4 | --tw-prose-lead: var(--tw-prose-body); 5 | --tw-prose-links: var(--color-primary); 6 | --tw-prose-kbd: var(--tw-prose-body); 7 | --tw-prose-bold: var(--tw-prose-headings); 8 | --tw-prose-counters: var(--tw-prose-headings); 9 | --tw-prose-bullets: color-mix(in oklab, var(--color-base-content) 50%, #0000); 10 | --tw-prose-hr: color-mix(in oklab, var(--color-base-content) 20%, #0000); 11 | --tw-prose-quotes: var(--tw-prose-headings); 12 | --tw-prose-quote-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); 13 | --tw-prose-captions: color-mix(in oklab, var(--color-base-content) 40%, #0000); 14 | --tw-prose-code: #e83e8c; 15 | --tw-prose-pre-code: var(--color-neutral-content); 16 | --tw-prose-pre-bg: var(--color-neutral); 17 | --tw-prose-th-borders: color-mix(in oklab, var(--color-base-content) 24%, #0000); 18 | --tw-prose-td-borders: var(--tw-prose-th-borders); 19 | line-height: 1.5rem; 20 | } 21 | .prose { 22 | & :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::before, 23 | & :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *))::after { 24 | display: none; 25 | } 26 | pre { 27 | code { 28 | border-radius: 0; 29 | padding: 0; 30 | } 31 | } 32 | & :where(code):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 33 | font-weight: 500; 34 | font-size: 0.875rem; 35 | } 36 | 37 | & :where(kbd):not(:where([class~="not-prose"], [class~="not-prose"] *)) { 38 | @apply p-0 shadow-none; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/pinInput.css: -------------------------------------------------------------------------------- 1 | .pin-input { 2 | @apply border-base-content/25 bg-base-100 placeholder:text-base-content/80 rounded-field border text-center text-base; 3 | height: var(--size); 4 | width: var(--size); 5 | transition: 6 | border-color 0.15s ease-in-out, 7 | box-shadow 0.15s ease-in-out; 8 | --size: calc(var(--size-field, 0.25rem) * 9.5); 9 | 10 | &:hover:not(:focus, :focus-within) { 11 | @apply border-base-content/60; 12 | } 13 | 14 | &:focus, 15 | &:focus-within { 16 | @apply border-primary shadow-xs; 17 | --tw-shadow-color: color-mix(in oklab, var(--color-primary) 30%, #0000); 18 | outline: 1px solid var(--color-primary); 19 | isolation: isolate; 20 | } 21 | 22 | &.disabled, 23 | &:disabled, 24 | &[disabled] { 25 | @apply border-base-content/25 bg-base-content/5 placeholder-base-content/40 text-base-content/50 pointer-events-none; 26 | } 27 | } 28 | 29 | /* Variant */ 30 | .pin-input-underline { 31 | @apply border-base-content/40 rounded-none border-0 border-b bg-transparent; 32 | 33 | &:focus, 34 | &:focus-within { 35 | @apply border-primary border-b-2 shadow-none outline-none; 36 | } 37 | } 38 | 39 | .pin-input-xs { 40 | @apply text-xs; 41 | --size: calc(var(--size-field, 0.25rem) * 6); 42 | } 43 | 44 | .pin-input-sm { 45 | @apply text-sm; 46 | --size: calc(var(--size-field, 0.25rem) * 7.5); 47 | } 48 | 49 | .pin-input-md { 50 | @apply text-base; 51 | --size: calc(var(--size-field, 0.25rem) * 9.5); 52 | } 53 | 54 | .pin-input-lg { 55 | @apply text-lg; 56 | --size: calc(var(--size-field, 0.25rem) * 11.5); 57 | } 58 | 59 | .pin-input-xl { 60 | @apply text-xl; 61 | --size: calc(var(--size-field, 0.25rem) * 14); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/link.css: -------------------------------------------------------------------------------- 1 | .link { 2 | --link-color: color-mix(in oklab, var(--color-base-content) 80%, #0000); 3 | @apply inline-block cursor-pointer font-medium underline; 4 | color: var(--link-color); 5 | &:hover { 6 | color: color-mix(in oklab, var(--link-color) 80%, #000); 7 | } 8 | &:focus { 9 | @apply outline-none; 10 | } 11 | &:focus-visible { 12 | outline: 2px solid currentColor; 13 | outline-offset: 2px; 14 | } 15 | &.disabled, 16 | &[disabled], 17 | &:disabled { 18 | @apply pointer-events-none opacity-50; 19 | } 20 | } 21 | 22 | .link:where(.link-hover) { 23 | @apply no-underline [@media(hover:hover)]:hover:underline; 24 | } 25 | 26 | .link:where(.link-animated) { 27 | @apply relative no-underline before:pointer-events-none before:absolute before:start-0 before:bottom-0 before:h-px before:w-full before:bg-current before:transition-transform before:duration-300 before:ease-in-out before:content-['']; 28 | } 29 | 30 | .link:where(.link-animated)::before { 31 | transform-origin: 100% 50%; 32 | transform: scale3d(0, 1, 1); 33 | } 34 | 35 | .link:where(.link-animated):hover::before { 36 | transform-origin: 0% 50%; 37 | transform: scale3d(1, 1, 1); 38 | } 39 | 40 | .link-primary { 41 | --link-color: var(--color-primary); 42 | } 43 | .link-secondary { 44 | --link-color: var(--color-secondary); 45 | } 46 | .link-accent { 47 | --link-color: var(--color-accent); 48 | } 49 | .link-neutral { 50 | --link-color: var(--color-neutral); 51 | } 52 | .link-success { 53 | --link-color: var(--color-success); 54 | } 55 | .link-info { 56 | --link-color: var(--color-info); 57 | } 58 | .link-warning { 59 | --link-color: var(--color-warning); 60 | } 61 | .link-error { 62 | --link-color: var(--color-error); 63 | } 64 | -------------------------------------------------------------------------------- /functions/generateChunks.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { getFileNames } from './getFileNames.js' 3 | import themeOrder from './themeOrder.js' 4 | 5 | export const generateChunks = async filename => { 6 | try { 7 | let content = '' 8 | // let content = '@layer base, themes, components, utilities;\n'; 9 | // content += `@import url(https://cdn.jsdelivr.net/npm/tailwindcss@next/preflight.min.css) layer(base);\n`; 10 | 11 | const themes = await getFileNames('./theme', '.css', false) 12 | const allowedThemes = ['light', 'dark'] 13 | themeOrder.forEach(theme => { 14 | if (themes.includes(theme) && allowedThemes.includes(theme)) { 15 | content += `@import url(theme/${theme}.css);\n` 16 | } 17 | }) 18 | 19 | const baseFiles = await getFileNames('./base', '.css', false) 20 | baseFiles.forEach(filePath => { 21 | content += `@import url(base/${filePath}.css);\n` 22 | }) 23 | 24 | const componentFiles = await getFileNames('./components', '.css', false) 25 | componentFiles.forEach(filePath => { 26 | content += `@import url(components/${filePath}.css);\n` 27 | }) 28 | 29 | const utilityFiles = await getFileNames('./utilities', '.css', false) 30 | utilityFiles.forEach(filePath => { 31 | content += `@import url(utilities/${filePath}.css);\n` 32 | }) 33 | 34 | // Load color files with specific ordering 35 | const colorFiles = await getFileNames('./colors', '.css', false) 36 | colorFiles.forEach(filePath => { 37 | content += `@import url(colors/${filePath}.css);\n` 38 | }) 39 | 40 | // Write to file 41 | await fs.writeFile(`./${filename}`, content, 'utf8') 42 | } catch (error) { 43 | throw new Error(`Failed to generate full CSS: ${error.message}`) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | @apply pointer-events-none fixed inset-0 z-80 m-0 grid size-full justify-items-center overflow-x-hidden overflow-y-auto p-4 opacity-0 transition-all; 3 | } 4 | 5 | .modal-dialog { 6 | @apply mx-auto w-full sm:max-w-128; 7 | } 8 | 9 | .modal-content { 10 | @apply bg-base-100 shadow-base-300/20 pointer-events-auto flex flex-col rounded-lg shadow-xl; 11 | max-height: calc(100vh - 2rem); 12 | overscroll-behavior: contain; 13 | } 14 | 15 | .modal-header { 16 | @apply relative flex items-center justify-between p-6; 17 | } 18 | 19 | .modal-title { 20 | @apply text-base-content text-2xl font-semibold; 21 | } 22 | 23 | .modal-body { 24 | @apply overflow-x-hidden overflow-y-auto p-6 text-base font-normal [&::-webkit-scrollbar]:w-2; 25 | } 26 | 27 | .modal-footer { 28 | @apply flex items-center justify-end gap-3 p-6; 29 | } 30 | 31 | :where(.modal-header) + .modal-body { 32 | padding-top: 0; 33 | } 34 | 35 | :where(.modal-body) + .modal-footer { 36 | padding-top: 0; 37 | } 38 | 39 | .modal-top { 40 | @apply place-items-start; 41 | } 42 | .modal-top-start { 43 | @apply justify-start; 44 | } 45 | .modal-top-end { 46 | @apply justify-end; 47 | } 48 | .modal-middle { 49 | @apply place-items-center; 50 | } 51 | .modal-middle-start { 52 | @apply place-items-center justify-start; 53 | } 54 | .modal-middle-end { 55 | @apply place-items-center justify-end; 56 | } 57 | .modal-bottom { 58 | @apply place-items-end; 59 | } 60 | .modal-bottom-start { 61 | @apply place-items-end justify-start; 62 | } 63 | .modal-bottom-end { 64 | @apply place-items-end justify-end; 65 | } 66 | 67 | .modal-dialog-sm { 68 | @apply max-w-96; 69 | } 70 | .modal-dialog-md { 71 | @apply max-w-128; 72 | } 73 | .modal-dialog-lg { 74 | @apply max-w-192; 75 | } 76 | .modal-dialog-xl { 77 | @apply max-w-256; 78 | } 79 | -------------------------------------------------------------------------------- /functions/cssToJs.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs' 2 | import postcss from 'postcss' 3 | import postcssJs from 'postcss-js' 4 | import { compileAndExtractStyles, loadThemes } from './compileAndExtractStyles.js' 5 | import { replaceApplyTrueWithEmptyObject } from './replaceApplyTrueWithEmptyObject.js' 6 | import { cleanCss } from './cleanCss.js' 7 | 8 | // function to convert camelCase to kebab-case 9 | const camelToKebab = str => { 10 | return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase() 11 | } 12 | 13 | // Function to transform object keys from camelCase to kebab-case 14 | const transformKeys = obj => { 15 | if (typeof obj !== 'object' || obj === null) return obj 16 | 17 | if (Array.isArray(obj)) { 18 | return obj.map(transformKeys) 19 | } 20 | 21 | return Object.fromEntries( 22 | Object.entries(obj).map(([key, value]) => [ 23 | camelToKebab(key), 24 | typeof value === 'object' ? transformKeys(value) : value 25 | ]) 26 | ) 27 | } 28 | 29 | export const cssToJs = async cssFile => { 30 | try { 31 | // Read the CSS file 32 | const cssContent = await fs.readFile(cssFile, 'utf-8') 33 | 34 | // Load themes 35 | const { defaultTheme, theme } = await loadThemes() 36 | 37 | // First convert Tailwind CSS to raw CSS 38 | const rawCss = await compileAndExtractStyles(cssContent, defaultTheme, theme) 39 | 40 | // Clean the CSS 41 | const cleanedCss = cleanCss(rawCss) 42 | 43 | // Parse the CSS and convert to JS object 44 | const root = postcss.parse(cleanedCss) 45 | const jsContent = postcssJs.objectify(root) 46 | 47 | const kebabCaseContent = transformKeys(jsContent) 48 | 49 | // Apply any necessary transformations 50 | replaceApplyTrueWithEmptyObject(kebabCaseContent) 51 | 52 | // Return stringified JS object 53 | return JSON.stringify(kebabCaseContent, null, null) 54 | } catch (error) { 55 | throw new Error(`Error converting CSS to JS: ${error.message}`) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/release-new-version.yml: -------------------------------------------------------------------------------- 1 | name: "🎉 Release new version" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish: 8 | name: 🚀 Publish 9 | timeout-minutes: 10 10 | runs-on: ubuntu-latest 11 | env: 12 | runtime: bun 13 | outputs: 14 | flyonuiversion: ${{ steps.package-version.outputs.version }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 1 20 | clean: false 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: "latest" 26 | registry-url: https://registry.npmjs.org 27 | 28 | - name: Setup Bun 29 | uses: oven-sh/setup-bun@v2 30 | 31 | - name: Remove everything first 32 | run: rm -rf ./base/ ./colors/ ./components/ ./dist/ ./theme/ ./utilities/ ./flyonui.css ./flyonui.js ./flyonui.d.ts ./chunks.css ./imports.js ./themes.css node_modules bun.lock 33 | 34 | - name: Install package dependencies 35 | run: ${{ env.runtime }} install 36 | 37 | - name: Build package 38 | run: ${{ env.runtime }} build:css && ${{ env.runtime }} build:js && ${{ env.runtime }} build:mjs && ${{ env.runtime }} build:generate-dts && ${{ env.runtime }} build:flyonui 39 | 40 | - name: Read package version from package.json 41 | id: package-version 42 | run: | 43 | version=$(grep -o '"version": *"[^"]*"' package.json | cut -d'"' -f4) 44 | echo "version=$version" >> $GITHUB_OUTPUT 45 | 46 | - name: Publish package to NPM 47 | if: github.repository == 'themeselection/flyonui' 48 | run: npm publish 49 | env: 50 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 51 | 52 | write-release-notes: 53 | name: 📝 Release notes 54 | needs: publish 55 | uses: ./.github/workflows/write-release-notes.yml 56 | secrets: inherit 57 | with: 58 | flyonuiversion: ${{ needs.publish.outputs.flyonuiversion }} 59 | -------------------------------------------------------------------------------- /src/components/swap.css: -------------------------------------------------------------------------------- 1 | .swap { 2 | @apply relative inline-grid cursor-pointer place-content-center align-middle select-none; 3 | 4 | input { 5 | @apply appearance-none; 6 | border: none; 7 | } 8 | 9 | > * { 10 | @apply col-start-1 row-start-1; 11 | transition-property: transform, rotate, opacity; 12 | transition-duration: 0.2s; 13 | transition-timing-function: cubic-bezier(0, 0, 0.2, 1); 14 | } 15 | 16 | .swap-on, 17 | .swap-indeterminate, 18 | input:indeterminate ~ .swap-on { 19 | @apply opacity-0; 20 | } 21 | 22 | input:is(:checked, :indeterminate) { 23 | & ~ .swap-off { 24 | @apply opacity-0; 25 | } 26 | } 27 | 28 | input:checked ~ .swap-on, 29 | input:indeterminate ~ .swap-indeterminate { 30 | @apply opacity-100; 31 | backface-visibility: visible; 32 | } 33 | } 34 | 35 | /* swap active */ 36 | .swap-active { 37 | .swap-off { 38 | @apply opacity-0; 39 | } 40 | 41 | .swap-on { 42 | @apply opacity-100; 43 | } 44 | } 45 | 46 | /* swap rotate */ 47 | .swap-rotate { 48 | .swap-on, 49 | input:indeterminate ~ .swap-on { 50 | @apply rotate-45; 51 | } 52 | 53 | input:is(:checked, :indeterminate) ~ .swap-on, 54 | &.swap-active .swap-on { 55 | @apply rotate-0; 56 | } 57 | 58 | input:is(:checked, :indeterminate) ~ .swap-off, 59 | &.swap-active .swap-off { 60 | @apply -rotate-45; 61 | } 62 | } 63 | 64 | /* swap flip */ 65 | .swap-flip { 66 | transform-style: preserve-3d; 67 | perspective: 20rem; 68 | 69 | .swap-on, 70 | .swap-indeterminate, 71 | input:indeterminate ~ .swap-on { 72 | transform: rotateY(180deg); 73 | backface-visibility: hidden; 74 | @apply opacity-100; 75 | } 76 | 77 | input:is(:checked, :indeterminate) ~ .swap-on, 78 | &.swap-active .swap-on { 79 | transform: rotateY(0deg); 80 | } 81 | 82 | input:is(:checked, :indeterminate) ~ .swap-off, 83 | &.swap-active .swap-off { 84 | transform: rotateY(-180deg); 85 | backface-visibility: hidden; 86 | @apply opacity-100; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /functions/cleanCss.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { cleanCss } from './cleanCss' 3 | 4 | test('removes empty fallbacks', () => { 5 | const input = 'color: var(--primary-color, );' 6 | const output = 'color: var(--primary-color);' 7 | expect(cleanCss(input)).toBe(output) 8 | }) 9 | 10 | test('removes spacing variable with fallback', () => { 11 | const input = 'margin: var(--spacing-large, 10px);' 12 | const output = 'margin: 10px;' 13 | expect(cleanCss(input)).toBe(output) 14 | }) 15 | 16 | test('removes width variable with fallback', () => { 17 | const input = 'width: var(--width-large, 100%);' 18 | const output = 'width: 100%;' 19 | expect(cleanCss(input)).toBe(output) 20 | }) 21 | 22 | test('keeps var() with no fallback', () => { 23 | const input = 'color: var(--primary-color);' 24 | const output = 'color: var(--primary-color);' 25 | expect(cleanCss(input)).toBe(output) 26 | }) 27 | 28 | test('keeps var() with non-spacing/width fallback', () => { 29 | const input = 'color: var(--primary-color, red);' 30 | const output = 'color: var(--primary-color, red);' 31 | expect(cleanCss(input)).toBe(output) 32 | }) 33 | 34 | test('handles multiple var() in a single line', () => { 35 | const input = 'margin: var(--spacing-large, 10px); width: var(--width-large, 100%);' 36 | const output = 'margin: 10px; width: 100%;' 37 | expect(cleanCss(input)).toBe(output) 38 | }) 39 | 40 | test('handles var() with complex fallback values', () => { 41 | const input = 'margin: var(--spacing-large, calc(10px + 5%));' 42 | const output = 'margin: calc(10px + 5%);' 43 | expect(cleanCss(input)).toBe(output) 44 | }) 45 | 46 | test('does not remove var() with empty fallback if not spacing or width', () => { 47 | const input = 'color: var(--primary-color, );' 48 | const output = 'color: var(--primary-color);' 49 | expect(cleanCss(input)).toBe(output) 50 | }) 51 | 52 | test('does not remove var() with fallback if not spacing or width', () => { 53 | const input = 'color: var(--primary-color, red);' 54 | const output = 'color: var(--primary-color, red);' 55 | expect(cleanCss(input)).toBe(output) 56 | }) 57 | -------------------------------------------------------------------------------- /src/components/divider.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | @apply flex w-full items-center self-stretch text-sm whitespace-nowrap; 3 | 4 | &:not(:empty) { 5 | @apply gap-4; 6 | } 7 | } 8 | 9 | .divider:before, 10 | .divider:after { 11 | content: ""; 12 | @apply border-base-content/20 h-px w-full grow border-e-0 border-t border-solid; 13 | } 14 | 15 | /* divider direction */ 16 | .divider-horizontal { 17 | &.divider { 18 | @apply h-auto w-auto flex-col; 19 | 20 | &:before, 21 | &:after { 22 | @apply h-full w-px border-e border-t-0; 23 | } 24 | } 25 | } 26 | 27 | .divider-vertical { 28 | &.divider { 29 | @apply h-auto w-auto flex-row; 30 | &:before, 31 | &:after { 32 | @apply h-px border-e-0 border-t; 33 | } 34 | } 35 | } 36 | 37 | /* divider colors */ 38 | .divider-neutral { 39 | &:before, 40 | &:after { 41 | @apply border-neutral; 42 | } 43 | } 44 | 45 | .divider-primary { 46 | &:before, 47 | &:after { 48 | @apply border-primary; 49 | } 50 | } 51 | 52 | .divider-secondary { 53 | &:before, 54 | &:after { 55 | @apply border-secondary; 56 | } 57 | } 58 | 59 | .divider-accent { 60 | &:before, 61 | &:after { 62 | @apply border-accent; 63 | } 64 | } 65 | 66 | .divider-success { 67 | &:before, 68 | &:after { 69 | @apply border-success; 70 | } 71 | } 72 | 73 | .divider-warning { 74 | &:before, 75 | &:after { 76 | @apply border-warning; 77 | } 78 | } 79 | 80 | .divider-info { 81 | &:before, 82 | &:after { 83 | @apply border-info; 84 | } 85 | } 86 | 87 | .divider-error { 88 | &:before, 89 | &:after { 90 | @apply border-error; 91 | } 92 | } 93 | 94 | /* divider modifiers */ 95 | .divider-dotted { 96 | &:before, 97 | &:after { 98 | @apply border-dotted; 99 | } 100 | } 101 | 102 | .divider-dashed { 103 | &:before, 104 | &:after { 105 | @apply border-dashed; 106 | } 107 | } 108 | 109 | /* divider placements */ 110 | .divider-start:before { 111 | @apply hidden; 112 | } 113 | 114 | .divider-end:after { 115 | @apply hidden; 116 | } 117 | -------------------------------------------------------------------------------- /functions/generateThemesObject.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | 4 | export const generateThemesObject = async outputPath => { 5 | const themesDir = path.join(import.meta.dirname, '../theme') 6 | const themeObjects = {} 7 | 8 | const themeNames = await fs.readdir(themesDir) 9 | 10 | // Use Promise.all to parallelize theme imports 11 | await Promise.all( 12 | themeNames.map(async themeName => { 13 | const themeObjectPath = path.join(themesDir, themeName, 'object.js') 14 | if ( 15 | await fs 16 | .stat(themeObjectPath) 17 | .then(stats => stats.isFile()) 18 | .catch(() => false) 19 | ) { 20 | try { 21 | const themeModule = await import(themeObjectPath) 22 | themeObjects[themeName] = themeModule.default 23 | } catch (error) { 24 | throw new Error(`Error importing theme: ${themeName}`, error) 25 | } 26 | } 27 | }) 28 | ) 29 | 30 | // Convert themeObjects to a string in the desired format 31 | const themeObjectsString = `export default ${JSON.stringify(themeObjects, null, null)}` 32 | 33 | // Write the string to the specified output file 34 | await fs.writeFile(outputPath, themeObjectsString, 'utf8') 35 | 36 | // types 37 | await generateThemesObjectDeclaration(outputPath.replace('.js', '.d.ts'), themeObjects) 38 | } 39 | 40 | const generateThemesObjectDeclaration = async (outputPath, themeObjects) => { 41 | const themeKeys = Object.keys(themeObjects) 42 | const themeProperties = Object.keys(themeObjects[themeKeys[0]]) 43 | 44 | const themeInterface = ` 45 | interface Theme { 46 | ${themeProperties.map(prop => `"${prop}": string`).join('\n ')} 47 | } 48 | ` 49 | const themesInterface = ` 50 | interface Themes { 51 | ${themeKeys.map(theme => `${theme}: Theme`).join('\n ')} 52 | [key: string]: Theme 53 | } 54 | ` 55 | const declarationContent = ` 56 | ${themeInterface} 57 | ${themesInterface} 58 | declare const themes: Themes 59 | export default themes 60 | ` 61 | 62 | // Write the TypeScript declaration file 63 | await fs.writeFile(outputPath, declarationContent.trim(), 'utf8') 64 | } 65 | -------------------------------------------------------------------------------- /functions/generateImports.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import { getDirectoriesWithTargetFile } from './getDirectoriesWithTargetFile.js' 3 | 4 | const generateJSContent = async () => { 5 | // Create separate arrays for each category 6 | let baseItems = [] 7 | let componentItems = [] 8 | let utilityItems = [] 9 | let imports = '' 10 | 11 | try { 12 | // Function to process each category 13 | const processCategory = async category => { 14 | const items = await getDirectoriesWithTargetFile(`./${category}`, 'index.js') 15 | items.forEach(item => { 16 | const importName = `${item}` 17 | imports += `import ${importName} from './${category}/${item}/index.js';\n` 18 | 19 | // Add items to their respective arrays 20 | switch (category) { 21 | case 'base': 22 | baseItems.push(importName) 23 | break 24 | case 'components': 25 | componentItems.push(importName) 26 | break 27 | case 'utilities': 28 | utilityItems.push(importName) 29 | break 30 | } 31 | }) 32 | } 33 | 34 | // Process all categories 35 | await processCategory('base') 36 | await processCategory('components') 37 | await processCategory('utilities') 38 | 39 | // Generate the content with separate exports 40 | const content = `${imports} 41 | export const base = {${baseItems.join(',')}}; 42 | export const components = {${componentItems.join(',')}}; 43 | export const utilities = {${utilityItems.join(',')}}; 44 | ` 45 | 46 | return { content } 47 | } catch (error) { 48 | throw new Error(`Failed to generate JS content: ${error.message}`) 49 | } 50 | } 51 | 52 | // Write the generated content to a file 53 | const writeToFile = async (content, filename) => { 54 | try { 55 | await fs.writeFile(filename, content, 'utf8') 56 | } catch (error) { 57 | throw new Error(`Failed to write file ${filename}: ${error.message}`) 58 | } 59 | } 60 | 61 | // Main function to generate JS 62 | export const generateImports = async filename => { 63 | try { 64 | const { content: jsContent } = await generateJSContent() 65 | await writeToFile(jsContent, filename) 66 | } catch (error) { 67 | throw new Error(`Failed to generate ${filename}: ${error.message}`) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /functions/themes.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { readdirSync, readFileSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | const requiredValues = [ 6 | 'color-scheme', 7 | '--color-base-100', 8 | '--color-base-200', 9 | '--color-base-300', 10 | '--color-base-content', 11 | '--color-primary', 12 | '--color-primary-content', 13 | '--color-secondary', 14 | '--color-secondary-content', 15 | '--color-accent', 16 | '--color-accent-content', 17 | '--color-neutral', 18 | '--color-neutral-content', 19 | '--color-info', 20 | '--color-info-content', 21 | '--color-success', 22 | '--color-success-content', 23 | '--color-warning', 24 | '--color-warning-content', 25 | '--color-error', 26 | '--color-error-content', 27 | '--radius-selector', 28 | '--radius-field', 29 | '--radius-box', 30 | '--size-selector', 31 | '--size-field', 32 | '--border', 33 | '--depth', 34 | '--noise' 35 | ] 36 | 37 | const isOklch = value => /^oklch\(\d+(\.\d+)?% \d+(\.\d+)? \d+(\.\d+)?\)$/.test(value) 38 | const isRem = value => /^\d+(\.\d+)?rem$/.test(value) 39 | const isPx = value => /^\d+(\.\d+)?px$/.test(value) 40 | 41 | const themesDir = join(import.meta.dirname, '../src/themes') 42 | const themeFiles = readdirSync(themesDir).filter(file => file.endsWith('.css')) 43 | 44 | test('All theme files should include all required values', () => { 45 | themeFiles.forEach(file => { 46 | const filePath = join(themesDir, file) 47 | const content = readFileSync(filePath, 'utf-8') 48 | 49 | requiredValues.forEach(value => { 50 | const regex = new RegExp(`${value}:\\s*([^;]+);`) 51 | const match = content.match(regex) 52 | expect(match).toBeTruthy() 53 | if (match) { 54 | const [, cssValue] = match 55 | if (value === 'color-scheme') { 56 | // Check that color-scheme does not have extra quotes 57 | expect(cssValue).not.toMatch(/^".*"$/) 58 | expect(['light', 'dark']).toContain(cssValue) 59 | } else if (value.startsWith('--color-')) { 60 | expect(isOklch(cssValue)).toBe(true) 61 | } else if (value.startsWith('--radius-')) { 62 | expect(isRem(cssValue)).toBe(true) 63 | } else if (value === '--border') { 64 | expect(isPx(cssValue)).toBe(true) 65 | } 66 | } 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /functions/generateChunks.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, mock } from 'bun:test' 2 | import fs from 'fs/promises' 3 | import { generateChunks } from './generateChunks' 4 | 5 | // Mock the fs functions 6 | const mockWriteFile = mock(async () => {}) 7 | 8 | fs.writeFile = mockWriteFile 9 | 10 | // Mock the getFileNames function 11 | const mockGetFileNames = mock(async dir => { 12 | if (dir === './theme') { 13 | return ['light', 'dark', 'custom'] 14 | } else if (dir === './base') { 15 | return ['base1', 'base2'] 16 | } else if (dir === './components') { 17 | return ['component1', 'component2'] 18 | } else if (dir === './utilities') { 19 | return ['utility1', 'utility2'] 20 | } else if (dir === './colors') { 21 | return ['color1', 'color2'] 22 | } 23 | return [] 24 | }) 25 | 26 | mock.module('./getFileNames', () => ({ 27 | getFileNames: mockGetFileNames 28 | })) 29 | 30 | test('generateChunks generates correct CSS imports', async () => { 31 | const filename = 'output.css' 32 | await generateChunks(filename) 33 | 34 | const expectedContent = 35 | [ 36 | '@import url(theme/light.css);', 37 | '@import url(theme/dark.css);', 38 | '@import url(base/base1.css);', 39 | '@import url(base/base2.css);', 40 | '@import url(components/component1.css);', 41 | '@import url(components/component2.css);', 42 | '@import url(utilities/utility1.css);', 43 | '@import url(utilities/utility2.css);', 44 | '@import url(colors/color1.css);', 45 | '@import url(colors/color2.css);' 46 | ].join('\n') + '\n' 47 | 48 | expect(mockGetFileNames).toHaveBeenCalledWith('./theme', '.css', false) 49 | expect(mockGetFileNames).toHaveBeenCalledWith('./base', '.css', false) 50 | expect(mockGetFileNames).toHaveBeenCalledWith('./components', '.css', false) 51 | expect(mockGetFileNames).toHaveBeenCalledWith('./utilities', '.css', false) 52 | expect(mockGetFileNames).toHaveBeenCalledWith('./colors', '.css', false) 53 | expect(mockWriteFile).toHaveBeenCalledWith(`./${filename}`, expectedContent, 'utf8') 54 | }) 55 | 56 | test('generateChunks throws error if writing to file fails', async () => { 57 | const filename = 'output.css' 58 | 59 | mockWriteFile.mockRejectedValueOnce(new Error('Write error')) 60 | 61 | await expect(generateChunks(filename)).rejects.toThrow('Failed to generate full CSS: Write error') 62 | }) 63 | -------------------------------------------------------------------------------- /functions/validatecss.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { readdirSync, readFileSync, statSync } from 'fs' 3 | import { join, resolve } from 'path' 4 | import { transform } from 'lightningcss' 5 | 6 | // Function to get all CSS files in a directory and its subdirectories, excluding specific directories 7 | const getCssFiles = (dir, excludeDirs = []) => { 8 | let cssFiles = [] 9 | const files = readdirSync(dir) 10 | 11 | files.forEach(file => { 12 | const filePath = join(dir, file) 13 | const fileStat = statSync(filePath) 14 | 15 | if (fileStat.isDirectory()) { 16 | const resolvedPath = resolve(filePath) 17 | if (!excludeDirs.includes(resolvedPath)) { 18 | cssFiles = cssFiles.concat(getCssFiles(filePath, excludeDirs)) 19 | } 20 | } else if (file.endsWith('.css')) { 21 | cssFiles.push(filePath) 22 | } 23 | }) 24 | 25 | return cssFiles 26 | } 27 | 28 | // Directory containing CSS files 29 | const cssDir = join(import.meta.dirname, '../') 30 | 31 | // Directories to exclude (absolute paths) 32 | const excludeDirs = [resolve(cssDir, 'src'), resolve(cssDir, 'functions')] 33 | 34 | // Get all CSS files, excluding specified directories 35 | const cssFiles = getCssFiles(cssDir, excludeDirs) 36 | 37 | // Test all CSS files in a single test block 38 | test('Validate all CSS files', () => { 39 | cssFiles.forEach(file => { 40 | const cssContent = readFileSync(file, 'utf-8') 41 | let result 42 | 43 | try { 44 | result = transform({ 45 | code: Buffer.from(cssContent), 46 | filename: file, 47 | minify: false 48 | }) 49 | } catch (error) { 50 | console.error(`Error in CSS file: ${file}`, error) 51 | throw error 52 | } 53 | 54 | const warningsToIgnore = [{ type: 'AtRuleInvalid', value: 'property' }] 55 | 56 | const relevantWarnings = result.warnings.filter(warning => { 57 | const shouldIgnore = warningsToIgnore.some(rule => rule.type === warning.type && rule.value === warning.value) 58 | return !shouldIgnore 59 | }) 60 | 61 | // Log remaining warnings if there are any 62 | if (relevantWarnings.length > 0) { 63 | console.error(`Warnings in CSS file: ${file}`) 64 | relevantWarnings.forEach(warning => { 65 | console.error(warning) 66 | }) 67 | } 68 | 69 | // Check if there are any warnings (excluding the filtered ones) 70 | expect(relevantWarnings.length).toBe(0) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /src/components/chat.css: -------------------------------------------------------------------------------- 1 | .chat { 2 | @apply grid grid-cols-2 gap-x-4 py-1; 3 | .chat-avatar { 4 | @apply row-span-2 self-end; 5 | } 6 | .chat-header { 7 | @apply row-start-1 mb-1 text-xs; 8 | } 9 | .chat-footer { 10 | @apply row-start-3 mt-1 text-xs; 11 | } 12 | .chat-bubble { 13 | @apply rounded-box relative block w-fit p-3; 14 | max-width: 90%; 15 | min-height: 2.75rem; 16 | min-width: 2.75rem; 17 | &:before { 18 | @apply absolute bottom-0 size-4; 19 | background-color: inherit; 20 | content: ""; 21 | mask-size: contain; 22 | mask-repeat: no-repeat; 23 | mask-position: center; 24 | } 25 | } 26 | &.chat-receiver { 27 | @apply place-items-start; 28 | grid-template-columns: auto 1fr; 29 | .chat-header { 30 | @apply col-start-2; 31 | } 32 | .chat-footer { 33 | @apply col-start-2; 34 | } 35 | .chat-avatar { 36 | @apply col-start-1; 37 | } 38 | .chat-bubble { 39 | @apply bg-base-100 text-base-content col-start-2 rounded-es-none; 40 | &:before { 41 | inset-inline-start: -0.749rem; 42 | mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); 43 | } 44 | [dir="rtl"] &:before { 45 | mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); 46 | } 47 | } 48 | } 49 | &.chat-sender { 50 | @apply place-items-end; 51 | grid-template-columns: 1fr auto; 52 | .chat-header { 53 | @apply col-start-1; 54 | } 55 | .chat-footer { 56 | @apply col-start-1; 57 | } 58 | .chat-avatar { 59 | @apply col-start-2; 60 | } 61 | .chat-bubble { 62 | @apply bg-primary text-primary-content col-start-1 rounded-ee-none text-end; 63 | &:before { 64 | inset-inline-start: 99.9%; 65 | mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); 66 | } 67 | [dir="rtl"] &:before { 68 | mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/base/reset.css: -------------------------------------------------------------------------------- 1 | /* a smaller version of Tailwind CSS 4 preflight.css - MIT License - Copyright (c) Tailwind Labs, Inc. */ 2 | *, 3 | ::after, 4 | ::backdrop, 5 | ::before, 6 | ::file-selector-button { 7 | box-sizing: border-box; 8 | margin: 0; 9 | padding: 0; 10 | border: 0 solid; 11 | } 12 | 13 | :host, 14 | html { 15 | line-height: 1.5; 16 | font-family: var( 17 | --default-font-family, 18 | ui-sans-serif, 19 | system-ui, 20 | sans-serif, 21 | "Apple Color Emoji", 22 | "Segoe UI Emoji", 23 | "Segoe UI Symbol", 24 | "Noto Color Emoji" 25 | ); 26 | -webkit-tap-highlight-color: #0000; 27 | } 28 | 29 | body { 30 | line-height: inherit; 31 | } 32 | 33 | hr { 34 | height: 0; 35 | color: inherit; 36 | border-top-width: 1px; 37 | } 38 | 39 | h1, 40 | h2, 41 | h3, 42 | h4, 43 | h5, 44 | h6 { 45 | font-size: inherit; 46 | font-weight: inherit; 47 | } 48 | 49 | a { 50 | color: inherit; 51 | -webkit-text-decoration: inherit; 52 | text-decoration: inherit; 53 | } 54 | 55 | table { 56 | text-indent: 0; 57 | border-color: inherit; 58 | border-collapse: collapse; 59 | } 60 | 61 | ::file-selector-button, 62 | button, 63 | input, 64 | optgroup, 65 | select, 66 | textarea { 67 | font: inherit; 68 | font-feature-settings: inherit; 69 | font-variation-settings: inherit; 70 | letter-spacing: inherit; 71 | color: inherit; 72 | background: 0 0; 73 | } 74 | 75 | input:where(:not([type="button"], [type="reset"], [type="submit"], [type="file"], [type="range"])), 76 | select, 77 | textarea { 78 | border-width: 1px; 79 | } 80 | 81 | ::file-selector-button, 82 | button, 83 | input:where([type="button"], [type="reset"], [type="submit"]) { 84 | appearance: button; 85 | } 86 | 87 | :-moz-focusring { 88 | outline: auto; 89 | } 90 | 91 | :-moz-ui-invalid { 92 | box-shadow: none; 93 | } 94 | 95 | ::-webkit-search-decoration { 96 | -webkit-appearance: none; 97 | } 98 | 99 | menu, 100 | ol, 101 | ul { 102 | list-style: none; 103 | } 104 | 105 | textarea { 106 | resize: vertical; 107 | } 108 | 109 | ::placeholder { 110 | opacity: 1; 111 | color: color-mix(in oklch, currentColor 50%, #0000); 112 | } 113 | 114 | audio, 115 | canvas, 116 | embed, 117 | iframe, 118 | img, 119 | object, 120 | svg, 121 | video { 122 | display: block; 123 | vertical-align: middle; 124 | } 125 | 126 | img, 127 | video { 128 | max-width: 100%; 129 | height: auto; 130 | } 131 | -------------------------------------------------------------------------------- /src/components/progress.css: -------------------------------------------------------------------------------- 1 | .progress { 2 | @apply bg-base-200 rounded-box flex h-1.5 w-full overflow-hidden; 3 | } 4 | 5 | .progress-label { 6 | @apply border-base-content/25 relative inline w-fit rounded-sm border px-1.5 py-0.5 text-xs font-medium; 7 | } 8 | 9 | .progress-horizontal { 10 | @apply rounded-box h-1.5 w-full flex-row justify-start; 11 | } 12 | 13 | .progress-vertical { 14 | @apply rounded-box h-full w-1.5 flex-col justify-end; 15 | } 16 | 17 | .progress-striped { 18 | background-image: linear-gradient( 19 | 45deg, 20 | rgba(255, 255, 255, 0.15) 25%, 21 | transparent 25%, 22 | transparent 50%, 23 | rgba(255, 255, 255, 0.15) 50%, 24 | rgba(255, 255, 255, 0.15) 75%, 25 | transparent 75%, 26 | transparent 27 | ); 28 | background-size: 0.75rem 0.75rem; 29 | } 30 | 31 | .progress-animated { 32 | animation: progress-bar-stripes 1s linear infinite; 33 | } 34 | 35 | .progress-bar { 36 | @apply bg-neutral text-neutral-content rounded-box flex items-center justify-center overflow-hidden text-xs font-medium whitespace-nowrap transition duration-500 ease-in-out; 37 | } 38 | 39 | /* progress colors */ 40 | .progress-primary { 41 | @apply bg-primary text-primary-content; 42 | } 43 | .progress-secondary { 44 | @apply bg-secondary text-secondary-content; 45 | } 46 | .progress-accent { 47 | @apply bg-accent text-accent-content; 48 | } 49 | .progress-info { 50 | @apply bg-info text-info-content; 51 | } 52 | .progress-success { 53 | @apply bg-success text-success-content; 54 | } 55 | .progress-warning { 56 | @apply bg-warning text-warning-content; 57 | } 58 | .progress-error { 59 | @apply bg-error text-error-content; 60 | } 61 | 62 | /* progress indeterminate */ 63 | .progress-bar.progress-indeterminate { 64 | @apply w-full; 65 | background: linear-gradient(90deg, rgba(0, 0, 0, 0) 50%, var(--progress-color) 50%); 66 | background-size: 50% 100%; 67 | background-repeat: no-repeat; 68 | animation: indeterminate-progress 4s infinite ease-in-out; 69 | } 70 | 71 | @keyframes indeterminate-progress { 72 | 0% { 73 | background-position-x: -75%; 74 | } 75 | 50% { 76 | background-position-x: 125%; 77 | } 78 | 100% { 79 | background-position-x: -75%; 80 | } 81 | } 82 | 83 | @keyframes progress-bar-stripes { 84 | 0% { 85 | background-position-x: 0.75rem; 86 | } 87 | } 88 | /* progress step */ 89 | .progress-step { 90 | @apply text-base-100 bg-base-content/10 flex h-1.5 w-full flex-col justify-center text-center text-xs whitespace-nowrap; 91 | } 92 | -------------------------------------------------------------------------------- /functions/plugin.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { plugin } from './plugin' 3 | 4 | // Mock plugin function and config function 5 | const mockPluginFunction = options => `handler with ${options}` 6 | const mockConfigFunction = options => `config with ${options}` 7 | 8 | test('plugin.withOptions should return a function with handler and config properties', () => { 9 | const options = 'testOptions' 10 | const optionsFunction = plugin.withOptions(mockPluginFunction, mockConfigFunction) 11 | 12 | // Check if the returned function has the __isOptionsFunction property 13 | expect(optionsFunction.__isOptionsFunction).toBe(true) 14 | 15 | // Call the options function with test options 16 | const result = optionsFunction(options) 17 | 18 | // Check if the handler and config properties are correctly set 19 | expect(result.handler).toBe(`handler with ${options}`) 20 | expect(result.config).toBe(`config with ${options}`) 21 | }) 22 | 23 | test('plugin.withOptions should use default config function if not provided', () => { 24 | const options = 'testOptions' 25 | const optionsFunction = plugin.withOptions(mockPluginFunction) 26 | 27 | // Check if the returned function has the __isOptionsFunction property 28 | expect(optionsFunction.__isOptionsFunction).toBe(true) 29 | 30 | // Call the options function with test options 31 | const result = optionsFunction(options) 32 | 33 | // Check if the handler is correctly set 34 | expect(result.handler).toBe(`handler with ${options}`) 35 | // Check if the config is an empty object (default config function) 36 | expect(result.config).toEqual({}) 37 | }) 38 | 39 | test('plugin.withOptions should handle no options', () => { 40 | const optionsFunction = plugin.withOptions(mockPluginFunction, mockConfigFunction) 41 | 42 | // Call the options function with no options 43 | const result = optionsFunction() 44 | 45 | // Check if the handler and config properties are correctly set 46 | expect(result.handler).toBe(`handler with undefined`) 47 | expect(result.config).toBe(`config with undefined`) 48 | }) 49 | 50 | test('plugin.withOptions should handle null options', () => { 51 | const optionsFunction = plugin.withOptions(mockPluginFunction, mockConfigFunction) 52 | 53 | // Call the options function with null options 54 | const result = optionsFunction(null) 55 | 56 | // Check if the handler and config properties are correctly set 57 | expect(result.handler).toBe(`handler with null`) 58 | expect(result.config).toBe(`config with null`) 59 | }) 60 | -------------------------------------------------------------------------------- /src/js/helpers/clipboard/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @version: 3.2.3 3 | * @author: Preline Labs Ltd. 4 | * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) 5 | * Copyright 2024 Preline Labs Ltd. 6 | */ 7 | 8 | declare var ClipboardJS: any 9 | 10 | const clipboardSelector = window?.HS_CLIPBOARD_SELECTOR ?? '.js-clipboard' 11 | 12 | function clipboardHelper(selector: string) { 13 | const $clipboards = document.querySelectorAll(selector) 14 | 15 | $clipboards.forEach((el: HTMLElement) => { 16 | const clipboard = new ClipboardJS(el, { 17 | text: (trigger: HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement) => { 18 | const clipboardText = trigger.dataset.clipboardText 19 | 20 | if (clipboardText) return clipboardText 21 | 22 | const clipboardTarget = trigger.dataset.clipboardTarget 23 | const $element: HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement = 24 | document.querySelector(clipboardTarget) 25 | 26 | if ($element.tagName === 'SELECT' || $element.tagName === 'INPUT' || $element.tagName === 'TEXTAREA') { 27 | return $element.value 28 | } else return $element.textContent 29 | } 30 | }) 31 | clipboard.on('success', () => { 32 | const $default: HTMLElement = el.querySelector('.js-clipboard-default') 33 | const $success: HTMLElement = el.querySelector('.js-clipboard-success') 34 | const $successText = el.querySelector('.js-clipboard-success-text') 35 | const successText = el.dataset.clipboardSuccessText || '' 36 | const tooltip = el.closest('.tooltip') 37 | let oldSuccessText: string 38 | 39 | if ($successText) { 40 | oldSuccessText = $successText.textContent 41 | $successText.textContent = successText 42 | } 43 | if ($default && $success) { 44 | $default.style.display = 'none' 45 | $success.style.display = 'block' 46 | } 47 | if (tooltip) (window.HSTooltip as any).show(tooltip) 48 | 49 | setTimeout(function () { 50 | if ($successText && oldSuccessText) { 51 | $successText.textContent = oldSuccessText 52 | } 53 | if (tooltip) (window.HSTooltip as any).hide(tooltip) 54 | if ($default && $success) { 55 | $success.style.display = '' 56 | $default.style.display = '' 57 | } 58 | }, 2000) 59 | }) 60 | }) 61 | } 62 | 63 | window.addEventListener('load', () => { 64 | clipboardHelper(clipboardSelector) 65 | }) 66 | 67 | export default clipboardHelper 68 | -------------------------------------------------------------------------------- /functions/contrast.test.js: -------------------------------------------------------------------------------- 1 | import { wcagContrast } from 'culori' 2 | import { expect, test } from 'bun:test' 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | const minContrast = 3 7 | const optimalContrast = 4.5 8 | const maxContrast = 7 9 | 10 | function rateContrast(contrast) { 11 | if (contrast >= maxContrast) { 12 | return 'AAA' 13 | } 14 | if (contrast >= optimalContrast) { 15 | return 'AA' 16 | } 17 | if (contrast <= minContrast) { 18 | return '🛑' 19 | } 20 | return '' 21 | } 22 | 23 | const themesDir = path.resolve(import.meta.dirname, '../src/themes') 24 | const colorPairs = [ 25 | ['--color-base-100', '--color-base-content'], 26 | ['--color-base-200', '--color-base-content'], 27 | ['--color-base-300', '--color-base-content'], 28 | ['--color-primary', '--color-primary-content'], 29 | ['--color-secondary', '--color-secondary-content'], 30 | ['--color-accent', '--color-accent-content'], 31 | ['--color-neutral', '--color-neutral-content'], 32 | ['--color-info', '--color-info-content'], 33 | ['--color-success', '--color-success-content'], 34 | ['--color-warning', '--color-warning-content'], 35 | ['--color-error', '--color-error-content'] 36 | ] 37 | 38 | const getCssVariables = cssContent => { 39 | const variables = {} 40 | const regex = /--([\w-]+):\s*([^;]+);/g 41 | let match 42 | while ((match = regex.exec(cssContent)) !== null) { 43 | variables[`--${match[1]}`] = match[2].trim() 44 | } 45 | return variables 46 | } 47 | 48 | const themes = fs.readdirSync(themesDir).filter(file => file.endsWith('.css')) 49 | 50 | test('All theme color contrasts', () => { 51 | const results = [] 52 | 53 | themes.forEach(theme => { 54 | const themeName = path.basename(theme, '.css') 55 | const themePath = path.join(themesDir, theme) 56 | const cssContent = fs.readFileSync(themePath, 'utf8') 57 | const variables = getCssVariables(cssContent) 58 | 59 | colorPairs.forEach(([color1, color2]) => { 60 | const colorValue1 = variables[color1] 61 | const colorValue2 = variables[color2] 62 | const contrast = parseFloat(wcagContrast(colorValue1, colorValue2).toFixed(2)) 63 | const result = { 64 | theme: themeName, 65 | colorPair: `${color1.replace('--color-', '')}, ${color2.replace('--color-', '')}`, 66 | ratio: contrast, 67 | rate: rateContrast(contrast) 68 | } 69 | results.push(result) 70 | }) 71 | }) 72 | 73 | console.table(results) 74 | 75 | results.forEach(({ ratio }) => { 76 | expect(ratio).toBeGreaterThan(minContrast) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /functions/createDirectoryBasedOnFileNames.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, mock } from 'bun:test' 2 | import path from 'node:path' 3 | import { createDirectoryBasedOnFileNames } from './createDirectoryBasedOnFileNames' 4 | import { promises as fs } from 'node:fs' 5 | 6 | // Mock the fs.mkdir function 7 | const mockMkdir = mock(async () => {}) 8 | 9 | fs.mkdir = mockMkdir 10 | 11 | test('createDirectoryBasedOnFileNames creates directory based on file name', async () => { 12 | const fileName = '/path/to/source/component.js' 13 | const fileExtension = '.js' 14 | const distDir = '/path/to/destination' 15 | 16 | const expectedComponentName = 'component' 17 | const expectedComponentDir = path.join(distDir, expectedComponentName) 18 | 19 | const result = await createDirectoryBasedOnFileNames(fileName, fileExtension, distDir) 20 | 21 | expect(mockMkdir).toHaveBeenCalledWith(expectedComponentDir, { recursive: true }) 22 | expect(result).toBe(expectedComponentDir) 23 | }) 24 | 25 | test('createDirectoryBasedOnFileNames creates directory with different extension', async () => { 26 | const fileName = '/path/to/source/component.tsx' 27 | const fileExtension = '.tsx' 28 | const distDir = '/path/to/destination' 29 | 30 | const expectedComponentName = 'component' 31 | const expectedComponentDir = path.join(distDir, expectedComponentName) 32 | 33 | const result = await createDirectoryBasedOnFileNames(fileName, fileExtension, distDir) 34 | 35 | expect(mockMkdir).toHaveBeenCalledWith(expectedComponentDir, { recursive: true }) 36 | expect(result).toBe(expectedComponentDir) 37 | }) 38 | 39 | test('createDirectoryBasedOnFileNames handles nested directories', async () => { 40 | const fileName = '/path/to/source/nested/component.js' 41 | const fileExtension = '.js' 42 | const distDir = '/path/to/destination' 43 | 44 | const expectedComponentName = 'component' 45 | const expectedComponentDir = path.join(distDir, expectedComponentName) 46 | 47 | const result = await createDirectoryBasedOnFileNames(fileName, fileExtension, distDir) 48 | 49 | expect(mockMkdir).toHaveBeenCalledWith(expectedComponentDir, { recursive: true }) 50 | expect(result).toBe(expectedComponentDir) 51 | }) 52 | 53 | test('createDirectoryBasedOnFileNames throws error if mkdir fails', async () => { 54 | const fileName = '/path/to/source/component.js' 55 | const fileExtension = '.js' 56 | const distDir = '/path/to/destination' 57 | 58 | const mockError = new Error('Mock mkdir error') 59 | mockMkdir.mockRejectedValueOnce(mockError) 60 | 61 | await expect(createDirectoryBasedOnFileNames(fileName, fileExtension, distDir)).rejects.toThrow(mockError) 62 | }) 63 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TerserPlugin = require('terser-webpack-plugin') 3 | 4 | module.exports = { 5 | mode: 'production', 6 | stats: 'minimal', 7 | entry: { 8 | index: './src/js/index.ts', 9 | accordion: './src/js/plugins/accordion/index.ts', 10 | carousel: './src/js/plugins/carousel/index.ts', 11 | collapse: './src/js/plugins/collapse/index.ts', 12 | combobox: './src/js/plugins/combobox/index.ts', 13 | 'copy-markup': './src/js/plugins/copy-markup/index.ts', 14 | datatable: './src/js/plugins/datatable/index.ts', 15 | dropdown: './src/js/plugins/dropdown/index.ts', 16 | 'file-upload': './src/js/plugins/file-upload/index.ts', 17 | 'input-number': './src/js/plugins/input-number/index.ts', 18 | overlay: './src/js/plugins/overlay/index.ts', 19 | 'pin-input': './src/js/plugins/pin-input/index.ts', 20 | 'range-slider': './src/js/plugins/range-slider/index.ts', 21 | 'remove-element': './src/js/plugins/remove-element/index.ts', 22 | scrollspy: './src/js/plugins/scrollspy/index.ts', 23 | select: './src/js/plugins/select/index.ts', 24 | stepper: './src/js/plugins/stepper/index.ts', 25 | 'strong-password': './src/js/plugins/strong-password/index.ts', 26 | tabs: './src/js/plugins/tabs/index.ts', 27 | 'toggle-count': './src/js/plugins/toggle-count/index.ts', 28 | 'toggle-password': './src/js/plugins/toggle-password/index.ts', 29 | tooltip: './src/js/plugins/tooltip/index.ts', 30 | 'tree-view': './src/js/plugins/tree-view/index.ts', 31 | 32 | // Helpers 33 | 'helper-apexcharts': './src/js/helpers/apexcharts/index.ts', 34 | 'helper-clipboard': './src/js/helpers/clipboard/index.ts' 35 | }, 36 | module: { 37 | rules: [ 38 | { test: /\.ts?$/, enforce: 'pre', use: ['source-map-loader'] }, 39 | { test: /\.ts?$/, use: 'ts-loader', exclude: /node_modules/ } 40 | ] 41 | }, 42 | resolve: { extensions: ['.ts', '.js'] }, 43 | output: { 44 | path: path.resolve(__dirname, 'dist'), 45 | filename: '[name].js', 46 | library: { type: 'umd' } 47 | }, 48 | externals: { 49 | jquery: 'jQuery', 50 | lodash: '_', 51 | apexcharts: 'ApexCharts', 52 | 'datatable.net-dt': 'DataTable', 53 | dropzone: 'Dropzone', 54 | clipboard: 'ClipboardJS', 55 | noUiSlider: 'noUiSlider' 56 | }, 57 | optimization: { 58 | minimize: true, 59 | minimizer: [ 60 | new TerserPlugin({ 61 | extractComments: false 62 | }) 63 | ] 64 | }, 65 | performance: { 66 | hints: 'warning', 67 | maxEntrypointSize: 400000, // 400 KiB 68 | maxAssetSize: 400000 // 400 KiB 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/badge.css: -------------------------------------------------------------------------------- 1 | .badge { 2 | @apply rounded-selector inline-flex items-center justify-center gap-1.5 text-center align-middle text-sm; 3 | border: var(--border) solid var(--badge-border); 4 | padding-inline: calc(0.25rem * 3); 5 | width: fit-content; 6 | color: var(--badge-fg); 7 | background-size: auto, calc(var(--noise) * 100%); 8 | background-image: none, var(--fx-noise); 9 | background-color: var(--badge-bg); 10 | --badge-border: var(--badge-color, var(--color-neutral)); 11 | --badge-bg: var(--badge-color, var(--color-neutral)); 12 | --badge-fg: var(--color-neutral-content); 13 | --size: calc(var(--size-selector, 0.25rem) * 6); 14 | height: var(--size); 15 | 16 | &.badge-outline { 17 | --badge-fg: var(--badge-color, var(--color-neutral)); 18 | --badge-bg: transparent; 19 | background-image: none; 20 | } 21 | 22 | &.badge-soft { 23 | --badge-border: none; 24 | --badge-fg: var(--badge-color, var(--color-neutral)); 25 | --badge-bg: color-mix(in oklab, var(--badge-color, var(--color-neutral)) 10%, var(--color-base-100)); 26 | background-image: none; 27 | } 28 | } 29 | 30 | .badge-primary { 31 | --badge-color: var(--color-primary); 32 | --badge-fg: var(--color-primary-content); 33 | } 34 | 35 | .badge-secondary { 36 | --badge-color: var(--color-secondary); 37 | --badge-fg: var(--color-secondary-content); 38 | } 39 | 40 | .badge-accent { 41 | --badge-color: var(--color-accent); 42 | --badge-fg: var(--color-accent-content); 43 | } 44 | 45 | .badge-info { 46 | --badge-color: var(--color-info); 47 | --badge-fg: var(--color-info-content); 48 | } 49 | 50 | .badge-success { 51 | --badge-color: var(--color-success); 52 | --badge-fg: var(--color-success-content); 53 | } 54 | 55 | .badge-warning { 56 | --badge-color: var(--color-warning); 57 | --badge-fg: var(--color-warning-content); 58 | } 59 | 60 | .badge-error { 61 | --badge-color: var(--color-error); 62 | --badge-fg: var(--color-error-content); 63 | } 64 | 65 | .badge-xs { 66 | --size: calc(var(--size-selector, 0.25rem) * 4); 67 | font-size: 0.625rem; 68 | padding-inline: calc(0.25rem * 1.5); 69 | } 70 | 71 | .badge-sm { 72 | @apply text-xs; 73 | --size: calc(var(--size-selector, 0.25rem) * 5); 74 | padding-inline: calc(0.25rem * 2); 75 | } 76 | 77 | .badge-md { 78 | @apply text-sm; 79 | --size: calc(var(--size-selector, 0.25rem) * 6); 80 | padding-inline: calc(0.25rem * 3); 81 | } 82 | 83 | .badge-lg { 84 | @apply text-base; 85 | --size: calc(var(--size-selector, 0.25rem) * 7); 86 | padding-inline: calc(0.25rem * 3.5); 87 | } 88 | 89 | .badge-xl { 90 | @apply text-lg; 91 | --size: calc(var(--size-selector, 0.25rem) * 8); 92 | padding-inline: calc(0.25rem * 4); 93 | } 94 | -------------------------------------------------------------------------------- /functions/extractClasses.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | 4 | // Function to extract class names from CSS content 5 | const extractClassNames = async cssContent => { 6 | const classRegex = /\.([a-zA-Z_-][a-zA-Z0-9_-]*)(?=\s*[{,:(])|:where\(\.([a-zA-Z_-][a-zA-Z0-9_-]*)\)/g 7 | const matches = cssContent.match(classRegex) 8 | const classNames = matches 9 | ? matches.map(match => { 10 | const cleanedMatch = match.replace(/:where\(\.|[{,:()]/g, '').trim() 11 | return cleanedMatch.startsWith('.') ? cleanedMatch.slice(1) : cleanedMatch 12 | }) 13 | : [] 14 | return [...new Set(classNames)] // Remove duplicates 15 | } 16 | 17 | // Function to process a single CSS file 18 | const processCssFile = async (srcDir, filePath) => { 19 | try { 20 | const cssContent = await fs.readFile(filePath, 'utf8') 21 | const classNames = await extractClassNames(cssContent) 22 | 23 | const fileName = path.basename(filePath, '.css') 24 | const outputDir = path.join(import.meta.dirname, '..', srcDir, fileName) 25 | const outputFilePath = path.join(outputDir, 'class.json') 26 | 27 | // Create directory if it doesn't exist 28 | try { 29 | await fs.mkdir(outputDir, { recursive: true }) 30 | } catch (err) { 31 | if (err.code !== 'EEXIST') throw err 32 | } 33 | 34 | // Create JSON string 35 | const jsonString = JSON.stringify(classNames, null, 2) 36 | 37 | // Write to a new JSON file 38 | await fs.writeFile(outputFilePath, jsonString) 39 | 40 | return classNames.length 41 | } catch (error) { 42 | throw new Error(`Error processing file ${filePath}: ${error.message}`) 43 | } 44 | } 45 | 46 | // Function to process all CSS files 47 | export const extractClasses = async ({ srcDir }) => { 48 | try { 49 | // Read all CSS files from the styles directory 50 | const stylesDir = path.join(import.meta.dirname, '..', 'src', srcDir) 51 | const cssFiles = await fs.readdir(stylesDir) 52 | const filteredCssFiles = cssFiles.filter(file => file.endsWith('.css')) 53 | 54 | if (filteredCssFiles.length === 0) { 55 | throw new Error('No CSS files found in the specified directory') 56 | } 57 | 58 | // Process each CSS file and sum up the total number of class names 59 | const classNameCounts = await Promise.all( 60 | filteredCssFiles.map(async file => { 61 | const filePath = path.join(stylesDir, file) 62 | return await processCssFile(srcDir, filePath) 63 | }) 64 | ) 65 | 66 | const totalClassNames = classNameCounts.reduce((total, count) => total + count, 0) 67 | 68 | return totalClassNames 69 | } catch (error) { 70 | throw new Error(`Error extracting classes: ${error.message}`) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /functions/packCss.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | import path from 'path' 3 | import { getFileNames } from './getFileNames.js' 4 | import { cleanCss } from './cleanCss.js' 5 | 6 | const readFileContent = async filePath => { 7 | return await fs.readFile(filePath, 'utf8') 8 | } 9 | const getThemeDirs = () => ['light', 'dark'] 10 | const createThemePath = theme => path.join('./theme', `${theme}.css`) 11 | const wrapThemeContent = contents => `@layer base{\n${contents.join('\n')}\n}` 12 | const readThemeCSS = async () => { 13 | const themeDirs = getThemeDirs() 14 | const themeContents = await Promise.all(themeDirs.map(theme => readFileContent(createThemePath(theme)))) 15 | return wrapThemeContent(themeContents) 16 | } 17 | 18 | const directoryMap = { 19 | './base': false, 20 | './components': false, 21 | './utilities': false, 22 | './colors': 'utilities' 23 | } 24 | 25 | const wrapInLayer = (content, layerName) => { 26 | return layerName ? `@layer ${layerName}{\n${content}\n}` : content 27 | } 28 | 29 | const filterExcludedFiles = (files, excludeFiles) => { 30 | return files.filter(file => !excludeFiles.includes(`${file}.css`)) 31 | } 32 | 33 | const readDirectoryContent = async (directory, layerName, excludeFiles = []) => { 34 | const files = await getFileNames(directory, '.css', false) 35 | const filteredFiles = filterExcludedFiles(files, excludeFiles) 36 | 37 | const contents = await Promise.all( 38 | filteredFiles.map(async file => { 39 | const content = await readFileContent(`${directory}/${file}.css`) 40 | return wrapInLayer(content, layerName) 41 | }) 42 | ) 43 | 44 | return contents 45 | } 46 | 47 | const readAllCSSDirectories = async (excludeFiles = []) => { 48 | const directories = Object.keys(directoryMap) 49 | 50 | const allContents = await Promise.all( 51 | directories.map(dir => readDirectoryContent(dir, directoryMap[dir], excludeFiles)) 52 | ) 53 | 54 | return allContents.flat() 55 | } 56 | const combineContent = (themeCSS, otherCSS) => { 57 | return [themeCSS, ...otherCSS].join('\n') 58 | } 59 | const writeContentToFile = async (file, content) => { 60 | const cleanedContent = cleanCss(content) 61 | await fs.writeFile(file, cleanedContent) 62 | } 63 | export const packCss = async ({ 64 | outputFile, 65 | exclude = { 66 | colors: [], 67 | components: [], 68 | utilities: [] 69 | } 70 | }) => { 71 | const allExcludeFiles = [ 72 | ...(exclude.colors?.map(file => `${file}.css`) || []), 73 | ...(exclude.components?.map(file => `${file}.css`) || []), 74 | ...(exclude.utilities?.map(file => `${file}.css`) || []) 75 | ] 76 | const [themeCSS, otherCSS] = await Promise.all([readThemeCSS(), readAllCSSDirectories(allExcludeFiles)]) 77 | 78 | const allContent = combineContent(themeCSS, otherCSS) 79 | await writeContentToFile(outputFile, allContent) 80 | } 81 | -------------------------------------------------------------------------------- /webpack.config.mjs.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const TerserPlugin = require('terser-webpack-plugin') 3 | 4 | module.exports = { 5 | mode: 'production', 6 | stats: 'minimal', 7 | entry: { 8 | index: './src/js/index.ts', 9 | accordion: './src/js/plugins/accordion/index.ts', 10 | carousel: './src/js/plugins/carousel/index.ts', 11 | collapse: './src/js/plugins/collapse/index.ts', 12 | combobox: './src/js/plugins/combobox/index.ts', 13 | 'copy-markup': './src/js/plugins/copy-markup/index.ts', 14 | datatable: './src/js/plugins/datatable/index.ts', 15 | dropdown: './src/js/plugins/dropdown/index.ts', 16 | 'file-upload': './src/js/plugins/file-upload/index.ts', 17 | 'input-number': './src/js/plugins/input-number/index.ts', 18 | overlay: './src/js/plugins/overlay/index.ts', 19 | 'pin-input': './src/js/plugins/pin-input/index.ts', 20 | 'range-slider': './src/js/plugins/range-slider/index.ts', 21 | 'remove-element': './src/js/plugins/remove-element/index.ts', 22 | scrollspy: './src/js/plugins/scrollspy/index.ts', 23 | select: './src/js/plugins/select/index.ts', 24 | stepper: './src/js/plugins/stepper/index.ts', 25 | 'strong-password': './src/js/plugins/strong-password/index.ts', 26 | tabs: './src/js/plugins/tabs/index.ts', 27 | 'toggle-count': './src/js/plugins/toggle-count/index.ts', 28 | 'toggle-password': './src/js/plugins/toggle-password/index.ts', 29 | tooltip: './src/js/plugins/tooltip/index.ts', 30 | 'tree-view': './src/js/plugins/tree-view/index.ts', 31 | 32 | // Helpers 33 | 'helper-apexcharts': './src/js/helpers/apexcharts/index.ts', 34 | 'helper-clipboard': './src/js/helpers/clipboard/index.ts' 35 | }, 36 | module: { 37 | rules: [ 38 | { test: /\.ts?$/, enforce: 'pre', use: ['source-map-loader'] }, 39 | { 40 | test: /\.ts?$/, 41 | use: [ 42 | { 43 | loader: 'ts-loader', 44 | options: { 45 | configFile: 'tsconfig.mjs.json' 46 | } 47 | } 48 | ], 49 | exclude: /node_modules/ 50 | } 51 | ] 52 | }, 53 | experiments: { 54 | outputModule: true 55 | }, 56 | resolve: { 57 | extensions: ['.ts', '.js'] 58 | }, 59 | output: { 60 | path: path.resolve(__dirname, 'dist/'), 61 | filename: '[name].mjs', 62 | libraryTarget: 'module' 63 | }, 64 | externals: { 65 | jquery: 'jQuery', 66 | lodash: '_', 67 | apexcharts: 'ApexCharts', 68 | 'datatable.net-dt': 'DataTable', 69 | dropzone: 'Dropzone', 70 | clipboard: 'ClipboardJS', 71 | noUiSlider: 'noUiSlider' 72 | }, 73 | optimization: { 74 | minimize: true, 75 | minimizer: [ 76 | new TerserPlugin({ 77 | extractComments: false 78 | }) 79 | ] 80 | }, 81 | performance: { 82 | hints: 'warning', 83 | maxEntrypointSize: 400000, // 400 KiB 84 | maxAssetSize: 400000 // 400 KiB 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a new Bug 2 | description: If you found a new bug on flyonui package, report it here. 3 | title: "bug: " 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | 9 | Thank you for submitting a bug report 💙 10 | If you need help about your project and it's **NOT** a flyonui bug, please [ask your question in discussion forum](https://github.com/themeselection/flyonui/discussions/new?category=q-a). 11 | 12 | 👉 Before you submit a new bug, please do the following steps: 13 | 14 | 1. Update your FlyonUI (and other dependencies) to the latest version (`npm i -D flyonui@latest`) 15 | 3. Search [existing issues](https://github.com/themeselection/flyonui/issues?q=is%3Aissue) and [discussion forum](https://github.com/themeselection/flyonui/discussions?discussions_q=). You may find a solution there 16 | 4. Open one issue per problem. Don't list multiple issues in the same page because that would be hard to track. 17 | 18 | - type: input 19 | id: version 20 | attributes: 21 | label: What version of FlyonUI are you using? 22 | description: You can see the flyonui version number on your `package.json` file. 23 | placeholder: "example: v1.0.0" 24 | validations: 25 | required: false 26 | 27 | - type: dropdown 28 | id: browsers 29 | attributes: 30 | label: Which browsers are you seeing the problem on? 31 | multiple: true 32 | options: 33 | - All browsers 34 | - Chrome 35 | - Chrome Android 36 | - Safari 37 | - Safari iOS 38 | - Firefox 39 | - Edge 40 | - Other 41 | 42 | - type: input 43 | id: reproduction 44 | attributes: 45 | label: Reproduction URL 46 | description: | 47 | 48 | ℹ️ Please provide a link to a small, minimal example source code where issue can be reproduced. Without a link it will be hard to guess what is the cause of the problem and it will take more time to fix. 49 | 50 | ℹ️ Provide a link to a demo on [CodeSandbox](https://codesandbox.io/) or [StackBlitz](https://stackblitz.com/) that reproduces the issue. 51 | You can fork from an existing [FlyonUI](https://github.com/themeselection/flyonui) demo if needed. 52 | 53 | placeholder: "https://" 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: description 59 | attributes: 60 | label: Describe your issue 61 | description: | 62 | 63 | ℹ️ Explain the issue, including the circumstances under which it occurs, and provide screenshots if possible. 64 | 65 | ℹ️ Please include the steps to reproduce the issue (e.g., "Step 1: Open..."). 66 | 67 | ℹ️ To write a code block, use ``` before and after your code. 68 | 69 | ℹ️ Please don't use JSX or other framework specific code. Use plain HTML and CSS. 70 | 71 | validations: 72 | required: true 73 | -------------------------------------------------------------------------------- /functions/pluginOptionsHandler.js: -------------------------------------------------------------------------------- 1 | import themeOrder from './themeOrder.js' 2 | 3 | export const pluginOptionsHandler = (() => { 4 | let firstRun = true 5 | return (options, addBase, themesObject, packageVersion) => { 6 | const { 7 | logs = true, 8 | root = ':root', 9 | themes = ['light --default', 'dark --prefersdark'], 10 | include, 11 | exclude, 12 | prefix = '' 13 | } = options || {} 14 | 15 | if (logs !== false && firstRun) { 16 | console.log( 17 | `${atob('Lyoh')} ${decodeURIComponent('%F0%9F%9A%80')} ${atob('Zmx5b251aQ==')} ${packageVersion} ${atob('Ki8=')}` 18 | ) 19 | firstRun = false 20 | } 21 | 22 | const applyTheme = (themeName, flags) => { 23 | const theme = themesObject[themeName] 24 | if (theme) { 25 | let selector = `${root}:has(input.theme-controller[value=${themeName}]:checked),[data-theme=${themeName}]` 26 | if (flags.includes('--default')) { 27 | selector = `:where(${root}),${selector}` 28 | } 29 | addBase({ [selector]: theme }) 30 | 31 | if (flags.includes('--prefersdark')) { 32 | addBase({ '@media (prefers-color-scheme: dark)': { [root]: theme } }) 33 | } 34 | } 35 | } 36 | 37 | if (themes === 'all') { 38 | if (themesObject['light']) { 39 | applyTheme('light', ['--default']) 40 | } 41 | 42 | if (themesObject['dark']) { 43 | addBase({ '@media (prefers-color-scheme: dark)': { [root]: themesObject['dark'] } }) 44 | } 45 | 46 | themeOrder.forEach(themeName => { 47 | if (themesObject[themeName]) { 48 | applyTheme(themeName, []) 49 | } 50 | }) 51 | } else if (themes) { 52 | const themeArray = Array.isArray(themes) ? themes : [themes] 53 | 54 | // For single theme with --default flag, skip the other applications 55 | if (themeArray.length === 1 && themeArray[0].includes('--default')) { 56 | const [themeName, ...flags] = themeArray[0].split(' ') 57 | applyTheme(themeName, flags) 58 | return { include, exclude, prefix } 59 | } 60 | 61 | // default theme 62 | themeArray.forEach(themeOption => { 63 | const [themeName, ...flags] = themeOption.split(' ') 64 | if (flags.includes('--default')) { 65 | applyTheme(themeName, ['--default']) 66 | } 67 | }) 68 | 69 | // prefers dark theme 70 | themeArray.forEach(themeOption => { 71 | const [themeName, ...flags] = themeOption.split(' ') 72 | if (flags.includes('--prefersdark')) { 73 | addBase({ '@media (prefers-color-scheme: dark)': { [root]: themesObject[themeName] } }) 74 | } 75 | }) 76 | 77 | // other themes 78 | themeArray.forEach(themeOption => { 79 | const [themeName] = themeOption.split(' ') 80 | applyTheme(themeName, []) 81 | }) 82 | } 83 | 84 | return { include, exclude, prefix } 85 | } 86 | })() 87 | -------------------------------------------------------------------------------- /src/js/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @version: 3.2.3 3 | * @author: Preline Labs Ltd. 4 | * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html) 5 | * Copyright 2024 Preline Labs Ltd. 6 | */ 7 | 8 | declare var _: any 9 | declare var DataTable: any 10 | declare var Dropzone: any 11 | declare var noUiSlider: any 12 | 13 | import HSAccessibilityObserver from './plugins/accessibility-manager' 14 | 15 | if (typeof window !== 'undefined') { 16 | window.HSAccessibilityObserver = new HSAccessibilityObserver() 17 | } 18 | 19 | let HSDataTableModule 20 | let HSFileUploadModule 21 | let HSRangeSliderModule 22 | 23 | export { default as HSCopyMarkup } from './plugins/copy-markup' 24 | export { default as HSAccordion } from './plugins/accordion' 25 | export { default as HSCarousel } from './plugins/carousel' 26 | export { default as HSCollapse } from './plugins/collapse' 27 | export { default as HSComboBox } from './plugins/combobox' 28 | export { default as HSDropdown } from './plugins/dropdown' 29 | export { default as HSInputNumber } from './plugins/input-number' 30 | export { default as HSOverlay } from './plugins/overlay' 31 | export { default as HSPinInput } from './plugins/pin-input' 32 | export { default as HSRemoveElement } from './plugins/remove-element' 33 | export { default as HSScrollspy } from './plugins/scrollspy' 34 | export { default as HSSelect } from './plugins/select' 35 | export { default as HSStepper } from './plugins/stepper' 36 | export { default as HSStrongPassword } from './plugins/strong-password' 37 | export { default as HSTabs } from './plugins/tabs' 38 | export { default as HSToggleCount } from './plugins/toggle-count' 39 | export { default as HSTogglePassword } from './plugins/toggle-password' 40 | export { default as HSTooltip } from './plugins/tooltip' 41 | export { default as HSTreeView } from './plugins/tree-view' 42 | export { default as HSStaticMethods } from './static' 43 | 44 | if (typeof window !== 'undefined') { 45 | try { 46 | if (typeof DataTable !== 'undefined' && typeof jQuery !== 'undefined') { 47 | HSDataTableModule = require('./plugins/datatable').default 48 | } 49 | } catch (e) { 50 | console.warn('HSDataTable: Required dependencies not found') 51 | HSDataTableModule = null 52 | } 53 | 54 | try { 55 | if (typeof _ !== 'undefined' && typeof Dropzone !== 'undefined') { 56 | HSFileUploadModule = require('./plugins/file-upload').default 57 | } 58 | } catch (e) { 59 | console.warn('HSFileUpload: Required dependencies not found') 60 | HSFileUploadModule = null 61 | } 62 | 63 | try { 64 | if (typeof noUiSlider !== 'undefined') { 65 | HSRangeSliderModule = require('./plugins/range-slider').default 66 | } 67 | } catch (e) { 68 | console.warn('HSRangeSlider: Required dependencies not found') 69 | HSRangeSliderModule = null 70 | } 71 | } 72 | 73 | export { HSDataTableModule as HSDataTable } 74 | export { HSFileUploadModule as HSFileUpload } 75 | export { HSRangeSliderModule as HSRangeSlider } 76 | -------------------------------------------------------------------------------- /functions/extractClasses.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, mock } from 'bun:test' 2 | import path from 'node:path' 3 | import fs from 'fs/promises' 4 | import { extractClasses } from './extractClasses' 5 | 6 | // Mock the fs functions 7 | const mockReadFile = mock(async filePath => { 8 | if (filePath.endsWith('file1.css')) { 9 | return '.class1 { color: red; } .class2, .class3 { color: blue; }' 10 | } else if (filePath.endsWith('file2.css')) { 11 | return '.class4 { color: green; } .class5:hover { color: yellow; }' 12 | } 13 | throw new Error('File not found') 14 | }) 15 | 16 | const mockReaddir = mock(async dirPath => { 17 | if (dirPath.endsWith('styles')) { 18 | return ['file1.css', 'file2.css'] 19 | } 20 | throw new Error('Directory not found') 21 | }) 22 | 23 | const mockMkdir = mock(async () => {}) 24 | const mockWriteFile = mock(async () => {}) 25 | 26 | fs.readFile = mockReadFile 27 | fs.readdir = mockReaddir 28 | fs.mkdir = mockMkdir 29 | fs.writeFile = mockWriteFile 30 | 31 | test('extractClasses processes CSS files and extracts class names', async () => { 32 | const srcDir = 'styles' 33 | const totalClassNames = await extractClasses({ srcDir }) 34 | 35 | expect(mockReaddir).toHaveBeenCalledWith(path.join(import.meta.dirname, '..', 'src', srcDir)) 36 | expect(mockReadFile).toHaveBeenCalledWith(path.join(import.meta.dirname, '..', 'src', srcDir, 'file1.css'), 'utf8') 37 | expect(mockReadFile).toHaveBeenCalledWith(path.join(import.meta.dirname, '..', 'src', srcDir, 'file2.css'), 'utf8') 38 | expect(mockMkdir).toHaveBeenCalledWith(path.join(import.meta.dirname, '..', srcDir, 'file1'), { 39 | recursive: true 40 | }) 41 | expect(mockMkdir).toHaveBeenCalledWith(path.join(import.meta.dirname, '..', srcDir, 'file2'), { 42 | recursive: true 43 | }) 44 | expect(mockWriteFile).toHaveBeenCalledWith( 45 | path.join(import.meta.dirname, '..', srcDir, 'file1', 'class.json'), 46 | JSON.stringify(['class1', 'class2', 'class3'], null, 2) 47 | ) 48 | expect(mockWriteFile).toHaveBeenCalledWith( 49 | path.join(import.meta.dirname, '..', srcDir, 'file2', 'class.json'), 50 | JSON.stringify(['class4', 'class5'], null, 2) 51 | ) 52 | expect(totalClassNames).toBe(5) 53 | }) 54 | 55 | test('extractClasses throws error if no CSS files are found', async () => { 56 | const srcDir = 'emptyStyles' 57 | 58 | mockReaddir.mockResolvedValueOnce([]) 59 | 60 | await expect(extractClasses({ srcDir })).rejects.toThrow('No CSS files found in the specified directory') 61 | }) 62 | 63 | test('extractClasses throws error if reading CSS file fails', async () => { 64 | const srcDir = 'styles' 65 | 66 | mockReadFile.mockRejectedValueOnce(new Error('File not found')) 67 | 68 | await expect(extractClasses({ srcDir })).rejects.toThrow('Error processing file') 69 | }) 70 | 71 | test('extractClasses throws error if reading directory fails', async () => { 72 | const srcDir = 'invalidDir' 73 | 74 | mockReaddir.mockRejectedValueOnce(new Error('Directory not found')) 75 | 76 | await expect(extractClasses({ srcDir })).rejects.toThrow('Error extracting classes: Directory not found') 77 | }) 78 | -------------------------------------------------------------------------------- /src/components/avatar.css: -------------------------------------------------------------------------------- 1 | .avatar-group { 2 | @apply flex; 3 | 4 | :where(.avatar) { 5 | @apply overflow-hidden rounded-full; 6 | border: 2px solid var(--color-base-100); 7 | } 8 | } 9 | 10 | .avatar { 11 | @apply relative inline-flex align-middle text-base; 12 | 13 | & > div { 14 | @apply block aspect-square overflow-hidden; 15 | } 16 | 17 | & > span { 18 | @apply block aspect-square overflow-hidden; 19 | } 20 | } 21 | 22 | :where(.avatar) img { 23 | @apply h-full w-full object-cover; 24 | } 25 | 26 | .avatar-placeholder { 27 | & > div { 28 | @apply flex items-center justify-center; 29 | } 30 | 31 | & > span { 32 | @apply flex items-center justify-center; 33 | } 34 | } 35 | /* Pull up avatar style */ 36 | .pull-up { 37 | .avatar { 38 | @apply transition-all duration-250 ease-in; 39 | &:hover { 40 | @apply shadow-base-300/20 z-30 -translate-y-1 transform rounded-full shadow-sm; 41 | } 42 | } 43 | } 44 | 45 | .avatar-online-top { 46 | &:before { 47 | content: ""; 48 | @apply bg-success outline-base-100 absolute z-10 block rounded-full outline outline-2; 49 | width: 20%; 50 | height: 20%; 51 | top: 3%; 52 | right: 3%; 53 | } 54 | } 55 | .avatar-offline-top { 56 | &:before { 57 | content: ""; 58 | @apply bg-base-200 outline-base-100 absolute z-10 block rounded-full outline outline-2; 59 | width: 20%; 60 | height: 20%; 61 | top: 3%; 62 | right: 3%; 63 | } 64 | } 65 | .avatar-busy-top { 66 | &:before { 67 | content: ""; 68 | @apply bg-error outline-base-100 absolute z-10 block rounded-full outline outline-2; 69 | width: 20%; 70 | height: 20%; 71 | top: 3%; 72 | right: 3%; 73 | } 74 | } 75 | .avatar-away-top { 76 | &:before { 77 | content: ""; 78 | @apply bg-warning outline-base-100 absolute z-10 block rounded-full outline outline-2; 79 | width: 20%; 80 | height: 20%; 81 | top: 3%; 82 | right: 3%; 83 | } 84 | } 85 | .avatar-online-bottom { 86 | &:before { 87 | content: ""; 88 | @apply bg-success outline-base-100 absolute z-10 block rounded-full outline outline-2; 89 | width: 20%; 90 | height: 20%; 91 | bottom: 3%; 92 | right: 3%; 93 | } 94 | } 95 | .avatar-offline-bottom { 96 | &:before { 97 | content: ""; 98 | @apply bg-base-200 outline-base-100 absolute z-10 block rounded-full outline outline-2; 99 | width: 20%; 100 | height: 20%; 101 | bottom: 3%; 102 | right: 3%; 103 | } 104 | } 105 | .avatar-busy-bottom { 106 | &:before { 107 | content: ""; 108 | @apply bg-error outline-base-100 absolute z-10 block rounded-full outline outline-2; 109 | width: 20%; 110 | height: 20%; 111 | bottom: 3%; 112 | right: 3%; 113 | } 114 | } 115 | .avatar-away-bottom { 116 | &:before { 117 | content: ""; 118 | @apply bg-warning outline-base-100 absolute z-10 block rounded-full outline outline-2; 119 | width: 20%; 120 | height: 20%; 121 | bottom: 3%; 122 | right: 3%; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/vendor/notyf.css: -------------------------------------------------------------------------------- 1 | @keyframes notyf-fadeinright { 2 | 0% { 3 | opacity: 0; 4 | transform: translateX(-25%); 5 | } 6 | to { 7 | opacity: 1; 8 | transform: translateX(0%); 9 | } 10 | } 11 | 12 | @keyframes notyf-fadeoutleft { 13 | 0% { 14 | opacity: 1; 15 | transform: translateX(0); 16 | } 17 | to { 18 | opacity: 0; 19 | transform: translateX(-25%); 20 | } 21 | } 22 | 23 | .notyf__toast { 24 | @apply rounded-field shadow-base-300/20 !p-0 shadow-md max-sm:m-2 max-sm:w-11/12; 25 | 26 | .notyf__wrapper { 27 | @apply px-3 py-2.5; 28 | 29 | .notyf__dismiss { 30 | @apply !mr-0 rtl:!right-auto rtl:!left-0; 31 | 32 | .notyf__dismiss-btn { 33 | @apply w-full !opacity-50 hover:!opacity-100 active:!opacity-100; 34 | } 35 | } 36 | } 37 | 38 | .notyf__wrapper:has(> .notyf__dismiss) { 39 | @apply !pe-12; 40 | } 41 | } 42 | 43 | .notyf__toast--error { 44 | .notyf__ripple { 45 | @apply !bg-error; 46 | } 47 | .notyf__icon i { 48 | @apply !text-error; 49 | } 50 | } 51 | 52 | .notyf__toast--success { 53 | .notyf__ripple { 54 | @apply !bg-success; 55 | } 56 | .notyf__icon i { 57 | @apply !text-success; 58 | } 59 | } 60 | 61 | .notyf__icon { 62 | @apply bg-base-100 ring-base-100/40 !me-3 flex size-[26px] shrink-0 items-center justify-center rounded-full ring-4 rtl:!mr-0; 63 | 64 | & i { 65 | @apply size-[1.125rem]; 66 | } 67 | 68 | .notyf__icon--success:before, 69 | .notyf__icon--success:after, 70 | .notyf__icon--error:before, 71 | .notyf__icon--error:after { 72 | content: none; 73 | } 74 | 75 | .notyf__icon--error, 76 | .notyf__icon--success { 77 | display: inline-block; 78 | width: 1.125rem; 79 | height: 1.125rem; 80 | background-color: currentColor; 81 | -webkit-mask-image: var(--svg); 82 | mask-image: var(--svg); 83 | -webkit-mask-repeat: no-repeat; 84 | mask-repeat: no-repeat; 85 | -webkit-mask-size: 100% 100%; 86 | mask-size: 100% 100%; 87 | } 88 | 89 | .notyf__icon--error { 90 | --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0m2.7-6.3l12.6 12.6'/%3E%3C/svg%3E"); 91 | } 92 | 93 | .notyf__icon--success { 94 | --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M3 12a9 9 0 1 0 18 0a9 9 0 1 0-18 0'/%3E%3Cpath d='m9 12l2 2l4-4'/%3E%3C/g%3E%3C/svg%3E"); 95 | } 96 | } 97 | 98 | .notyf__message { 99 | @apply text-base; 100 | } 101 | 102 | [dir="rtl"] .notyf__dismiss { 103 | animation: notyf-fadeinright 0.3s forwards !important; 104 | animation-delay: 0.35s !important; 105 | } 106 | 107 | [dir="rtl"] .notyf__toast--disappear .notyf__dismiss { 108 | animation: notyf-fadeoutleft 0.3s forwards !important; 109 | } 110 | -------------------------------------------------------------------------------- /functions/filesExist.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'bun:test' 2 | import { readdirSync, statSync, existsSync } from 'fs' 3 | import { join } from 'path' 4 | 5 | const directories = ['base', 'components', 'themes', 'utilities'] 6 | const basePath = join(import.meta.dirname, '../src') 7 | const targetPath = join(import.meta.dirname, '..') 8 | 9 | // Helper function to check if a file exists 10 | const fileExists = path => existsSync(path) 11 | 12 | // Helper function to check if a directory exists 13 | const directoryExists = path => { 14 | try { 15 | return statSync(path).isDirectory() 16 | } catch (error) { 17 | console.log(error) 18 | return false 19 | } 20 | } 21 | 22 | // Test 1: Ensure corresponding CSS files and directories exist 23 | directories.forEach(dir => { 24 | test(`Ensure corresponding CSS files and directories exist for ${dir}`, () => { 25 | const srcDir = join(basePath, dir) 26 | const targetDir = join(targetPath, dir === 'themes' ? 'theme' : dir) 27 | 28 | if (!directoryExists(srcDir)) { 29 | throw new Error(`Source directory does not exist: ${srcDir}`) 30 | } 31 | 32 | const files = readdirSync(srcDir).filter(file => file.endsWith('.css')) 33 | 34 | // we need .css for reset but not .js 35 | const exclusions = ['reset'] 36 | 37 | files.forEach(file => { 38 | const targetFile = join(targetDir, file) 39 | expect(fileExists(targetFile)).toBe(true) 40 | 41 | const fileName = file.replace('.css', '') 42 | const dirPath = join(targetDir, fileName) 43 | 44 | if (exclusions.includes(fileName)) { 45 | return 46 | } 47 | 48 | expect(directoryExists(dirPath)).toBe(true) 49 | expect(fileExists(join(dirPath, 'index.js'))).toBe(true) 50 | expect(fileExists(join(dirPath, 'object.js'))).toBe(true) 51 | }) 52 | }) 53 | }) 54 | 55 | // Test 2: Ensure specific CSS files exist in ../colors 56 | test('Ensure specific CSS files exist in ../colors', () => { 57 | const colorFiles = ['properties.css', 'responsive.css', 'states.css'] 58 | const colorsDir = join(targetPath, 'colors') 59 | 60 | if (!directoryExists(colorsDir)) { 61 | throw new Error(`Colors directory does not exist: ${colorsDir}`) 62 | } 63 | 64 | colorFiles.forEach(file => { 65 | const filePath = join(colorsDir, file) 66 | expect(fileExists(filePath)).toBe(true) 67 | }) 68 | }) 69 | 70 | // Test 3: Ensure specific files exist in the root directory 71 | test('Ensure specific files exist in the root directory', () => { 72 | const rootFiles = [ 73 | 'chunks.css', 74 | 'flyonui.css', 75 | 'themes.css', 76 | 'imports.js', 77 | 'index.js', 78 | 'build.js', 79 | 'LICENSE', 80 | 'package.json', 81 | 'README.md' 82 | ] 83 | 84 | rootFiles.forEach(file => { 85 | const filePath = join(targetPath, file) 86 | expect(fileExists(filePath)).toBe(true) 87 | }) 88 | }) 89 | 90 | // Test 4: Ensure index.js exists in ../theme 91 | test('Ensure index.js exists in ../theme', () => { 92 | const themeIndexPath = join(targetPath, 'theme', 'index.js') 93 | expect(fileExists(themeIndexPath)).toBe(true) 94 | }) 95 | -------------------------------------------------------------------------------- /src/utilities/radius.css: -------------------------------------------------------------------------------- 1 | .rounded-box { 2 | border-radius: var(--radius-box); 3 | } 4 | 5 | .rounded-field { 6 | border-radius: var(--radius-field); 7 | } 8 | 9 | .rounded-selector { 10 | border-radius: var(--radius-selector); 11 | } 12 | 13 | .rounded-t-box { 14 | border-top-left-radius: var(--radius-box); 15 | border-top-right-radius: var(--radius-box); 16 | } 17 | 18 | .rounded-b-box { 19 | border-bottom-left-radius: var(--radius-box); 20 | border-bottom-right-radius: var(--radius-box); 21 | } 22 | 23 | .rounded-l-box { 24 | border-top-left-radius: var(--radius-box); 25 | border-bottom-left-radius: var(--radius-box); 26 | } 27 | 28 | .rounded-r-box { 29 | border-top-right-radius: var(--radius-box); 30 | border-bottom-right-radius: var(--radius-box); 31 | } 32 | 33 | .rounded-tl-box { 34 | border-top-left-radius: var(--radius-box); 35 | } 36 | 37 | .rounded-tr-box { 38 | border-top-right-radius: var(--radius-box); 39 | } 40 | 41 | .rounded-br-box { 42 | border-bottom-right-radius: var(--radius-box); 43 | } 44 | 45 | .rounded-bl-box { 46 | border-bottom-left-radius: var(--radius-box); 47 | } 48 | 49 | .rounded-t-field { 50 | border-top-left-radius: var(--radius-field); 51 | border-top-right-radius: var(--radius-field); 52 | } 53 | 54 | .rounded-b-field { 55 | border-bottom-left-radius: var(--radius-field); 56 | border-bottom-right-radius: var(--radius-field); 57 | } 58 | 59 | .rounded-l-field { 60 | border-top-left-radius: var(--radius-field); 61 | border-bottom-left-radius: var(--radius-field); 62 | } 63 | 64 | .rounded-r-field { 65 | border-top-right-radius: var(--radius-field); 66 | border-bottom-right-radius: var(--radius-field); 67 | } 68 | 69 | .rounded-tl-field { 70 | border-top-left-radius: var(--radius-field); 71 | } 72 | 73 | .rounded-tr-field { 74 | border-top-right-radius: var(--radius-field); 75 | } 76 | 77 | .rounded-br-field { 78 | border-bottom-right-radius: var(--radius-field); 79 | } 80 | 81 | .rounded-bl-field { 82 | border-bottom-left-radius: var(--radius-field); 83 | } 84 | 85 | .rounded-t-selector { 86 | border-top-left-radius: var(--radius-selector); 87 | border-top-right-radius: var(--radius-selector); 88 | } 89 | 90 | .rounded-b-selector { 91 | border-bottom-left-radius: var(--radius-selector); 92 | border-bottom-right-radius: var(--radius-selector); 93 | } 94 | 95 | .rounded-l-selector { 96 | border-top-left-radius: var(--radius-selector); 97 | border-bottom-left-radius: var(--radius-selector); 98 | } 99 | 100 | .rounded-r-selector { 101 | border-top-right-radius: var(--radius-selector); 102 | border-bottom-right-radius: var(--radius-selector); 103 | } 104 | 105 | .rounded-tl-selector { 106 | border-top-left-radius: var(--radius-selector); 107 | } 108 | 109 | .rounded-tr-selector { 110 | border-top-right-radius: var(--radius-selector); 111 | } 112 | 113 | .rounded-br-selector { 114 | border-bottom-right-radius: var(--radius-selector); 115 | } 116 | 117 | .rounded-bl-selector { 118 | border-bottom-left-radius: var(--radius-selector); 119 | } 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to FlyonUI 2 | 3 | We welcome contributions from the community! Whether you're reporting bugs, suggesting new features, or fixing issues, your help is appreciated 🤝 4 | 5 | ## Reporting Issues 6 | 7 | - Before opening a new issue, please first [search for existing issues](https://github.com/themeselection/flyonui/issues?q=) to avoid duplicates. 8 | - Provide detailed reports with as much context as possible, including steps to reproduce the issue, your environment, and any relevant logs or screenshots. 9 | - For hard-to-reproduce bugs, please include a minimal reproducible repository or detailed steps to recreate the issue. 10 | 11 | ## Fixing Existing Issues 12 | 13 | - Help us by [fixing existing issues](https://github.com/themeselection/flyonui/issues?q=). 14 | - Avoid working on issues already assigned to others to prevent duplicate efforts. 15 | - If you're interested in working on an issue, please add a comment to request assignment before starting work. This helps maintainers track who's working on what and prevents duplication of effort. 16 | - Use the following commit message format for fixes: `fix: # [description]`. This automatically closes the issue and adds the fix to the changelog upon release. 17 | 18 | ## Feature Requests 19 | 20 | - If you have an idea you'd like to discuss with the community, please [open a discussion](https://github.com/themeselection/flyonui/discussions/new?category=ideas-request-new-feature). 21 | - Please note that not all feature requests will be accepted, as some may not align with the vision or scope of the library. Don't take it personally if a request is rejected. 22 | 23 | ## Pull Requests 24 | 25 | - Pull requests should address [an open issue](https://github.com/themeselection/flyonui/issues?q=is%3Aissue+is%3Aopen+) **that is assigned to you**. If no issue exists, create one first. If an issue is not assigned to you, please request assignment in the comments before submitting a PR. This ensures we avoid duplicate efforts. 26 | - For minor changes like fixing typos, an issue is not required. Feel free to directly open a pull request. 27 | - For documentation fixes, including updates to the website, you can also submit a pull request without opening an issue first. 28 | 29 | ## Local Development Setup 30 | 31 | To build the FlyonUI package locally, follow these steps: 32 | 33 | 1. [Fork the repository](https://github.com/themeselection/flyonui/fork) and clone it to your local machine. 34 | ```bash 35 | git clone https://github.com/your-username/flyonui.git 36 | ``` 37 | 2. Install the necessary package dependencies: 38 | ```bash 39 | bun install 40 | ``` 41 | 3. Build the package: 42 | ```bash 43 | bun run build:css && bun run build:js 44 | ``` 45 | 4. You can now import FlyonUI into your `app.css`: 46 | ```css 47 | @import "tailwindcss"; 48 | @plugin "./path/to/flyonui/index.js"; 49 | @import "./path/to/flyonui/variants.css"; 50 | @source "./path/to/flyonui/dist/index.js" // only include if you have added node_modules/ in .gitignore 51 | ``` 52 | 5. Finally, include the `flyonui.js` file in your HTML: 53 | ```html 54 | 55 | -------------------------------------------------------------------------------- /src/components/stack.css: -------------------------------------------------------------------------------- 1 | .stack { 2 | @apply inline-grid place-items-center items-end; 3 | & > * { 4 | @apply col-start-1 row-start-1 h-full w-full opacity-60; 5 | transform: translateY(14%) scale(0.9); 6 | z-index: 1; 7 | } 8 | & > *:nth-child(2) { 9 | @apply opacity-80; 10 | transform: translateY(7%) scale(0.95); 11 | z-index: 2; 12 | } 13 | & > *:nth-child(1) { 14 | @apply opacity-100; 15 | transform: translateY(0) scale(1) translateX(0); 16 | z-index: 3; 17 | } 18 | } 19 | 20 | .stack-bottom-start > * { 21 | transform: translateY(14%) scale(0.9) translateX(-14%); 22 | z-index: 1; 23 | } 24 | .stack-bottom-start > *:nth-child(2) { 25 | transform: translateY(7%) scale(0.95) translateX(-7%); 26 | z-index: 2; 27 | } 28 | .stack-bottom-end > * { 29 | transform: translateY(14%) scale(0.9) translateX(14%); 30 | z-index: 1; 31 | } 32 | .stack-bottom-end > *:nth-child(2) { 33 | transform: translateY(7%) scale(0.95) translateX(7%); 34 | z-index: 2; 35 | } 36 | 37 | .stack-start > * { 38 | transform: translateX(-14%) scale(0.9); 39 | z-index: 1; 40 | } 41 | .stack-start > *:nth-child(2) { 42 | transform: translateX(-7%) scale(0.95); 43 | z-index: 2; 44 | } 45 | 46 | .stack-end > * { 47 | transform: translateX(14%) scale(0.9); 48 | z-index: 1; 49 | } 50 | .stack-end > *:nth-child(2) { 51 | transform: translateX(7%) scale(0.95); 52 | z-index: 2; 53 | } 54 | 55 | .stack-top > * { 56 | transform: translateY(-14%) scale(0.9); 57 | z-index: 1; 58 | } 59 | .stack-top > *:nth-child(2) { 60 | transform: translateY(-7%) scale(0.95); 61 | z-index: 2; 62 | } 63 | .stack-top-start > * { 64 | transform: translateY(-14%) scale(0.9) translateX(-14%); 65 | z-index: 1; 66 | } 67 | .stack-top-start > *:nth-child(2) { 68 | transform: translateY(-7%) scale(0.95) translateX(-7%); 69 | z-index: 2; 70 | } 71 | .stack-top-end > * { 72 | transform: translateY(-14%) scale(0.9) translateX(14%); 73 | z-index: 1; 74 | } 75 | .stack-top-end > *:nth-child(2) { 76 | transform: translateY(-7%) scale(0.95) translateX(7%); 77 | z-index: 2; 78 | } 79 | .stack-animated:hover > * { 80 | transform: translateY(7%) scale(1) translateX(0%); 81 | @apply transition-transform duration-300 ease-in-out; 82 | } 83 | .stack-top.stack-animated:hover > * { 84 | transform: translateY(-7%) scale(1); 85 | @apply transition-transform duration-300 ease-in-out; 86 | } 87 | .stack-bottom-start.stack-animated:hover > * { 88 | transform: translateY(7%) scale(1) translateX(-7%); 89 | @apply transition-transform duration-300 ease-in-out; 90 | } 91 | .stack-bottom-end.stack-animated:hover > * { 92 | transform: translateY(7%) scale(1) translateX(7%); 93 | @apply transition-transform duration-300 ease-in-out; 94 | } 95 | .stack-top-start.stack-animated:hover > * { 96 | transform: translateY(-7%) scale(1) translateX(-7%); 97 | @apply transition-transform duration-300 ease-in-out; 98 | } 99 | .stack-top-end.stack-animated:hover > * { 100 | transform: translateY(-7%) scale(1) translateX(7%); 101 | @apply transition-transform duration-300 ease-in-out; 102 | } 103 | -------------------------------------------------------------------------------- /src/components/mockup.css: -------------------------------------------------------------------------------- 1 | .mockup-code { 2 | @apply bg-base-200 text-base-content/80 rounded-box relative min-w-72 overflow-x-auto overflow-y-hidden py-5; 3 | direction: ltr; 4 | &:before { 5 | content: ""; 6 | @apply mb-4 block h-3 w-3 rounded-full opacity-30; 7 | box-shadow: 8 | 1.4em 0, 9 | 2.8em 0, 10 | 4.2em 0; 11 | } 12 | pre { 13 | @apply pe-5; 14 | &:before { 15 | content: ""; 16 | margin-right: 2.2ch; 17 | } 18 | } 19 | pre[data-prefix] { 20 | &:before { 21 | content: attr(data-prefix); 22 | @apply inline-block w-8 text-right opacity-50; 23 | } 24 | } 25 | } 26 | .mockup-window { 27 | @apply rounded-box relative flex w-full flex-col overflow-x-auto overflow-y-hidden pt-5; 28 | &:before { 29 | content: ""; 30 | @apply mb-4 block aspect-square h-3 shrink-0 self-start rounded-full opacity-30; 31 | box-shadow: 32 | 1.4em 0, 33 | 2.8em 0, 34 | 4.2em 0; 35 | } 36 | [dir="rtl"] &:before { 37 | @apply self-end; 38 | } 39 | pre[data-prefix] { 40 | &:before { 41 | content: attr(data-prefix); 42 | @apply inline-block text-right; 43 | } 44 | } 45 | } 46 | 47 | .mockup-browser { 48 | @apply rounded-box relative w-full overflow-x-auto overflow-y-hidden; 49 | pre[data-prefix] { 50 | &:before { 51 | content: attr(data-prefix); 52 | @apply inline-block text-right; 53 | } 54 | } 55 | .mockup-browser-toolbar { 56 | @apply my-3 inline-flex w-full items-center pe-[1.4em]; 57 | direction: ltr; 58 | &:before { 59 | content: ""; 60 | @apply me-[4.8rem] inline-block aspect-square h-3 rounded-full opacity-30; 61 | box-shadow: 62 | 1.4em 0, 63 | 2.8em 0, 64 | 4.2em 0; 65 | } 66 | .input { 67 | @apply relative mx-auto block h-7 w-96 overflow-hidden ps-8 pt-0.5 text-ellipsis whitespace-nowrap; 68 | &:before { 69 | content: ""; 70 | @apply absolute start-2 top-1/2 aspect-square h-3 -translate-y-1/2 rounded-full border-2 border-current opacity-60; 71 | } 72 | &:after { 73 | content: ""; 74 | @apply absolute start-5 top-1/2 h-2 translate-y-1/4 -rotate-45 rounded-full border border-current opacity-60; 75 | } 76 | } 77 | } 78 | } 79 | 80 | .mockup-phone { 81 | display: inline-block; 82 | border: 4px solid #4e4d4d; 83 | border-radius: 50px; 84 | background-color: #000; 85 | padding: 6px; 86 | margin: 0 auto; 87 | overflow: hidden; 88 | .mockup-phone-camera { 89 | @apply rounded-full; 90 | position: relative; 91 | top: 5px; 92 | left: 0px; 93 | background: #000; 94 | height: 25px; 95 | width: 33%; 96 | margin: 0 auto; 97 | 98 | &:after { 99 | content: ""; 100 | position: absolute; 101 | top: 25%; 102 | right: 8%; 103 | width: 12px; 104 | height: 12px; 105 | border-radius: 5px; 106 | background-color: #2a292d; 107 | } 108 | } 109 | .mockup-phone-display { 110 | overflow: hidden; 111 | border-radius: 40px; 112 | margin-top: -25px; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/js/plugins/select/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ISingleOptionOptions { 2 | description: string 3 | icon: string 4 | } 5 | 6 | export interface ISingleOption { 7 | title: string 8 | val: string 9 | disabled?: boolean 10 | selected?: boolean 11 | options?: ISingleOptionOptions | null 12 | } 13 | 14 | export interface IApiFieldMap { 15 | id: string 16 | val: string 17 | title: string 18 | icon?: string | null 19 | description?: string | null 20 | page?: string 21 | offset?: string 22 | limit?: string 23 | [key: string]: unknown 24 | } 25 | 26 | export interface ISelectOptions { 27 | value?: string | string[] 28 | isOpened?: boolean 29 | placeholder?: string 30 | hasSearch?: boolean 31 | minSearchLength?: number 32 | preventSearchFocus?: boolean 33 | mode?: string 34 | 35 | viewport?: string 36 | 37 | wrapperClasses?: string 38 | 39 | apiUrl?: string | null 40 | apiQuery?: string | null 41 | apiOptions?: RequestInit | null 42 | apiDataPart?: string | null 43 | apiSearchQueryKey?: string | null 44 | apiFieldsMap?: IApiFieldMap | null 45 | apiSelectedValues?: string | string[] 46 | apiIconTag?: string | null 47 | apiLoadMore?: 48 | | boolean 49 | | { 50 | perPage: number 51 | scrollThreshold: number 52 | } 53 | 54 | toggleTag?: string 55 | toggleClasses?: string 56 | toggleSeparators?: { 57 | items?: string 58 | betweenItemsAndCounter?: string 59 | } 60 | toggleCountText?: string | null 61 | toggleCountTextPlacement?: 'postfix' | 'prefix' | 'postfix-no-space' | 'prefix-no-space' 62 | toggleCountTextMinItems?: number 63 | toggleCountTextMode?: string 64 | 65 | tagsItemTemplate?: string 66 | tagsItemClasses?: string 67 | tagsInputId?: string 68 | tagsInputClasses?: string 69 | 70 | dropdownTag?: string 71 | dropdownClasses?: string 72 | dropdownDirectionClasses?: { 73 | top?: string 74 | bottom?: string 75 | } 76 | dropdownSpace: number 77 | dropdownPlacement: string | null 78 | dropdownVerticalFixedPlacement: 'top' | 'bottom' | null 79 | dropdownScope: 'window' | 'parent' 80 | 81 | extraMarkup?: string | string[] | null 82 | 83 | searchTemplate?: string 84 | searchWrapperTemplate?: string 85 | searchId?: string 86 | searchLimit?: number | typeof Infinity 87 | isSearchDirectMatch?: boolean 88 | searchClasses?: string 89 | searchWrapperClasses?: string 90 | searchPlaceholder?: string 91 | searchNoResultTemplate?: string | null 92 | searchNoResultText?: string | null 93 | searchNoResultClasses?: string | null 94 | optionAllowEmptyOption?: boolean 95 | 96 | optionTemplate?: string 97 | optionTag?: string 98 | optionClasses?: string 99 | 100 | descriptionClasses?: string 101 | 102 | iconClasses?: string 103 | 104 | isAddTagOnEnter?: boolean 105 | dropdownAutoPlacement?: boolean 106 | 107 | isSelectedOptionOnTop?: boolean 108 | } 109 | 110 | export interface ISelect { 111 | options?: ISelectOptions 112 | 113 | setValue(val: string | string[]): void 114 | open(): void 115 | close(): void 116 | addOption(items: ISingleOption | ISingleOption[]): void 117 | removeOption(values: string | string[]): void 118 | recalculateDirection(): void 119 | destroy(): void 120 | } 121 | --------------------------------------------------------------------------------