diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 34eeb9f..0000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -doc -mock-shop/** -public -nginx -node_modules -tests/mock-server/public -tests/e2e/dist -tests/e2e \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 852e02a..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,127 +0,0 @@ -/* eslint-disable */ -module.exports = { - root: true, - parser: 'vue-eslint-parser', - parserOptions: { - ecmaVersion: 2022, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - parser: '@typescript-eslint/parser', - ecmaFeatures: { - jsx: true // Allows for the parsing of JSX - }, - }, - extends: [ - 'materya', - 'materya/typescript', - "plugin:vue/vue3-recommended", - "eslint:recommended", - "@vue/typescript/recommended" - ], - env: { - node: true, - }, - settings: { - 'import/resolver': { - node: { - extensions: [".js", ".jsx", ".ts", ".tsx", '.vue'], - paths: ['@types'], - }, - }, - }, - rules: { - '@typescript-eslint/naming-convention': 'off', - 'max-len': ['error', { - code: 79, - ignoreComments: true, - ignoreTemplateLiterals: true, - ignoreStrings: true, - ignoreUrls: true, - }], - semi: ['error', 'never'], - 'import/extensions': 'off', - 'import/no-cycle': 'off', - 'import/named': 'off', // Does not work well with TS - camelcase: ['off', { ignoreDestructuring: true }], - 'no-unused-expressions': ['off', { allowShortCircuit: true }], - 'arrow-parens': ['error', 'as-needed', { requireForBlockBody: false }], - 'space-before-function-paren': ['off'], - '@typescript-eslint/no-unused-vars': ['error', { - varsIgnorePattern: '^\\\$', - }], - 'object-curly-newline': ['error', { - ObjectExpression: { consistent: true }, - }], - 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }], - 'template-tag-spacing': ['error', 'always'], - 'no-console': process.env.NODE_ENV === 'dev' ? 'off' : 'error', - 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', - 'comma-dangle': [2, 'always-multiline'], - 'import/no-unresolved': ['error', { - ignore: ['^@/@types/cresh-ui$', '^@', 'config'], - }], - 'lines-between-class-members': ['warn', 'always', { exceptAfterSingleLine: true }], - '@typescript-eslint/member-delimiter-style': ['error', { - "multiline": { - 'delimiter': 'none', - 'requireLast': false - }, - 'singleline': { - 'delimiter': 'semi', - 'requireLast': false - } - }], - 'vue/component-name-in-template-casing': ['error', 'PascalCase', { - 'registeredComponentsOnly': true, - 'ignores': [], - }], - 'vue/html-indent': 'off', - 'vue/attributes-order': 'off', - 'vue/no-boolean-default': ['error', 'default-false'], - 'vue/no-reserved-component-names': ['error'], - 'vue/padding-line-between-blocks': ['error', 'always'], - 'vue/require-name-property': ['error'], - 'vue/one-component-per-file': ['error'], - 'vue/custom-event-name-casing': ['error'], - 'vue/no-arrow-functions-in-watch': ['error'], - 'vue/no-custom-modifiers-on-v-model': ['error'], - 'vue/no-dupe-v-else-if': ['error'], - 'vue/no-multiple-template-root': ['off'], - 'no-undef': 'off', // @typescript-eslint/no-undef - 'no-param-reassign': ['error', { "props": false }], - 'no-shadow': 'off', // @typescript-eslint/no-shadow - 'vue/custom-event-name-casing': 'off', - 'no-nested-ternary': ['warn'], - 'vue/max-len': ['off'], // max-len exists - 'import/no-extraneous-dependencies':[ - 'error', - { - 'devDependencies':[ - '**/*.test.{js,ts,tsx}', - '**/*.spec.{js,ts,tsx}', - '**/cypress.config.{js,ts}', - '**/cypress/**', - '**/tests/**', - '**/vite.config.{js,ts}', - '**/vitest.config.{js,ts}', - '**/*.cy.{js,ts,tsx}', - ] - } - ] - }, - overrides: [ - { - files: [ - '**/__tests__/*.{j,t}s?(x)', - '**/tests/unit/**/*.spec.{j,t}s?(x)', - '**/tests/unit/**/*.test.{j,t}s?(x)' - ], - env: { - mocha: true - } - } - ], - ignorePatterns: [ - 'src/pages/*', // pages for examples purposes only - 'cypress.config.ts', - ] -} diff --git a/.gitignore b/.gitignore index 5c0d32c..a5e58b2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,12 @@ # Artifacts .DS_Store + +!.storybook + +!API_CONVENTIONS.md +!CONTRIBUTING.md +!CODE_OF_CONDUCT.md +!SUPPORT.md +!SECURITY.md +!FUNDING.yml \ No newline at end of file diff --git a/.storybook/locales/cn.json b/.storybook/locales/cn.json new file mode 100644 index 0000000..a655b57 --- /dev/null +++ b/.storybook/locales/cn.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "现代 Vue 3 组件库" + }, + "getting_started": { + "title": "开始使用", + "description": "了解如何在您的项目中安装和使用 Cresh UI" + }, + "components": { + "title": "组件", + "description": "探索所有可用组件" + }, + "theme": { + "title": "设计系统", + "description": "颜色、排版和设计令牌" + }, + "language_setup": { + "title": "国际化 (i18n)", + "description": "多语言支持和安装指南" + }, + "i18n_stories": { + "title": "i18n 展示", + "description": "语言切换器的交互式演示" + } + } +} diff --git a/.storybook/locales/de.json b/.storybook/locales/de.json new file mode 100644 index 0000000..06075ed --- /dev/null +++ b/.storybook/locales/de.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "Moderne Vue 3 Komponentenbibliothek" + }, + "getting_started": { + "title": "Erste Schritte", + "description": "Erfahren Sie, wie Sie Cresh UI in Ihrem Projekt installieren und verwenden" + }, + "components": { + "title": "Komponenten", + "description": "Erkunden Sie alle verfügbaren Komponenten" + }, + "theme": { + "title": "Designsystem", + "description": "Farben, Typographie und Design-Tokens" + }, + "language_setup": { + "title": "Internationalisierung (i18n)", + "description": "Mehrsprachiger Support und Installationsanleitung" + }, + "i18n_stories": { + "title": "i18n Präsentation", + "description": "Interaktive Demonstration des Sprachumschalters" + } + } +} diff --git a/.storybook/locales/en.json b/.storybook/locales/en.json new file mode 100644 index 0000000..5c333c6 --- /dev/null +++ b/.storybook/locales/en.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "Modern Vue 3 Component Library" + }, + "getting_started": { + "title": "Getting Started", + "description": "Learn how to install and use Cresh UI in your project" + }, + "components": { + "title": "Components", + "description": "Explore all available components" + }, + "theme": { + "title": "Design System", + "description": "Colors, typography, and design tokens" + }, + "language_setup": { + "title": "Internationalization (i18n)", + "description": "Multi-language support and setup guide" + }, + "i18n_stories": { + "title": "i18n Showcase", + "description": "Interactive language switcher demonstration" + } + } +} diff --git a/.storybook/locales/es.json b/.storybook/locales/es.json new file mode 100644 index 0000000..6afaf1b --- /dev/null +++ b/.storybook/locales/es.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "Biblioteca moderna de componentes Vue 3" + }, + "getting_started": { + "title": "Primeros pasos", + "description": "Aprenda a instalar y usar Cresh UI en su proyecto" + }, + "components": { + "title": "Componentes", + "description": "Explore todos los componentes disponibles" + }, + "theme": { + "title": "Sistema de diseño", + "description": "Colores, tipografía y tokens de diseño" + }, + "language_setup": { + "title": "Internacionalización (i18n)", + "description": "Soporte multilingüe y guía de instalación" + }, + "i18n_stories": { + "title": "Demostración i18n", + "description": "Demostración interactiva del selector de idiomas" + } + } +} diff --git a/.storybook/locales/fr.json b/.storybook/locales/fr.json new file mode 100644 index 0000000..93c48fe --- /dev/null +++ b/.storybook/locales/fr.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "Bibliothèque de composants Vue 3 moderne" + }, + "getting_started": { + "title": "Commencer", + "description": "Apprenez à installer et utiliser Cresh UI dans votre projet" + }, + "components": { + "title": "Composants", + "description": "Découvrez tous les composants disponibles" + }, + "theme": { + "title": "Système de design", + "description": "Couleurs, typographie et tokens de design" + }, + "language_setup": { + "title": "Internationalisation (i18n)", + "description": "Guide d'installation et support multilingue" + }, + "i18n_stories": { + "title": "Démonstration i18n", + "description": "Démonstration interactive du sélecteur de langue" + } + } +} diff --git a/.storybook/locales/it.json b/.storybook/locales/it.json new file mode 100644 index 0000000..2eedfc1 --- /dev/null +++ b/.storybook/locales/it.json @@ -0,0 +1,28 @@ +{ + "docs": { + "home": { + "title": "Cresh UI", + "subtitle": "Moderna libreria di componenti Vue 3" + }, + "getting_started": { + "title": "Iniziare", + "description": "Scopri come installare e utilizzare Cresh UI nel tuo progetto" + }, + "components": { + "title": "Componenti", + "description": "Esplora tutti i componenti disponibili" + }, + "theme": { + "title": "Sistema di design", + "description": "Colori, tipografia e token di design" + }, + "language_setup": { + "title": "Internazionalizzazione (i18n)", + "description": "Supporto multilingue e guida all'installazione" + }, + "i18n_stories": { + "title": "Dimostrazioni i18n", + "description": "Dimostrazione interattiva del selettore di lingua" + } + } +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..8392cd1 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,43 @@ +import type { StorybookConfig } from '@storybook/vue3-vite' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(ts|tsx|js|jsx)'], + addons: ['@storybook/addon-links', '@storybook/addon-docs'], + + framework: { + name: '@storybook/vue3-vite', + options: { + vueDocgenOptions: { + alias: { + '@': new URL('../src', import.meta.url).pathname, + }, + }, + }, + }, + + viteFinal: async config => ({ + ...config, + css: { + preprocessorOptions: { + scss: { + loadPaths: [path.resolve(__dirname, '../src/styles/preprocessor')], + additionalData: ` + @import "fonts"; + @import "break-points"; + @import "computeds"; + @import "utils"; + @import "default-colors"; + `, + quietDeps: true, + silenceDeprecations: ['legacy-js-api', 'import'], + }, + }, + }, + }), +} + +export default config diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html new file mode 100644 index 0000000..8aa9184 --- /dev/null +++ b/.storybook/manager-head.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/.storybook/manager.ts b/.storybook/manager.ts new file mode 100644 index 0000000..d613602 --- /dev/null +++ b/.storybook/manager.ts @@ -0,0 +1,6 @@ +import { addons } from 'storybook/manager-api' + +// Simple Storybook manager configuration +addons.setConfig({ + panelPosition: 'bottom', +}) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..32daa73 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.storybook/preview-localization.ts b/.storybook/preview-localization.ts new file mode 100644 index 0000000..b8bdbc0 --- /dev/null +++ b/.storybook/preview-localization.ts @@ -0,0 +1,479 @@ +type StorybookLocale = 'en' | 'fr' | 'de' | 'es' | 'it' | 'cn' + +type TranslationDictionary = Record, Record> + +const translations: TranslationDictionary = { + de: { + Name: 'Name', + Description: 'Beschreibung', + Default: 'Standard', + Control: 'Steuerung', + Props: 'Props', + Events: 'Ereignisse', + Slots: 'Slots', + 'Set string': 'Text setzen', + 'Set boolean': 'Booleschen Wert setzen', + 'Set number': 'Zahl setzen', + 'Set object': 'Objekt setzen', + 'Set other': 'Wert setzen', + 'Show code': 'Code anzeigen', + 'Checked state (v-model binding)': 'Ausgewählter Zustand (v-model-Bindung)', + 'Label text': 'Beschriftungstext', + 'Input name attribute': 'Name-Attribut des Eingabefelds', + 'Disable the checkbox': 'Kontrollkästchen deaktivieren', + 'The input value (v-model binding)': 'Eingabewert (v-model-Bindung)', + 'Label for the input field': 'Beschriftung für das Eingabefeld', + 'Placeholder text': 'Platzhaltertext', + 'Input type': 'Eingabetyp', + 'Disable the input': 'Eingabe deaktivieren', + 'Selected value (v-model binding)': 'Ausgewählter Wert (v-model-Bindung)', + 'Label for the select': 'Beschriftung für die Auswahl', + 'Disable the select': 'Auswahl deaktivieren', + 'Disable the radio button': 'Optionsfeld deaktivieren', + 'The value of this radio option': 'Wert dieser Radio-Option', + 'Disable the switch': 'Schalter deaktivieren', + 'Switch state (v-model binding)': 'Schalterzustand (v-model-Bindung)', + 'Switch label': 'Schalterbeschriftung', + 'Button variant/style': 'Button-Variante/Stil', + 'Button size': 'Button-Größe', + 'Disable the button': 'Button deaktivieren', + 'Show loading state': 'Ladezustand anzeigen', + 'Progress value (0-100)': 'Fortschrittswert (0-100)', + 'Progress bar color': 'Farbe der Fortschrittsleiste', + 'Indeterminate loading state': 'Unbestimmter Ladezustand', + 'Image source URL': 'Bildquell-URL', + 'Alternative text for accessibility': 'Alternativtext für Barrierefreiheit', + 'Tooltip title on hover': 'Tooltip-Titel beim Hover', + 'Fallback image URL for errors': 'Fallback-Bild-URL bei Fehlern', + 'Error message text': 'Fehlermeldungstext', + 'Show error message text': 'Fehlermeldung anzeigen', + 'Image width in pixels': 'Bildbreite in Pixeln', + 'Image height in pixels': 'Bildhöhe in Pixeln', + 'Aspect ratio preset': 'Seitenverhältnis-Voreinstellung', + 'Enable lazy loading': 'Lazy Loading aktivieren', + 'Responsive image srcset': 'Responsives Bild-Set', + 'Array of items with key and value': 'Array von Einträgen mit Schlüssel und Wert', + 'Dropdown open/closed state': 'Dropdown geöffnet/geschlossen', + 'Show chevron icon': 'Chevron-Symbol anzeigen', + 'Pre-selected item': 'Vorab ausgewählter Eintrag', + 'Alert type': 'Alarmtyp', + 'Alert title': 'Alarmtitel', + 'Show close button': 'Schließen-Schaltfläche anzeigen', + 'Error message (string, array, or object with message property)': 'Fehlermeldung (String, Array oder Objekt mit Nachricht)', + 'Hide the error icon': 'Fehlersymbol ausblenden', + 'Empty state message': 'Leerzustandsnachricht', + 'Icon name from icon library': 'Icon-Name aus der Bibliothek', + 'Card title': 'Kartentitel', + 'Show card border': 'Kartenrahmen anzeigen', + 'Show card shadow': 'Kartenschatten anzeigen', + 'Show/hide modal': 'Modal anzeigen/ausblenden', + 'Modal title': 'Modaltitel', + 'Prevent closing on background click': 'Schließen bei Hintergrundklick verhindern', + 'Modal width in pixels': 'Modalbreite in Pixeln', + 'Hide footer with action buttons': 'Footer mit Aktionsbuttons ausblenden', + 'Document label': 'Dokumentbezeichnung', + 'Document link/URL': 'Dokumentlink/URL', + 'Approval status (undefined, true, or false)': 'Freigabestatus (undefined, true oder false)', + 'Available actions: upload, validate, download, read': 'Verfügbare Aktionen: hochladen, validieren, herunterladen, lesen', + 'Show/hide the document viewer modal': 'Dokumentenanzeige-Modal anzeigen/ausblenden', + 'Document to display (label, link, extension)': 'Anzuzeigendes Dokument (Bezeichnung, Link, Erweiterung)', + 'Hide navigation arrows': 'Navigationspfeile ausblenden', + 'Hide left arrow': 'Linken Pfeil ausblenden', + 'Hide right arrow': 'Rechten Pfeil ausblenden', + 'Maximum file size in KB (default: 2048)': 'Maximale Dateigröße in KB (Standard: 2048)', + 'Array of valid MIME types': 'Array gültiger MIME-Typen', + 'Show uploaded filename': 'Hochgeladenen Dateinamen anzeigen', + 'Drawer placement position': 'Position des Drawers', + 'Drawer width in pixels (for left/right placement)': 'Drawer-Breite in Pixeln (für links/rechts)', + 'Drawer height in pixels (for top/bottom placement)': 'Drawer-Höhe in Pixeln (für oben/unten)', + 'Mobile breakpoint width to auto-close drawer': 'Mobiler Breakpoint zum automatischen Schließen des Drawers', + 'Whether the drawer is open': 'Ob der Drawer geöffnet ist', + 'Array of step items': 'Array von Schritten', + 'Allow clicking on steps to navigate': 'Klicken auf Schritte zur Navigation erlauben', + 'Currently selected step index': 'Aktuell ausgewählter Schrittindex', + 'Array of step items with state': 'Array von Schritten mit Status', + 'Default date filter': 'Standard-Datumsfilter', + 'Date range array [startDate, endDate]': 'Datumsbereich-Array [Startdatum, Enddatum]', + 'Which input initiated the selection': 'Welches Feld die Auswahl gestartet hat', + 'Allow navigation between steps': 'Navigation zwischen Schritten erlauben', + 'Menu title': 'Menütitel', + 'Currently selected menu item': 'Aktuell ausgewählter Menüpunkt', + 'Divider direction': 'Ausrichtung des Trenners', + 'Optional text to display': 'Optional anzuzeigender Text', + 'Tag color/type': 'Tag-Farbe/-Typ', + 'Spinner size': 'Spinner-Größe', + 'Spinner color': 'Spinner-Farbe', + 'Make tabs read-only': 'Tabs schreibgeschützt machen', + 'Accept terms and conditions': 'Allgemeine Geschäftsbedingungen akzeptieren', + 'Checked:': 'Aktiviert:', + 'This option is disabled': 'Diese Option ist deaktiviert', + 'Disabled and checked': 'Deaktiviert und aktiviert', + 'Full Name': 'Vollständiger Name', + 'Enter your full name': 'Geben Sie Ihren vollständigen Namen ein', + 'Value:': 'Wert:', + Email: 'E-Mail', + 'Enter your email': 'Geben Sie Ihre E-Mail ein', + Password: 'Passwort', + 'Enter your password': 'Geben Sie Ihr Passwort ein', + 'Disabled Input': 'Deaktivierte Eingabe', + 'Input with Error': 'Eingabe mit Fehler', + 'Primary Button': 'Primärer Button', + 'Secondary Button': 'Sekundärer Button', + Delete: 'Löschen', + 'Disabled Button': 'Deaktivierter Button', + 'Loading...': 'Lädt...', + Small: 'Klein', + 'Large Button': 'Großer Button', + 'Project A - 30%': 'Projekt A - 30 %', + 'Project B - 60%': 'Projekt B - 60 %', + 'Project C - 100%': 'Projekt C - 100 %', + 'Loading - Indeterminate': 'Ladevorgang - Unbestimmt', + }, + fr: { + Description: 'Description', + Default: 'Defaut', + Control: 'Controle', + Props: 'Props', + Events: 'Evenements', + Slots: 'Slots', + 'Set string': 'Definir une chaine', + 'Set boolean': 'Definir un booleen', + 'Set number': 'Definir un nombre', + 'Set object': 'Definir un objet', + 'Set other': 'Definir une valeur', + 'Show code': 'Afficher le code', + 'Checked state (v-model binding)': 'Etat coche (liaison v-model)', + 'Label text': 'Texte du libelle', + 'Input name attribute': 'Attribut name du champ', + 'Disable the checkbox': 'Desactiver la case a cocher', + 'The input value (v-model binding)': 'Valeur du champ (liaison v-model)', + 'Label for the input field': 'Libelle du champ de saisie', + 'Placeholder text': 'Texte du placeholder', + 'Input type': 'Type de champ', + 'Disable the input': 'Desactiver le champ', + 'Selected value (v-model binding)': 'Valeur selectionnee (liaison v-model)', + 'Label for the select': 'Libelle de la liste', + 'Disable the select': 'Desactiver la liste', + 'Disable the radio button': 'Desactiver le bouton radio', + 'Button variant/style': 'Variante/style du bouton', + 'Button size': 'Taille du bouton', + 'Disable the button': 'Desactiver le bouton', + 'Show loading state': 'Afficher l etat de chargement', + 'Progress value (0-100)': 'Valeur de progression (0-100)', + 'Progress bar color': 'Couleur de la barre de progression', + 'Indeterminate loading state': 'Etat de chargement indetermine', + 'Alert type': 'Type d alerte', + 'Alert title': 'Titre de l alerte', + 'Show close button': 'Afficher le bouton de fermeture', + 'Accept terms and conditions': 'Accepter les termes et conditions', + 'Checked:': 'Coche :', + 'This option is disabled': 'Cette option est desactivee', + 'Disabled and checked': 'Desactivee et cochee', + 'Full Name': 'Nom complet', + 'Enter your full name': 'Saisissez votre nom complet', + 'Value:': 'Valeur :', + Email: 'E-mail', + 'Enter your email': 'Saisissez votre e-mail', + Password: 'Mot de passe', + 'Enter your password': 'Saisissez votre mot de passe', + 'Disabled Input': 'Champ desactive', + 'Input with Error': 'Champ avec erreur', + 'Primary Button': 'Bouton principal', + 'Secondary Button': 'Bouton secondaire', + Delete: 'Supprimer', + 'Disabled Button': 'Bouton desactive', + 'Loading...': 'Chargement...', + Small: 'Petit', + 'Large Button': 'Grand bouton', + 'Project A - 30%': 'Projet A - 30 %', + 'Project B - 60%': 'Projet B - 60 %', + 'Project C - 100%': 'Projet C - 100 %', + 'Loading - Indeterminate': 'Chargement - Indetermine', + }, + es: { + Description: 'Descripcion', + Default: 'Predeterminado', + Control: 'Control', + Props: 'Props', + Events: 'Eventos', + Slots: 'Slots', + 'Set string': 'Definir texto', + 'Set boolean': 'Definir booleano', + 'Set number': 'Definir numero', + 'Set object': 'Definir objeto', + 'Set other': 'Definir valor', + 'Show code': 'Mostrar codigo', + 'Checked state (v-model binding)': 'Estado marcado (v-model)', + 'Label text': 'Texto de la etiqueta', + 'Input name attribute': 'Atributo name del campo', + 'Disable the checkbox': 'Desactivar la casilla', + 'The input value (v-model binding)': 'Valor del campo (v-model)', + 'Label for the input field': 'Etiqueta del campo', + 'Placeholder text': 'Texto de placeholder', + 'Input type': 'Tipo de entrada', + 'Disable the input': 'Desactivar la entrada', + 'Selected value (v-model binding)': 'Valor seleccionado (v-model)', + 'Label for the select': 'Etiqueta del selector', + 'Disable the select': 'Desactivar el selector', + 'Disable the radio button': 'Desactivar el boton de opcion', + 'Button variant/style': 'Variante/estilo del boton', + 'Button size': 'Tamano del boton', + 'Disable the button': 'Desactivar el boton', + 'Show loading state': 'Mostrar estado de carga', + 'Progress value (0-100)': 'Valor de progreso (0-100)', + 'Progress bar color': 'Color de la barra de progreso', + 'Indeterminate loading state': 'Estado de carga indeterminado', + 'Accept terms and conditions': 'Aceptar terminos y condiciones', + 'Checked:': 'Marcado:', + 'This option is disabled': 'Esta opcion esta desactivada', + 'Disabled and checked': 'Desactivado y marcado', + 'Full Name': 'Nombre completo', + 'Enter your full name': 'Introduzca su nombre completo', + 'Value:': 'Valor:', + Email: 'Correo electronico', + 'Enter your email': 'Introduzca su correo electronico', + Password: 'Contrasena', + 'Enter your password': 'Introduzca su contrasena', + 'Disabled Input': 'Entrada desactivada', + 'Input with Error': 'Entrada con error', + 'Primary Button': 'Boton principal', + 'Secondary Button': 'Boton secundario', + Delete: 'Eliminar', + 'Disabled Button': 'Boton desactivado', + 'Loading...': 'Cargando...', + Small: 'Pequeno', + 'Large Button': 'Boton grande', + }, + it: { + Description: 'Descrizione', + Default: 'Predefinito', + Control: 'Controllo', + Props: 'Props', + Events: 'Eventi', + Slots: 'Slots', + 'Set string': 'Imposta testo', + 'Set boolean': 'Imposta booleano', + 'Set number': 'Imposta numero', + 'Set object': 'Imposta oggetto', + 'Set other': 'Imposta valore', + 'Show code': 'Mostra codice', + 'Checked state (v-model binding)': 'Stato selezionato (binding v-model)', + 'Label text': 'Testo etichetta', + 'Input name attribute': 'Attributo name del campo', + 'Disable the checkbox': 'Disabilita la checkbox', + 'The input value (v-model binding)': 'Valore input (binding v-model)', + 'Label for the input field': 'Etichetta del campo', + 'Placeholder text': 'Testo segnaposto', + 'Input type': 'Tipo di input', + 'Disable the input': 'Disabilita l input', + 'Selected value (v-model binding)': 'Valore selezionato (binding v-model)', + 'Label for the select': 'Etichetta della select', + 'Disable the select': 'Disabilita la select', + 'Disable the radio button': 'Disabilita il pulsante radio', + 'Button variant/style': 'Variante/stile del pulsante', + 'Button size': 'Dimensione del pulsante', + 'Disable the button': 'Disabilita il pulsante', + 'Show loading state': 'Mostra stato di caricamento', + 'Progress value (0-100)': 'Valore di avanzamento (0-100)', + 'Progress bar color': 'Colore della barra di avanzamento', + 'Indeterminate loading state': 'Stato di caricamento indeterminato', + 'Accept terms and conditions': 'Accetta termini e condizioni', + 'Checked:': 'Selezionato:', + 'This option is disabled': 'Questa opzione e disabilitata', + 'Disabled and checked': 'Disabilitato e selezionato', + 'Full Name': 'Nome completo', + 'Enter your full name': 'Inserisci il tuo nome completo', + 'Value:': 'Valore:', + Email: 'Email', + 'Enter your email': 'Inserisci la tua email', + Password: 'Password', + 'Enter your password': 'Inserisci la tua password', + 'Disabled Input': 'Input disabilitato', + 'Input with Error': 'Input con errore', + 'Primary Button': 'Pulsante primario', + 'Secondary Button': 'Pulsante secondario', + Delete: 'Elimina', + 'Disabled Button': 'Pulsante disabilitato', + 'Loading...': 'Caricamento...', + Small: 'Piccolo', + 'Large Button': 'Pulsante grande', + }, + cn: { + Description: '描述', + Default: '默认值', + Control: '控件', + Props: '属性', + Events: '事件', + Slots: '插槽', + 'Set string': '设置文本', + 'Set boolean': '设置布尔值', + 'Set number': '设置数字', + 'Set object': '设置对象', + 'Set other': '设置值', + 'Show code': '显示代码', + 'Checked state (v-model binding)': '选中状态(v-model 绑定)', + 'Label text': '标签文本', + 'Input name attribute': '输入框 name 属性', + 'Disable the checkbox': '禁用复选框', + 'The input value (v-model binding)': '输入值(v-model 绑定)', + 'Label for the input field': '输入框标签', + 'Placeholder text': '占位文本', + 'Input type': '输入类型', + 'Disable the input': '禁用输入框', + 'Selected value (v-model binding)': '选中值(v-model 绑定)', + 'Label for the select': '选择框标签', + 'Disable the select': '禁用选择框', + 'Disable the radio button': '禁用单选按钮', + 'Button variant/style': '按钮变体/样式', + 'Button size': '按钮尺寸', + 'Disable the button': '禁用按钮', + 'Show loading state': '显示加载状态', + 'Progress value (0-100)': '进度值(0-100)', + 'Progress bar color': '进度条颜色', + 'Indeterminate loading state': '不确定加载状态', + 'Accept terms and conditions': '接受条款和条件', + 'Checked:': '选中:', + 'This option is disabled': '该选项已禁用', + 'Disabled and checked': '已禁用且已选中', + 'Full Name': '姓名', + 'Enter your full name': '请输入您的姓名', + 'Value:': '值:', + Email: '邮箱', + 'Enter your email': '请输入您的邮箱', + Password: '密码', + 'Enter your password': '请输入您的密码', + 'Disabled Input': '禁用输入框', + 'Input with Error': '带错误的输入框', + 'Primary Button': '主按钮', + 'Secondary Button': '次按钮', + Delete: '删除', + 'Disabled Button': '禁用按钮', + 'Loading...': '加载中...', + Small: '小', + 'Large Button': '大按钮', + }, +} + +const ignoredTags = new Set(['CODE', 'PRE', 'SCRIPT', 'STYLE']) +const textSources = new WeakMap() +const attributeSources = new WeakMap>() + +let observer: MutationObserver | null = null +let currentLocale: StorybookLocale = 'en' +let isApplying = false + +function translateValue(input: string, locale: StorybookLocale) { + if (locale === 'en') { + return input + } + + const dictionary = translations[locale] + + return Object.entries(dictionary) + .sort((left, right) => right[0].length - left[0].length) + .reduce((value, [source, target]) => value.split(source).join(target), input) +} + +function shouldIgnoreTextNode(node: Text) { + const parent = node.parentElement + + if (!parent) { + return true + } + + return ignoredTags.has(parent.tagName) || Boolean(parent.closest('code, pre, .docblock-source')) +} + +function localizeTextNode(node: Text) { + if (shouldIgnoreTextNode(node)) { + return + } + + if (!textSources.has(node)) { + textSources.set(node, node.textContent || '') + } + + const source = textSources.get(node) || '' + const translated = translateValue(source, currentLocale) + + if (node.textContent !== translated) { + node.textContent = translated + } +} + +function localizeAttribute(element: Element, attribute: string) { + const value = element.getAttribute(attribute) + + if (!value) { + return + } + + const attributes = attributeSources.get(element) || {} + + if (!(attribute in attributes)) { + attributes[attribute] = value + attributeSources.set(element, attributes) + } + + const translated = translateValue(attributes[attribute], currentLocale) + + if (element.getAttribute(attribute) !== translated) { + element.setAttribute(attribute, translated) + } +} + +function walkAndLocalize(root: ParentNode) { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT) + let currentNode = walker.nextNode() + + while (currentNode) { + localizeTextNode(currentNode as Text) + currentNode = walker.nextNode() + } + + root.querySelectorAll?.('[placeholder], [title], [aria-label]').forEach(element => { + localizeAttribute(element, 'placeholder') + localizeAttribute(element, 'title') + localizeAttribute(element, 'aria-label') + }) +} + +function applyLocalization() { + if (typeof document === 'undefined' || !document.body || isApplying) { + return + } + + isApplying = true + + try { + walkAndLocalize(document.body) + } finally { + isApplying = false + } +} + +export function installStorybookPreviewLocalization(locale: StorybookLocale) { + currentLocale = locale + + if (typeof window === 'undefined' || typeof document === 'undefined') { + return + } + + if (!observer) { + observer = new MutationObserver(() => { + applyLocalization() + }) + + observer.observe(document.body, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ['placeholder', 'title', 'aria-label'], + }) + } + + window.requestAnimationFrame(() => { + applyLocalization() + }) +} diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..d25d689 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,82 @@ +import { setup, type Preview } from '@storybook/vue3' +import i18n from '../src/plugins/i18n.plugin' +import { installStorybookPreviewLocalization } from './preview-localization' + +// Import global styles +import '../src/styles/colors.css' + +setup(app => { + app.use(i18n) +}) + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: 'padded', + }, + globalTypes: { + locale: { + name: 'Locale', + description: 'Internationalization locale', + defaultValue: i18n.global.locale.value, + toolbar: { + icon: 'globe', + items: [ + { value: 'en', title: 'English' }, + { value: 'fr', title: 'Francais' }, + { value: 'de', title: 'Deutsch' }, + { value: 'es', title: 'Espanol' }, + { value: 'it', title: 'Italiano' }, + { value: 'cn', title: '中文' }, + ], + showName: true, + }, + }, + }, + decorators: [ + (story, context) => { + const locale = context.globals.locale || i18n.global.locale.value + + i18n.global.locale.value = locale + installStorybookPreviewLocalization(locale as 'en' | 'fr' | 'de' | 'es' | 'it' | 'cn') + + if (typeof document !== 'undefined') { + document.documentElement.lang = locale + document.documentElement.dir = 'ltr' + + // Update page metadata based on story + const storyTitle = context.title || 'Cresh UI' + const storyName = context.name || 'Story' + const fullTitle = `${storyTitle} - ${storyName} | Cresh UI` + + // Update document title + document.title = fullTitle + + // Update Open Graph title + const ogTitle = document.querySelector('meta[property="og:title"]') + if (ogTitle) { + ogTitle.setAttribute('content', fullTitle) + } + + // Update canonical URL if in a story view + const canonical = document.querySelector('link[rel="canonical"]') + if (canonical && context.id) { + canonical.setAttribute('href', `https://storybook.cresh-ui.dev/?path=/story/${context.id}`) + } + } + + return { + components: { story }, + template: '', + } + }, + ], +} + +export default preview diff --git a/API_CONVENTIONS.md b/API_CONVENTIONS.md new file mode 100644 index 0000000..4520625 --- /dev/null +++ b/API_CONVENTIONS.md @@ -0,0 +1,235 @@ +# Cresh UI - API Conventions Charter + +This document defines consistent API conventions for all Cresh UI components to reduce learning curve and ensure predictable behavior across the library. + +## 1. Form Input Components + +Components that accept user input should follow Vue 3's v-model pattern. + +### Pattern: +- **Prop**: `modelValue` (the current value) +- **Event**: `@update:modelValue` (emitted when value changes) +- **Usage**: `v-model="variable"` + +### Components: +- CInput +- CTextarea +- CSelect +- CSwitch +- CSlider +- CCheckbox +- CDatetimeInput +- CFileInput + +### Example: +```vue + + +``` + +--- + +## 2. Overlay & Dialog Components + +Components that control visibility should combine v-model for visibility with specific action events. + +### Pattern: +- **Prop**: `modelValue` (boolean) - controls visibility +- **Event**: `@update:modelValue` - emitted when visibility changes +- **Additional Events**: Specific action events (`ok`, `cancel`, `prev`, `next`, etc.) + +### Components: +- CModal +- CDocumentViewer + +### Example: +```vue + + Modal content + +``` + +--- + +## 3. File Upload Components + +File upload components emit file data directly without v-model binding for the file itself. + +### Pattern: +- **Event**: `@uploaded` - emitted with the uploaded File object +- **No v-model**: Files are not stored in a "value" model, only emitted as events + +### Components: +- CFileUploadZone +- CFileInput + +### Example: +```vue + +``` + +--- + +## 4. Navigation & Action Components + +Components that trigger navigation or selection actions should use consistent, descriptive event names. + +### Pattern: +- **Events**: Use simple, present-tense verbs (`select`, `prev`, `next`, `close`, etc.) +- **Prefix**: Navigation events use directional names (`prev`, `next`) +- **Selection events**: Use `select` or specific action names + +### Components: +- CDocumentViewer: `prev`, `next` +- CVerticalStepper: `select` +- CDropdown: `select` +- CAccordion: `accordion-change` + +### Example: +```vue + + + +``` + +--- + +## 5. Composite & Layout Components + +Components that compose other components should follow appropriate sub-pattern conventions. + +### Pattern: +- If contains form inputs: Use v-model for relevant properties +- If contains actions: Use specific event names +- If contains slots: Document slot scope and properties + +### Components: +- CHeader +- CTable +- CCarousel + +--- + +## 6. Event Naming Best Practices + +### ✅ DO: +- Use lowercase, kebab-case event names: `@update:modelValue`, `@uploaded`, `@prev` +- Use past participle for actions that complete: `uploaded`, `selected`, `changed` +- Use present tense for navigation: `prev`, `next` +- Use specific names for dialog actions: `ok`, `cancel` (not generic `submit`/`close`) + +### ❌ DON'T: +- Mix conventions: Don't use `switchState` when `update:modelValue` is available +- Use generic names: Avoid `@action` or `@change` alone +- Use camelCase in event names: Use `update:modelValue` not `updateModelValue` +- Use custom event names for v-model candidates + +--- + +## 7. Implementation Checklist + +When creating or updating a component, verify: + +- [ ] **Form Input?** → Use `modelValue` + `@update:modelValue` +- [ ] **Overlay/Dialog?** → Use `modelValue` for visibility + specific action events +- [ ] **File Upload?** → Emit `@uploaded` with File object +- [ ] **Navigation?** → Use `@prev`, `@next`, or `@select` +- [ ] **Custom Events?** → Use past participle (`loaded`, `changed`) or present tense (`select`, `next`) +- [ ] **Documentation?** → Document props, events, and slots in Storybook argTypes +- [ ] **Tests?** → Test event emissions match the expected convention + +--- + +## 8. Migration Guide + +If updating existing components to follow conventions: + +### ❌ Before (Inconsistent): +```typescript +// CSwitch - custom event +emit('switchState', checked) + +// CFileUploadZone - no v-model +emit('uploaded', file) + +// CAccordion - kebab-case custom event +emit('accordion-change', index) +``` + +### ✅ After (Consistent): +```typescript +// CSwitch - v-model support +emit('update:modelValue', checked) + +// CFileUploadZone - consistent event naming +emit('uploaded', file) + +// CAccordion - consistent action event naming (consider: emit('select', index)) +emit('accordion-change', index) +``` + +--- + +## 9. Examples by Component Type + +### Form Input (with v-model) +```vue + + + +``` + +### Overlay (v-model + actions) +```vue + + + +``` + +### File Upload (event-driven) +```vue + + + +``` + +--- + +## 10. Notes + +- **Vue 3 Compliance**: All conventions follow Vue 3 composition API best practices +- **Accessibility**: Event conventions should align with semantic HTML and ARIA patterns +- **Type Safety**: Use TypeScript generics with `defineEmits<{ (e: 'event-name', payload): void }>()` +- **Backward Compatibility**: Mark breaking changes clearly when updating event names diff --git a/CHANGELOG.md b/CHANGELOG.md index c6c5ddc..168ab0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,132 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [6.0.0](https://github.com/creshpay/cresh-ui/compare/v5.18.0...v6.0.0) (2026-04-30) + +### ⚠ BREAKING CHANGES + +- **peerDependencies bumped to majors:** + - `vue-router` `^4.2.2` → `^5.0.6` + - `vue-i18n` `^9.14.5` → `^11.4.0` + - `vue` peer floor raised to `^3.5.33` (still 3.x) +- **`@vuepic/vue-datepicker` 8 → 12** — the package no longer ships a default + export. Internal usage was migrated to + `import { VueDatePicker as Datepicker } from '@vuepic/vue-datepicker'`. + The `locale` prop now expects a `date-fns` `Locale` object instead of a string. +- **Node.js `>=22`** is now required (`engines.node`). +- **ESLint flat config** (`eslint.config.js`) replaces `.eslintrc.js`. Consumers + extending the legacy file path will need to migrate. + +### Features + +#### A11y & UX +- Added unified `error` slot on every form-control (`CInput`, `CSelect`, + `CTextarea`, `CRadio`, `CDatetimeInput`, `CCheckbox`). Legacy `errors` slot + still works but logs a deprecation warning in development. +- `CCheckbox` rebuilt with a visually-hidden native `` inside its label + (proper screen-reader support + visible focus ring). +- ARIA wiring on every form-control: `aria-invalid`, `aria-describedby`, + `aria-required`, error region with stable `id`. +- `CDropdown` now exposes a full ARIA listbox/option pattern with keyboard + navigation (↑ ↓ Home End Enter Space Esc Tab) and `aria-activedescendant`. +- `CSwitch` exposes `role="switch"` and an `aria-label` prop. +- `CModal` listens for `Escape` at the document level when open and now uses + `` (configurable via the new `teleport` and `teleportTarget` props). +- All hardcoded English `aria-label`s (`CTag`, `CDocumentViewer`, + `CFileUploadZone`) are translated through `vue-i18n`. Added + `common.aria.{close, previous, next, upload_zone}` to all 6 locales (en, fr, + de, es, it, cn). +- `CFileUploadZone` file `` is now `tabindex="-1" aria-hidden="true"`; + the visible button is the real keyboard trigger. + +#### API +- New composable `useFormControl(props, fallbackName)` centralising + `inputId` / `errorId` / `hasError` / `hasErrorSlot` / `hasValueOrPlaceholder`. +- `CButton` rewritten with ` diff --git a/src/components/form-controls/CCreditCardInput.stories.ts b/src/components/form-controls/CCreditCardInput.stories.ts new file mode 100644 index 0000000..b60fb26 --- /dev/null +++ b/src/components/form-controls/CCreditCardInput.stories.ts @@ -0,0 +1,111 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import { ref } from 'vue' +import { useI18n } from 'vue-i18n' +import CCreditCardInput from './CCreditCardInput.vue' + +const meta = { + title: 'Form Controls/CCreditCardInput', + component: CCreditCardInput, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { CCreditCardInput }, + setup() { + const { t } = useI18n() + const cardNumber = ref('') + return { cardNumber, t } + }, + template: ` +
+ +

+ {{ t('translate.showcase.credit_card.input_value') }} {{ cardNumber }} +

+
+ `, + }), +} + +export const WithError: Story = { + render: () => ({ + components: { CCreditCardInput }, + setup() { + const { t } = useI18n() + const cardNumber = ref('') + return { cardNumber, t } + }, + template: ` +
+ +
+ `, + }), +} + +export const Disabled: Story = { + render: () => ({ + components: { CCreditCardInput }, + setup() { + const { t } = useI18n() + const cardNumber = ref('4532 1488 0343 6467') + return { cardNumber, t } + }, + template: ` +
+ +
+ `, + }), +} + +export const NotBordered: Story = { + render: () => ({ + components: { CCreditCardInput }, + setup() { + const { t } = useI18n() + const cardNumber = ref('') + return { cardNumber, t } + }, + template: ` +
+ +

+ {{ t('translate.showcase.credit_card.input_value') }} {{ cardNumber }} +

+
+ `, + }), +} diff --git a/src/components/form-controls/CCreditCardInput.vue b/src/components/form-controls/CCreditCardInput.vue index 3d295e9..754e351 100644 --- a/src/components/form-controls/CCreditCardInput.vue +++ b/src/components/form-controls/CCreditCardInput.vue @@ -25,7 +25,7 @@ }" :name="name" :disabled="disabled" - :value="props.modelValue" + :value="model" :required="required" :autocomplete="autocomplete" :placeholder="placeholder" @@ -60,10 +60,11 @@ import { useSlots, computed } from 'vue' import baseProps from './base-control-props' -const emit = defineEmits(['update:modelValue', 'blur']) +const emit = defineEmits(['blur']) const props = defineProps({ ...baseProps, }) +const model = defineModel() const slots = useSlots() const hasErrorSlot = computed(() => !!slots.error) @@ -72,7 +73,7 @@ const hasError = computed(() => !!props.errorMessage) const hasPlaceholder = computed(() => !!props.placeholder) -const hasValue = computed(() => !!props.modelValue) +const hasValue = computed(() => !!model.value) const hasValueOrPlaceholder = computed( () => !!(hasValue.value || hasPlaceholder.value), @@ -85,7 +86,7 @@ function onInput (e: Event): void { const elementValue = target.value .replace(/[^0-9]/gi, '') .replace(/(.{4})/g, '$1 ').trim() - emit('update:modelValue', elementValue) + model.value = elementValue } } diff --git a/src/components/form-controls/CDatetimeInput.stories.ts b/src/components/form-controls/CDatetimeInput.stories.ts new file mode 100644 index 0000000..6de2947 --- /dev/null +++ b/src/components/form-controls/CDatetimeInput.stories.ts @@ -0,0 +1,135 @@ +import type { Meta, StoryObj } from '@storybook/vue3' +import { ref } from 'vue' +import { useI18n } from 'vue-i18n' +import CDatetimeInput from './CDatetimeInput.vue' + +const meta = { + title: 'Form Controls/CDatetimeInput', + component: CDatetimeInput, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + render: () => ({ + components: { CDatetimeInput }, + setup() { + const { t } = useI18n() + const date = ref('') + return { date, t } + }, + template: ` +
+ +

+ {{ t('translate.showcase.datetime.selected') }} {{ date || t('translate.showcase.datetime.not_set') }} +

+
+ `, + }), +} + +export const WithValue: Story = { + render: () => ({ + components: { CDatetimeInput }, + setup() { + const { t } = useI18n() + const date = ref('12/25/2024') + return { date, t } + }, + template: ` +
+ +

+ {{ t('translate.showcase.datetime.selected') }} {{ date }} +

+
+ `, + }), +} + +export const WithError: Story = { + render: () => ({ + components: { CDatetimeInput }, + setup() { + const { t } = useI18n() + const date = ref('') + return { date, t } + }, + template: ` +
+ +
+ `, + }), +} + +export const Required: Story = { + render: () => ({ + components: { CDatetimeInput }, + setup() { + const { t } = useI18n() + const date = ref('') + return { date, t } + }, + template: ` +
+ +
+ `, + }), +} + +export const Disabled: Story = { + render: () => ({ + components: { CDatetimeInput }, + setup() { + const { t } = useI18n() + const date = ref('01/15/2024') + return { date, t } + }, + template: ` +
+ +
+ `, + }), +} diff --git a/src/components/form-controls/CDatetimeInput.vue b/src/components/form-controls/CDatetimeInput.vue index 1d96378..ad068b0 100644 --- a/src/components/form-controls/CDatetimeInput.vue +++ b/src/components/form-controls/CDatetimeInput.vue @@ -8,14 +8,12 @@ > -