diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile deleted file mode 100644 index ff330f7..0000000 --- a/apps/api/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -# This file is generated by Nx. -# Build the docker image with `npx nx docker:build api`. -# Tip: Modify "docker:build" options in project.json to change docker build args. -# -# Run the container with `nx docker:run api -p 3000:3000`. -# -FROM docker.io/node:lts-alpine - -ENV HOST=0.0.0.0 -ENV PORT=3333 - - -WORKDIR /app - -COPY dist . - -# You can remove this install step if you build with `--bundle` option. -# The bundled output will include external dependencies. - -RUN npm --omit=dev -f install - -CMD [ "node", "main.js" ] diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs deleted file mode 100644 index b7f6277..0000000 --- a/apps/api/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import baseConfig from '../../eslint.config.mjs'; - -export default [...baseConfig]; diff --git a/apps/api/project.json b/apps/api/project.json deleted file mode 100644 index 873c498..0000000 --- a/apps/api/project.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "api", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/api/src", - "projectType": "application", - "tags": ["scope:api"], - "release": { - "docker": { - "repositoryName": "org/angular-template-api" - } - }, - "targets": { - "build": { - "executor": "@nx/esbuild:esbuild", - "outputs": ["{options.outputPath}"], - "defaultConfiguration": "production", - "options": { - "platform": "node", - "outputPath": "apps/api/dist", - "format": ["cjs"], - "bundle": false, - "main": "apps/api/src/main.ts", - "tsConfig": "apps/api/tsconfig.app.json", - "assets": ["apps/api/src/assets"], - "generatePackageJson": true, - "esbuildOptions": { - "sourcemap": true, - "outExtension": { - ".js": ".js" - } - } - }, - "configurations": { - "development": {}, - "production": { - "generateLockfile": true, - "esbuildOptions": { - "sourcemap": false, - "outExtension": { - ".js": ".js" - } - } - } - } - }, - "serve": { - "continuous": true, - "executor": "@nx/js:node", - "defaultConfiguration": "development", - "dependsOn": ["build"], - "options": { - "buildTarget": "api:build", - "runBuildTargetDependencies": false - }, - "configurations": { - "development": { - "buildTarget": "api:build:development" - }, - "production": { - "buildTarget": "api:build:production" - } - } - }, - "test": { - "options": { - "passWithNoTests": true - } - }, - "docker:build": { - "dependsOn": ["build"] - } - } -} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts deleted file mode 100644 index bc578a9..0000000 --- a/apps/api/src/main.ts +++ /dev/null @@ -1,139 +0,0 @@ -import express from 'express'; -import { ProductsService } from '@org/api/products'; -import { ApiResponse, Product, ProductFilter, PaginatedResponse } from '@org/models'; - -const host = process.env.HOST ?? 'localhost'; -const port = process.env.PORT ? Number(process.env.PORT) : 3333; - -const app = express(); -const productsService = new ProductsService(); - -// Middleware -app.use(express.json()); - -// CORS configuration for Angular app -app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); - if (req.method === 'OPTIONS') { - res.sendStatus(200); - } else { - next(); - } -}); - -app.get('/', (req, res) => { - res.send({ message: 'Hello API' }); -}); - -// Products endpoints -app.get('/api/products', (req, res) => { - try { - const filter: ProductFilter = {}; - - if (req.query.category) { - filter.category = req.query.category as string; - } - if (req.query.minPrice) { - filter.minPrice = Number(req.query.minPrice); - } - if (req.query.maxPrice) { - filter.maxPrice = Number(req.query.maxPrice); - } - if (req.query.inStock !== undefined) { - filter.inStock = req.query.inStock === 'true'; - } - if (req.query.searchTerm) { - filter.searchTerm = req.query.searchTerm as string; - } - - const page = req.query.page ? Number(req.query.page) : 1; - const pageSize = req.query.pageSize ? Number(req.query.pageSize) : 12; - - const result = productsService.getAllProducts(filter, page, pageSize); - - const response: ApiResponse> = { - data: result, - success: true, - }; - - res.json(response); - } catch (error) { - const response: ApiResponse = { - data: null, - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - res.status(500).json(response); - } -}); - -app.get('/api/products/:id', (req, res) => { - try { - const product = productsService.getProductById(req.params.id); - - if (!product) { - const response: ApiResponse = { - data: null, - success: false, - error: 'Product not found', - }; - return res.status(404).json(response); - } - - const response: ApiResponse = { - data: product, - success: true, - }; - - res.json(response); - } catch (error) { - const response: ApiResponse = { - data: null, - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - res.status(500).json(response); - } -}); - -app.get('/api/products-metadata/categories', (req, res) => { - try { - const categories = productsService.getCategories(); - const response: ApiResponse = { - data: categories, - success: true, - }; - res.json(response); - } catch (error) { - const response: ApiResponse = { - data: null, - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - res.status(500).json(response); - } -}); - -app.get('/api/products-metadata/price-range', (req, res) => { - try { - const priceRange = productsService.getPriceRange(); - const response: ApiResponse<{ min: number; max: number }> = { - data: priceRange, - success: true, - }; - res.json(response); - } catch (error) { - const response: ApiResponse = { - data: null, - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }; - res.status(500).json(response); - } -}); - -app.listen(port, host, () => { - console.log(`[ ready ] http://${host}:${port}`); -}); diff --git a/apps/api/tsconfig.app.json b/apps/api/tsconfig.app.json deleted file mode 100644 index 21e2061..0000000 --- a/apps/api/tsconfig.app.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json deleted file mode 100644 index c1e2dd4..0000000 --- a/apps/api/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "esModuleInterop": true - } -} diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json deleted file mode 100644 index 56b7488..0000000 --- a/apps/api/tsconfig.spec.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ] - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ] -} diff --git a/apps/api/vite.config.ts b/apps/api/vite.config.ts deleted file mode 100644 index b87ca39..0000000 --- a/apps/api/vite.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../node_modules/.vite/apps/api', - plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - test: { - name: 'api', - watch: false, - globals: true, - environment: 'node', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - passWithNoTests: true, - coverage: { - reportsDirectory: '../../coverage/apps/api', - provider: 'v8' as const, - }, - }, -})); diff --git a/apps/shop-e2e/eslint.config.mjs b/apps/shop-e2e/eslint.config.mjs deleted file mode 100644 index b2e9fac..0000000 --- a/apps/shop-e2e/eslint.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import playwright from 'eslint-plugin-playwright'; -import baseConfig from '../../eslint.config.mjs'; - -export default [ - playwright.configs['flat/recommended'], - ...baseConfig, - { - files: ['**/*.ts', '**/*.js'], - // Override or add rules here - rules: {}, - }, -]; diff --git a/apps/shop-e2e/playwright.config.ts b/apps/shop-e2e/playwright.config.ts deleted file mode 100644 index 421bc43..0000000 --- a/apps/shop-e2e/playwright.config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; -import { nxE2EPreset } from '@nx/playwright/preset'; -import { workspaceRoot } from '@nx/devkit'; - -// For CI, you may want to set BASE_URL to the deployed application. -const baseURL = process.env['BASE_URL'] || 'http://localhost:4200'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - ...nxE2EPreset(__filename, { testDir: './src' }), - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - baseURL, - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npx nx run shop:serve', - url: 'http://localhost:4200', - reuseExistingServer: true, - cwd: workspaceRoot, - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - // Uncomment to test wider variety of browsers - // Note, you will also need to update `.github/workflows/ci.yml` - // Replacing `npx playwright install --only-shell` with `npx playwright install --with-deps` - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, - - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, - - // Uncomment for mobile browsers support - /* { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, - }, */ - - // Uncomment for branded browsers - /* { - name: 'Microsoft Edge', - use: { ...devices['Desktop Edge'], channel: 'msedge' }, - }, - { - name: 'Google Chrome', - use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - } */ - ], -}); diff --git a/apps/shop-e2e/project.json b/apps/shop-e2e/project.json deleted file mode 100644 index e31f8c8..0000000 --- a/apps/shop-e2e/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "shop-e2e", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "sourceRoot": "apps/shop-e2e/src", - "implicitDependencies": ["shop"], - "targets": {} -} diff --git a/apps/shop-e2e/src/navigation-flow.spec.ts b/apps/shop-e2e/src/navigation-flow.spec.ts deleted file mode 100644 index e84ff25..0000000 --- a/apps/shop-e2e/src/navigation-flow.spec.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Navigation and User Flow', () => { - test('should complete a full user journey through the app', async ({ page }) => { - // Start at home page - await page.goto('/'); - - // Should redirect to products - await page.waitForURL('**/products'); - - // Verify we're on products page - const productsHeading = page.locator('h1:has-text("Our Products")'); - await expect(productsHeading).toBeVisible(); - - // Search for a specific product - const searchInput = page.locator('input[placeholder*="Search"]'); - await searchInput.fill('Product 1'); - await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0); - - // Click on the first search result - const firstResult = page.locator('[class*="product-card"]').first(); - const productName = await firstResult.locator('h3').textContent(); - await firstResult.click(); - - // Should navigate to product detail - await page.waitForURL('**/products/**'); - - // Verify product detail page - const detailProductName = page.locator('h1').filter({ hasText: productName || '' }); - await expect(detailProductName).toBeVisible(); - - // Click back to products - const backLink = page.locator('a:has-text("Back to Products")'); - await backLink.click(); - - // Should be back on products page - await page.waitForURL('**/products'); - await expect(productsHeading).toBeVisible(); - - // The search should be cleared - await expect(searchInput).toHaveValue(''); - }); - - test('should handle navigation via header link', async ({ page }) => { - // Start on a product detail page - await page.goto('/products/prod-1'); - await page.waitForLoadState('domcontentloaded'); - - // Click the Products link in the header - const headerProductsLink = page.locator('nav a:has-text("Products")'); - await headerProductsLink.click(); - - // Should navigate to products listing - await page.waitForURL('**/products'); - - // Verify products page is loaded - const productsGrid = page.locator('[class*="product"]').first(); - await expect(productsGrid).toBeVisible(); - }); - - test('should maintain filter state during navigation', async ({ page }) => { - // Go to products page - await page.goto('/products'); - await page.waitForLoadState('domcontentloaded'); - - // Apply filters - const categoryDropdown = page.locator('select'); - await categoryDropdown.selectOption('Electronics'); - - const searchInput = page.locator('input[placeholder*="Search"]'); - await searchInput.fill('Product'); - - // Wait for filtered results - await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0); - - // Navigate to a product - const product = page.locator('[class*="product-card"]').first(); - await product.click(); - - // Wait for product detail page - await page.waitForURL('**/products/**'); - - // Go back using browser back button - await page.goBack(); - - // Filters should be reset (this is the expected behavior in most SPAs) - // The search and category should be cleared - await expect(searchInput).toHaveValue(''); - await expect(categoryDropdown).toHaveValue(''); - }); - - test('should handle rapid navigation', async ({ page }) => { - // Navigate to products - await page.goto('/products'); - - // Quickly click multiple products - for (let i = 0; i < 3; i++) { - const product = page.locator('[class*="product-card"]').nth(i); - await product.click(); - await page.waitForURL('**/products/**'); - - // Verify page loaded correctly - const productDetail = page.locator('h1').nth(1); - await expect(productDetail).toBeVisible(); - - // Go back - const backLink = page.locator('a:has-text("Back to Products")'); - await backLink.click(); - await page.waitForURL('**/products'); - } - - // Should still be functional - const productsHeading = page.locator('h1:has-text("Our Products")'); - await expect(productsHeading).toBeVisible(); - }); - - test('should handle direct URL navigation', async ({ page }) => { - // Navigate directly to a product detail page - await page.goto('/products/prod-5'); - await page.waitForLoadState('domcontentloaded'); - - // Should load the correct product - const productName = page.locator('h1').filter({ hasText: 'Product 5' }); - await expect(productName).toBeVisible(); - - // Navigate directly to products page - await page.goto('/products'); - await page.waitForLoadState('domcontentloaded'); - - // Should show products listing - const productsGrid = page.locator('[class*="product-card"]'); - const count = await productsGrid.count(); - expect(count).toBeGreaterThan(0); - - // Navigate to non-existent route should redirect to products - await page.goto('/non-existent-route'); - await page.waitForURL('**/products'); - - // Should be on products page - const productsHeading = page.locator('h1:has-text("Our Products")'); - await expect(productsHeading).toBeVisible(); - }); - - test('should display loading states during navigation', async ({ page }) => { - // Enable slow network to see loading states - await page.route('**/*', route => { - setTimeout(() => route.continue(), 100); - }); - - await page.goto('/products'); - - // Click on a product - const product = page.locator('[class*="product-card"]').first(); - await product.click(); - - // Should eventually load the product detail - await page.waitForURL('**/products/**', { timeout: 10000 }); - - const productDetail = page.locator('[class*="product-detail"]'); - await expect(productDetail).toBeVisible({ timeout: 10000 }); - }); -}); \ No newline at end of file diff --git a/apps/shop-e2e/src/product-detail.spec.ts b/apps/shop-e2e/src/product-detail.spec.ts deleted file mode 100644 index d21e68a..0000000 --- a/apps/shop-e2e/src/product-detail.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Product Detail Page', () => { - test.beforeEach(async ({ page }) => { - // Navigate to products page first - await page.goto('/products'); - await page.waitForLoadState('domcontentloaded'); - }); - - test('should navigate to product detail when clicking a product', async ({ page }) => { - // Click on the first product - const firstProduct = page.locator('[class*="product-card"]').first(); - await firstProduct.click(); - - // Wait for navigation - await page.waitForURL('**/products/**'); - - // Verify URL changed to product detail - expect(page.url()).toMatch(/\/products\/prod-\d+/); - - // Verify back link is visible - const backLink = page.locator('a:has-text("Back to Products")'); - await expect(backLink).toBeVisible(); - }); - - test('should display complete product information', async ({ page }) => { - // Navigate directly to a product detail page - await page.goto('/products/prod-1'); - await page.waitForLoadState('domcontentloaded'); - - // Check product name - const productName = page.locator('h1').filter({ hasText: /Product \d+/ }); - await expect(productName).toBeVisible(); - - // Check product image - const productImage = page.locator('[class*="product-detail"] img'); - await expect(productImage).toBeVisible(); - await expect(productImage).toHaveAttribute('alt', /Product/); - - // Check category - const category = page.locator('text=/Home & Garden|Electronics|Clothing|Sports|Books/'); - await expect(category.first()).toBeVisible(); - - // Check rating - const ratingSection = page.locator('[class*="product-rating"]'); - await expect(ratingSection).toBeVisible(); - const stars = ratingSection.locator('[class*="stars"]'); - await expect(stars).toBeVisible(); - - // Check price - const price = page.locator('text=/\\$\\d+\\.\\d{2}/').first(); - await expect(price).toBeVisible(); - - // Check description - const descriptionHeading = page.locator('h2:has-text("Description")'); - await expect(descriptionHeading).toBeVisible(); - const description = page.locator('p').filter({ hasText: /high-quality/ }); - await expect(description).toBeVisible(); - - // Check product information section - const productInfoHeading = page.locator('h3:has-text("Product Information")'); - await expect(productInfoHeading).toBeVisible(); - - // Check product ID - const productId = page.locator('text=/prod-\\d+/'); - await expect(productId).toBeVisible(); - - // Check availability - target the dd element in product info section to avoid matching the badge - const availability = page.locator('dd').filter({ hasText: /In Stock|Out of Stock/ }); - await expect(availability).toBeVisible(); - }); - - test('should show action buttons for in-stock products', async ({ page }) => { - // Navigate to products page - await page.goto('/products'); - - // Find an in-stock product (one without "Out of Stock" badge) - const inStockProduct = page.locator('[class*="product-card"]').filter({ hasNot: page.locator('text="Out of Stock"') }).first(); - await inStockProduct.click(); - - // Wait for product detail page - await page.waitForURL('**/products/**'); - - // Check for Add to Cart button - const addToCartButton = page.locator('button:has-text("Add to Cart")'); - await expect(addToCartButton).toBeVisible(); - await expect(addToCartButton).toBeEnabled(); - - // Check for Add to Wishlist button - const addToWishlistButton = page.locator('button:has-text("Add to Wishlist")'); - await expect(addToWishlistButton).toBeVisible(); - await expect(addToWishlistButton).toBeEnabled(); - - // Test clicking Add to Cart (should show alert) - page.on('dialog', dialog => { - expect(dialog.message()).toContain('Product added to cart'); - dialog.accept(); - }); - await addToCartButton.click(); - }); - - test('should show disabled button for out-of-stock products', async ({ page }) => { - // Navigate to products page - await page.goto('/products'); - - // Find an out-of-stock product. The dataset of 50 products marks ~20% as - // out of stock, so at least one is always present. - const outOfStockProduct = page.locator('[class*="product-card"]').filter({ has: page.locator('text="Out of Stock"') }).first(); - await expect(outOfStockProduct).toBeVisible(); - - await outOfStockProduct.click(); - - // Wait for product detail page - await page.waitForURL('**/products/**'); - - // Check for out of stock badge on detail page - use more specific selector to avoid strict mode violation - const outOfStockBadge = page.locator('.out-of-stock-badge').filter({ hasText: 'Out of Stock' }); - await expect(outOfStockBadge).toBeVisible(); - - // Check for disabled button - const unavailableButton = page.locator('button:has-text("Currently Unavailable")'); - await expect(unavailableButton).toBeVisible(); - await expect(unavailableButton).toBeDisabled(); - - // Should not have Add to Cart or Wishlist buttons - const addToCartButton = page.locator('button:has-text("Add to Cart")'); - await expect(addToCartButton).toBeHidden(); - }); - - test('should navigate back to products list', async ({ page }) => { - // Navigate to a product detail page - await page.goto('/products/prod-1'); - await page.waitForLoadState('domcontentloaded'); - - // Click the back link - const backLink = page.locator('a:has-text("Back to Products")'); - await backLink.click(); - - // Should navigate back to products page - await page.waitForURL('**/products'); - expect(page.url()).toContain('/products'); - - // Products grid should be visible - const productsGrid = page.locator('[class*="product-grid"], [class*="products"]'); - await expect(productsGrid).toBeVisible(); - }); -}); \ No newline at end of file diff --git a/apps/shop-e2e/src/product-listing.spec.ts b/apps/shop-e2e/src/product-listing.spec.ts deleted file mode 100644 index 85fc63a..0000000 --- a/apps/shop-e2e/src/product-listing.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Product Listing Page', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/products'); - await page.waitForLoadState('domcontentloaded'); - }); - - test('should display products grid with at least one product', async ({ page }) => { - // Check that products are displayed - const productCards = page.locator('[class*="product-card"]'); - const count = await productCards.count(); - expect(count).toBeGreaterThan(0); - - // Check first product has required elements - const firstProduct = productCards.first(); - await expect(firstProduct).toBeVisible(); - - // Check for product image - const productImage = firstProduct.locator('img'); - await expect(productImage).toBeVisible(); - - // Check for product name (heading level 3) - const productName = firstProduct.locator('h3'); - await expect(productName).toBeVisible(); - - // Check for price - const productPrice = firstProduct.locator('text=/\\$\\d+\\.\\d{2}/'); - await expect(productPrice).toBeVisible(); - }); - - test('should filter products by category', async ({ page }) => { - // Select a specific category from dropdown - const categoryDropdown = page.locator('select'); - await categoryDropdown.selectOption('Electronics'); - - // Wait for products to load - await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0); - - // Verify all visible products are from Electronics category - const productCategories = page.locator('[class*="product-card"] p:first-of-type'); - const count = await productCategories.count(); - - for (let i = 0; i < count; i++) { - const category = productCategories.nth(i); - await expect(category).toHaveText('Electronics'); - } - }); - - test('should filter products by search term', async ({ page }) => { - // Enter search term - const searchInput = page.locator('input[placeholder*="Search"]'); - await searchInput.fill('Product 1'); - - // Wait for debounce and results - await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0); - - // Check that filtered products contain the search term - const productNames = page.locator('[class*="product-card"] h3'); - const count = await productNames.count(); - - for (let i = 0; i < count; i++) { - const name = await productNames.nth(i).textContent(); - expect(name?.toLowerCase()).toContain('product 1'); - } - }); - - test('should filter by in-stock products only', async ({ page }) => { - // Get initial product count - const resultsInfo = page.locator('text=/Showing \\d+ of \\d+ products/'); - const initialText = await resultsInfo.textContent(); - - // Click the "In Stock Only" checkbox - const inStockCheckbox = page.locator('input[type="checkbox"]'); - await inStockCheckbox.check(); - - // Wait for the results to update (the total count should change) - await page.waitForFunction( - (initialText) => { - const resultsElement = document.querySelector('[class*="results-info"]'); - return resultsElement && resultsElement.textContent !== initialText; - }, - initialText, - { timeout: 5000 } - ); - - // Verify no "Out of Stock" badges are visible (web-first wait for the - // filtered DOM to settle) - const outOfStockBadges = page.locator('text="Out of Stock"'); - await expect(outOfStockBadges).toHaveCount(0); - }); - - test('should handle pagination', async ({ page }) => { - // Check that pagination controls exist - const paginationSection = page.locator('[class*="pagination"]'); - await expect(paginationSection).toBeVisible(); - - // Check initial state - Previous should be disabled, Next should be enabled - const prevButton = page.locator('button:has-text("Previous")'); - const nextButton = page.locator('button:has-text("Next")'); - const pageInfo = page.locator('text=/Page \\d+ of \\d+/'); - - await expect(prevButton).toBeDisabled(); - await expect(nextButton).toBeEnabled(); - await expect(pageInfo).toBeVisible(); - - // Click next button - await nextButton.click(); - - // Wait for new products to load - await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0); - - // Verify page changed - const newPageInfo = await pageInfo.textContent(); - expect(newPageInfo).toContain('Page 2'); - - // Now Previous should be enabled - await expect(prevButton).toBeEnabled(); - }); - - test('should show correct product count', async ({ page }) => { - // Check the results info text - const resultsInfo = page.locator('text=/Showing \\d+ of \\d+ products/'); - await expect(resultsInfo).toBeVisible(); - - // Get the count from the text. The visibility assertion above guarantees - // the "Showing X of Y" text is present, so the digits are deterministic. - const text = String(await resultsInfo.textContent()); - const numbers = Array.from(text.matchAll(/\d+/g), (m) => m[0]); - expect(numbers).toHaveLength(2); - - const [showingText, totalText] = numbers; - const showing = parseInt(showingText); - const total = parseInt(totalText); - - // Verify we're showing reasonable numbers - expect(showing).toBeGreaterThan(0); - expect(showing).toBeLessThanOrEqual(12); // Default page size - expect(total).toBeGreaterThan(0); - - // Count actual product cards - const productCards = page.locator('[class*="product-card"]'); - const actualCount = await productCards.count(); - expect(actualCount).toBe(showing); - }); -}); \ No newline at end of file diff --git a/apps/shop-e2e/src/shop-homepage.spec.ts b/apps/shop-e2e/src/shop-homepage.spec.ts deleted file mode 100644 index 4d5e55f..0000000 --- a/apps/shop-e2e/src/shop-homepage.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Shop Homepage', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should display the main header and navigation', async ({ page }) => { - // Check header title - const header = page.locator('h1').first(); - await expect(header).toContainText('Nx Shop Demo'); - - // Check navigation link exists - const productsLink = page.locator('nav a:has-text("Products")'); - await expect(productsLink).toBeVisible(); - await expect(productsLink).toHaveAttribute('href', '/products'); - }); - - test('should display footer with copyright information', async ({ page }) => { - // Check footer content - const footer = page.locator('footer'); - await expect(footer).toContainText('© 2025 Nx Shop Demo'); - await expect(footer).toContainText( - 'Frontend (Angular) + Backend (Express) + Shared Libraries' - ); - }); - - test('should redirect to products page by default', async ({ page }) => { - // The app should redirect from / to /products - await page.waitForURL('**/products'); - expect(page.url()).toContain('/products'); - - // Verify we're on the products page - const productsHeading = page.locator('h1:has-text("Our Products")'); - await expect(productsHeading).toBeVisible(); - }); - - test('should have proper page title and meta', async ({ page }) => { - // Check page title - await expect(page).toHaveTitle(/shop/i); - - // Check viewport is responsive - const viewport = page.viewportSize(); - expect(viewport).toBeTruthy(); - }); -}); diff --git a/apps/shop-e2e/tsconfig.json b/apps/shop-e2e/tsconfig.json deleted file mode 100644 index 329b993..0000000 --- a/apps/shop-e2e/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "allowJs": true, - "outDir": "../../dist/out-tsc", - "sourceMap": false, - "module": "commonjs", - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "types": ["node"] - }, - "include": [ - "**/*.ts", - "**/*.js", - "playwright.config.ts", - "src/**/*.spec.ts", - "src/**/*.spec.js", - "src/**/*.test.ts", - "src/**/*.test.js", - "src/**/*.d.ts" - ] -} diff --git a/apps/shop/eslint.config.mjs b/apps/shop/eslint.config.mjs deleted file mode 100644 index 9b3aa71..0000000 --- a/apps/shop/eslint.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import nx from '@nx/eslint-plugin'; -import baseConfig from '../../eslint.config.mjs'; - -export default [ - ...baseConfig, - ...nx.configs['flat/angular'], - ...nx.configs['flat/angular-template'], - { - files: ['**/*.ts'], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: 'app', - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: 'app', - style: 'kebab-case', - }, - ], - }, - }, - { - files: ['**/*.html'], - // Override or add rules here - rules: {}, - }, -]; diff --git a/apps/shop/project.json b/apps/shop/project.json deleted file mode 100644 index 1cba41e..0000000 --- a/apps/shop/project.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "name": "shop", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "application", - "prefix": "app", - "sourceRoot": "apps/shop/src", - "tags": ["scope:shop"], - "targets": { - "build": { - "executor": "@angular/build:application", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/apps/shop", - "browser": "apps/shop/src/main.ts", - "polyfills": ["zone.js"], - "tsConfig": "apps/shop/tsconfig.app.json", - "assets": [ - { - "glob": "**/*", - "input": "apps/shop/public" - } - ], - "styles": ["apps/shop/src/styles.css"], - "server": "apps/shop/src/main.server.ts", - "ssr": { - "entry": "apps/shop/src/server.ts" - }, - "outputMode": "server" - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kb", - "maximumError": "1mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "4kb", - "maximumError": "8kb" - } - ], - "outputHashing": "all" - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "dependsOn": ["api:serve"], - "continuous": true, - "executor": "@angular/build:dev-server", - "configurations": { - "production": { - "buildTarget": "shop:build:production" - }, - "development": { - "buildTarget": "shop:build:development" - } - }, - "defaultConfiguration": "development", - "options": { - "proxyConfig": "apps/shop/proxy.conf.json" - } - }, - "extract-i18n": { - "executor": "@angular/build:extract-i18n", - "options": { - "buildTarget": "shop:build" - } - }, - "serve-static": { - "continuous": true, - "executor": "@nx/web:file-server", - "options": { - "buildTarget": "shop:build", - "port": 4200, - "staticFilePath": "dist/apps/shop/browser", - "spa": true - } - } - } -} diff --git a/apps/shop/proxy.conf.json b/apps/shop/proxy.conf.json deleted file mode 100644 index 870e10f..0000000 --- a/apps/shop/proxy.conf.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "/@org/api": { - "target": "http://localhost:3333", - "secure": false - } -} diff --git a/apps/shop/public/favicon.ico b/apps/shop/public/favicon.ico deleted file mode 100644 index 317ebcb..0000000 Binary files a/apps/shop/public/favicon.ico and /dev/null differ diff --git a/apps/shop/src/app/app.config.server.ts b/apps/shop/src/app/app.config.server.ts deleted file mode 100644 index 608887d..0000000 --- a/apps/shop/src/app/app.config.server.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { appConfig } from './app.config'; -import { serverRoutes } from './app.routes.server'; - -const serverConfig: ApplicationConfig = { - providers: [provideServerRendering(withRoutes(serverRoutes))], -}; - -export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/apps/shop/src/app/app.config.ts b/apps/shop/src/app/app.config.ts deleted file mode 100644 index 8a1da78..0000000 --- a/apps/shop/src/app/app.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - ApplicationConfig, - provideBrowserGlobalErrorListeners, - provideZoneChangeDetection, -} from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { provideHttpClient, withFetch } from '@angular/common/http'; -import { appRoutes } from './app.routes'; -import { - provideClientHydration, - withEventReplay, -} from '@angular/platform-browser'; - -export const appConfig: ApplicationConfig = { - providers: [ - provideClientHydration(withEventReplay()), - provideBrowserGlobalErrorListeners(), - provideZoneChangeDetection({ eventCoalescing: true }), - provideRouter(appRoutes), - provideHttpClient(withFetch()), - ], -}; diff --git a/apps/shop/src/app/app.css b/apps/shop/src/app/app.css deleted file mode 100644 index e3bea50..0000000 --- a/apps/shop/src/app/app.css +++ /dev/null @@ -1,66 +0,0 @@ -.app-header { - background: #2c3e50; - color: white; - padding: 1rem 0; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.app-header .container { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px; - display: flex; - justify-content: space-between; - align-items: center; -} - -.app-header h1 { - margin: 0; - font-size: 1.5rem; - font-weight: 600; -} - -.app-header nav { - display: flex; - gap: 24px; -} - -.app-header nav a { - color: white; - text-decoration: none; - font-size: 1rem; - padding: 8px 16px; - border-radius: 4px; - transition: background 0.3s; -} - -.app-header nav a:hover { - background: rgba(255, 255, 255, 0.1); -} - -.app-header nav a.active { - background: #3498db; -} - -.app-main { - min-height: calc(100vh - 140px); - background: #f5f5f5; -} - -.app-footer { - background: #34495e; - color: white; - padding: 24px 0; - text-align: center; -} - -.app-footer .container { - max-width: 1200px; - margin: 0 auto; - padding: 0 24px; -} - -.app-footer p { - margin: 8px 0; - font-size: 0.9rem; -} \ No newline at end of file diff --git a/apps/shop/src/app/app.html b/apps/shop/src/app/app.html deleted file mode 100644 index 3eeb9a6..0000000 --- a/apps/shop/src/app/app.html +++ /dev/null @@ -1,19 +0,0 @@ -
-
-

{{ title }}

- -
-
- -
- -
- -
-
-

© 2025 Nx Shop Demo - Showcasing Nx Monorepo Features

-

Frontend (Angular) + Backend (Express) + Shared Libraries

-
-
diff --git a/apps/shop/src/app/app.routes.server.ts b/apps/shop/src/app/app.routes.server.ts deleted file mode 100644 index e737650..0000000 --- a/apps/shop/src/app/app.routes.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RenderMode, ServerRoute } from '@angular/ssr'; - -export const serverRoutes: ServerRoute[] = [ - { - path: 'products/:id', - renderMode: RenderMode.Server, - }, - { - path: '**', - renderMode: RenderMode.Server, - }, -]; diff --git a/apps/shop/src/app/app.routes.ts b/apps/shop/src/app/app.routes.ts deleted file mode 100644 index cac7b87..0000000 --- a/apps/shop/src/app/app.routes.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Route } from '@angular/router'; - -export const appRoutes: Route[] = [ - { - path: '', - redirectTo: 'products', - pathMatch: 'full', - }, - { - path: 'products', - loadChildren: () => - import('@org/shop/feature-products').then(m => m.featureProductsRoutes), - }, - { - path: 'products', - loadChildren: () => - import('@org/shop/feature-product-detail').then( - m => m.featureProductDetailRoutes - ), - }, - { - path: '**', - redirectTo: 'products', - }, -]; diff --git a/apps/shop/src/app/app.spec.ts b/apps/shop/src/app/app.spec.ts deleted file mode 100644 index d4b1ae8..0000000 --- a/apps/shop/src/app/app.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { App } from './app'; -import { appRoutes } from './app.routes'; -import { provideRouter } from '@angular/router'; -import { By } from '@angular/platform-browser'; - -describe('App', () => { - let component: App; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [App], - providers: [provideRouter(appRoutes)], - }).compileComponents(); - - fixture = TestBed.createComponent(App); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create the app component', () => { - expect(component).toBeTruthy(); - }); - - it('should render title in header', () => { - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Nx Shop Demo'); - }); - - it('should render navigation links', () => { - const navLinks = fixture.debugElement.queryAll(By.css('nav a')); - expect(navLinks.length).toBeGreaterThan(0); - expect(navLinks[0].nativeElement.textContent).toContain('Products'); - expect(navLinks[0].nativeElement.getAttribute('routerLink')).toBe( - '/products' - ); - }); - - it('should render footer with correct copyright', () => { - const footer = fixture.nativeElement.querySelector('.app-footer'); - expect(footer).toBeTruthy(); - expect(footer?.textContent).toContain('© 2025 Nx Shop Demo'); - expect(footer?.textContent).toContain( - 'Frontend (Angular) + Backend (Express) + Shared Libraries' - ); - }); - - it('should have router outlet for dynamic content', () => { - const routerOutlet = fixture.nativeElement.querySelector('router-outlet'); - expect(routerOutlet).toBeTruthy(); - }); - - it('should apply change detection strategy OnPush', () => { - const metadata = (App as unknown as { ɵcmp: { onPush: boolean } })['ɵcmp']; - expect(metadata.onPush).toBeTruthy(); - }); -}); diff --git a/apps/shop/src/app/app.ts b/apps/shop/src/app/app.ts deleted file mode 100644 index a093f9b..0000000 --- a/apps/shop/src/app/app.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@Component({ - imports: [RouterModule], - selector: 'app-root', - templateUrl: './app.html', - styleUrl: './app.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class App { - protected title = 'Nx Shop Demo'; -} diff --git a/apps/shop/src/index.html b/apps/shop/src/index.html deleted file mode 100644 index 8bac6f7..0000000 --- a/apps/shop/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - shop - - - - - - - - diff --git a/apps/shop/src/main.server.ts b/apps/shop/src/main.server.ts deleted file mode 100644 index c4639a1..0000000 --- a/apps/shop/src/main.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; -import { App } from './app/app'; -import { config } from './app/app.config.server'; - -const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); - -export default bootstrap; diff --git a/apps/shop/src/main.ts b/apps/shop/src/main.ts deleted file mode 100644 index 190f341..0000000 --- a/apps/shop/src/main.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; - -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); diff --git a/apps/shop/src/server.ts b/apps/shop/src/server.ts deleted file mode 100644 index cc21bbd..0000000 --- a/apps/shop/src/server.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - AngularNodeAppEngine, - createNodeRequestHandler, - isMainModule, - writeResponseToNodeResponse, -} from '@angular/ssr/node'; -import express from 'express'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const serverDistFolder = dirname(fileURLToPath(import.meta.url)); -const browserDistFolder = resolve(serverDistFolder, '../browser'); - -const app = express(); -const angularApp = new AngularNodeAppEngine(); - -/** - * Example Express Rest API endpoints can be defined here. - * Uncomment and define endpoints as necessary. - * - * Example: - * ```ts - * app.get('/api/**', (req, res) => { - * // Handle API request - * }); - * ``` - */ - -/** - * Serve static files from /browser - */ -app.use( - express.static(browserDistFolder, { - maxAge: '1y', - index: false, - redirect: false, - }) -); - -/** - * Handle all other requests by rendering the Angular application. - */ -app.use('/**', (req, res, next) => { - angularApp - .handle(req) - .then((response) => - response ? writeResponseToNodeResponse(response, res) : next() - ) - .catch(next); -}); - -/** - * Start the server if this module is the main entry point. - * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000. - */ -if (isMainModule(import.meta.url)) { - const port = process.env['PORT'] || 4000; - app.listen(port, () => { - console.log(`Node Express server listening on http://localhost:${port}`); - }); -} - -/** - * Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions. - */ -export const reqHandler = createNodeRequestHandler(app); diff --git a/apps/shop/src/styles.css b/apps/shop/src/styles.css deleted file mode 100644 index f91d459..0000000 --- a/apps/shop/src/styles.css +++ /dev/null @@ -1,38 +0,0 @@ -/* Global Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, - Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - line-height: 1.6; - color: #333; - background: #f5f5f5; -} - -a { - color: #3498db; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -button { - font-family: inherit; -} - -input, -select, -textarea { - font-family: inherit; -} - -img { - max-width: 100%; - height: auto; -} \ No newline at end of file diff --git a/apps/shop/src/test-setup.ts b/apps/shop/src/test-setup.ts deleted file mode 100644 index 9157d5f..0000000 --- a/apps/shop/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import '@angular/compiler'; -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() -); diff --git a/apps/shop/tsconfig.app.json b/apps/shop/tsconfig.app.json deleted file mode 100644 index cc39a5e..0000000 --- a/apps/shop/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": [ - "jest.config.ts", - "src/test-setup.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/test-setup.ts" - ] -} diff --git a/apps/shop/tsconfig.spec.json b/apps/shop/tsconfig.spec.json deleted file mode 100644 index 19ef165..0000000 --- a/apps/shop/tsconfig.spec.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ] - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ], - "files": ["src/test-setup.ts"] -} diff --git a/apps/shop/vite.config.mts b/apps/shop/vite.config.mts deleted file mode 100644 index 93403e2..0000000 --- a/apps/shop/vite.config.mts +++ /dev/null @@ -1,28 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import angular from '@analogjs/vite-plugin-angular'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../node_modules/.vite/apps/shop', - plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'shop', - watch: false, - globals: true, - environment: 'jsdom', - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['src/test-setup.ts'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../coverage/apps/shop', - provider: 'v8' as const, - }, - }, -})); diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..9e3d1db --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,6 @@ +import type { Config } from 'jest'; +import { getJestProjectsAsync } from '@nx/jest'; + +export default async (): Promise => ({ + projects: await getJestProjectsAsync() +}); \ No newline at end of file diff --git a/jest.preset.js b/jest.preset.js new file mode 100644 index 0000000..da8e8c2 --- /dev/null +++ b/jest.preset.js @@ -0,0 +1,3 @@ +const nxPreset = require('@nx/jest/preset').default; + +module.exports = { ...nxPreset }; \ No newline at end of file diff --git a/packages/shop/data/README.md b/libs/ui/README.md similarity index 59% rename from packages/shop/data/README.md rename to libs/ui/README.md index 44225fe..139ffca 100644 --- a/packages/shop/data/README.md +++ b/libs/ui/README.md @@ -1,7 +1,7 @@ -# data +# ui This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test data` to execute the unit tests. +Run `nx test ui` to execute the unit tests. diff --git a/libs/ui/eslint.config.mjs b/libs/ui/eslint.config.mjs new file mode 100644 index 0000000..52e7a6b --- /dev/null +++ b/libs/ui/eslint.config.mjs @@ -0,0 +1,56 @@ +import nx from "@nx/eslint-plugin"; +import baseConfig from "../../eslint.config.mjs"; + +export default [ + ...nx.configs["flat/angular"], + ...nx.configs["flat/angular-template"], + ...baseConfig, + { + files: [ + "**/*.json" + ], + rules: { + "@nx/dependency-checks": [ + "error", + { + ignoredFiles: [ + "{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}" + ] + } + ] + }, + languageOptions: { + parser: await import("jsonc-eslint-parser") + } + }, + { + files: [ + "**/*.ts" + ], + rules: { + "@angular-eslint/directive-selector": [ + "error", + { + type: "attribute", + prefix: "pi", + style: "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + type: "element", + prefix: "pi", + style: "kebab-case" + } + ] + } + }, + { + files: [ + "**/*.html" + ], + // Override or add rules here + rules: {} + } +]; diff --git a/libs/ui/jest.config.cts b/libs/ui/jest.config.cts new file mode 100644 index 0000000..34233b3 --- /dev/null +++ b/libs/ui/jest.config.cts @@ -0,0 +1,21 @@ +module.exports = { + displayName: 'ui', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../coverage/libs/ui', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ] +}; diff --git a/libs/ui/ng-package.json b/libs/ui/ng-package.json new file mode 100644 index 0000000..db0c4c0 --- /dev/null +++ b/libs/ui/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/libs/ui", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/ui/package.json b/libs/ui/package.json new file mode 100644 index 0000000..7dad134 --- /dev/null +++ b/libs/ui/package.json @@ -0,0 +1,9 @@ +{ + "name": "@pi/ui", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^22.0.0", + "@angular/core": "^22.0.0" + }, + "sideEffects": false +} diff --git a/libs/ui/project.json b/libs/ui/project.json new file mode 100644 index 0000000..4852c7b --- /dev/null +++ b/libs/ui/project.json @@ -0,0 +1,40 @@ +{ + "name": "ui", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/ui/src", + "prefix": "pi", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nx/angular:ng-packagr-lite", + "outputs": [ + "{workspaceRoot}/dist/{projectRoot}" + ], + "defaultConfiguration": "production", + "options": { + "project": "libs/ui/ng-package.json", + "tsConfig": "libs/ui/tsconfig.lib.json" + }, + "configurations": { + "production": { + "tsConfig": "libs/ui/tsconfig.lib.prod.json" + }, + "development": {} + } + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "libs/ui/jest.config.cts", + "tsConfig": "libs/ui/tsconfig.spec.json" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts new file mode 100644 index 0000000..c8ee443 --- /dev/null +++ b/libs/ui/src/index.ts @@ -0,0 +1,61 @@ +// Barrel público de @pi/ui — generado durante la migración. + +export * from './lib/styles'; + +export * from './lib/util/affix'; +export * from './lib/util/amplitude'; +export * from './lib/util/color'; +export * from './lib/util/common'; +export * from './lib/util/dato-cms'; +export * from './lib/util/device-configs'; +export * from './lib/util/environments'; +export * from './lib/util/form-control'; +export * from './lib/util/icon-registry'; +export * from './lib/util/infinite-scroll'; +// ./lib/util/list-configs: omitido(s) por colisión con otro módulo -> featureKey, getFeatureState, initialState +export { ListConfigsFacade, ListConfigsState, ListMode, ListModeActionsType, getListMode, listConfigsReducer, resetListMode, setListMode } from './lib/util/list-configs'; +export * from './lib/util/loaders'; +export * from './lib/util/local-storage'; +export * from './lib/util/menu'; +export * from './lib/util/models'; +export * from './lib/util/pipes'; +export * from './lib/util/router-store'; +export * from './lib/util/schema'; +export * from './lib/util/scroll-to'; +export * from './lib/util/status'; +export * from './lib/util/translate'; +export * from './lib/util/validator'; +export * from './lib/util/value'; +export * from './lib/util/view-title'; + +export * from './lib/ui/accordion'; +export * from './lib/ui/banner'; +export * from './lib/ui/button'; +export * from './lib/ui/card'; +export * from './lib/ui/carousel'; +export * from './lib/ui/chart'; +export * from './lib/ui/chip'; +export * from './lib/ui/data-grid'; +export * from './lib/ui/data-table'; +export * from './lib/ui/divider'; +export * from './lib/ui/error-status-code'; +export * from './lib/ui/form-field'; +export * from './lib/ui/icon'; +export * from './lib/ui/image'; +export * from './lib/ui/list-item'; +export * from './lib/ui/modal'; +export * from './lib/ui/note'; +export * from './lib/ui/notification'; +export * from './lib/ui/paginator'; +export * from './lib/ui/panel'; +export * from './lib/ui/progress'; +export * from './lib/ui/select'; +// ./lib/ui/slide-toggle: omitido(s) por colisión con otro módulo -> PiSlideToggleComponent +export { PiSharedUISlideToggleModule } from './lib/ui/slide-toggle'; +export * from './lib/ui/slider'; +export * from './lib/ui/spinner'; +export * from './lib/ui/tab'; +// ./lib/ui/text: omitido(s) por colisión con otro módulo -> PiDisabledDirective +export { PiBaseTextViewerComponent, PiSharedUITextModule, PiTextAreaExpansionComponent, PiTextCenteredDirective, PiTextColorDirective, PiTextCoveredDirective, PiTextGroupDirective, PiTextGroupIndicatorDirective, PiTextLargeDirective, PiTextLetterSpacingDirective, PiTextMediumDirective, PiTextSizeDirective, PiTextSmallDirective, PiTextViewerComponent, PiTextViewerLinkComponent, PiTextWeightBlackDirective, PiTextWeightBoldDirective, PiTextWeightLightDirective, PiTextWeightMediumDirective, PiTextWeightNormalDirective, PiTextXLargeDirective, PiTextXSmallDirective, PiTextXXLargeDirective, PiTextXXSmallDirective, PiTextXXXLargeDirective } from './lib/ui/text'; +export * from './lib/ui/tooltip'; +export * from './lib/ui/wrapper'; diff --git a/libs/ui/src/lib/styles/animations.ts b/libs/ui/src/lib/styles/animations.ts new file mode 100644 index 0000000..13affb2 --- /dev/null +++ b/libs/ui/src/lib/styles/animations.ts @@ -0,0 +1,46 @@ +import { animate, state, style, transition, trigger } from '@angular/animations'; +import { DURATION } from './configs'; + +export function transitionAnimation(duration: DURATION): any { + return [ + trigger('collapse', [ + state( + 'off', + style({ transform: 'translateY(0%)', opacity: '0', visibility: 'hidden', height: '0' }) + ), + state( + 'on', + style({ transform: 'translateY(0%)', opacity: '1', visibility: 'visible', height: '100%' }) + ), + transition('on <=> off', [animate('420ms ease-in-out')]), + ]), + ]; +} + +export const enterTransition = transition(':enter', [ + style({ + opacity: 0, + }), + animate( + '240ms 200ms ease-out', + style({ + opacity: 1, + }) + ), +]); + +export const leaveTransition = transition(':leave', [ + style({ + opacity: 1, + }), + animate( + '180ms 120ms ease-out', + style({ + opacity: 0, + }) + ), +]); + +export const fadeIn = trigger('fadeIn', [enterTransition]); + +export const fadeOut = trigger('fadeOut', [leaveTransition]); diff --git a/libs/ui/src/lib/styles/common/abstracts/_pi-color-variables.scss b/libs/ui/src/lib/styles/common/abstracts/_pi-color-variables.scss new file mode 100644 index 0000000..112d91d --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/_pi-color-variables.scss @@ -0,0 +1,342 @@ +// Primary +$pi-color-theme-primary: ( + 50: #f0f4ff, + 100: #d7e2fb, + 200: #c3d5ff, + 300: #a1bbf5, + 400: #8cacf2, + 500: #799ef0, + 600: #7196ee, + 700: #668cec, + 800: #5c82e9, + 900: #5772d4, + A100: #ffffff, + A200: #ffffff, + A400: #d6e0ff, + A700: #bdccff, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #000000, + 400: #000000, + 500: #000000, + 600: #000000, + 700: #000000, + 800: #000000, + 900: #000000, + A100: #000000, + A200: #000000, + A400: #000000, + A700: #000000, + ), +); + +// Accent +$pi-color-theme-accent: ( + 50: #fdf5e5, + 100: #fce0ad, + 200: #f6d393, + 300: #f6bc63, + 400: #f0b54a, + 500: #eda82a, + 600: #eba025, + 700: #d78f0f, + 800: #e58d19, + 900: #e07d0f, + A100: #ffffff, + A200: #ffecda, + A400: #ffd2a7, + A700: #ffc48d, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #000000, + 400: #000000, + 500: #000000, + 600: #000000, + 700: #000000, + 800: #000000, + 900: #000000, + A100: #000000, + A200: #000000, + A400: #000000, + A700: #000000, + ), +); + +// Warn +$pi-color-theme-warn: ( + 50: #fbe1e1, + 100: #f4b3b3, + 200: #ef8c8c, + 300: #e54e4e, + 400: #e02828, + 500: #da0202, + 600: #d60202, + 700: #d00101, + 800: #cb0101, + 900: #ba1a1a, + A100: #ffebeb, + A200: #ffb8b8, + A400: #ff8585, + A700: #ff6b6b, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #000000, + A400: #000000, + A700: #000000, + ), +); +// Highlighted +$pi-color-theme-highlighted: ( + 50: #e2e5ed, + 100: #b6bfd3, + 200: #8695b5, + 300: #566a97, + 400: #314a81, + 500: #0d2a6b, + 600: #0b2563, + 700: #091f58, + 800: #07194e, + 900: #030f3c, + A100: #7285ff, + A200: #3f59ff, + A400: #0c2dff, + A700: #0021f1, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, + ), +); + +$md-mcgpalette0: ( + 50: #e1e8f5, + 100: #b5c6e6, + 200: #83a1d5, + 300: #517bc4, + 400: #2c5eb7, + 500: #0d2a6b, + 600: #0b2563, + 700: #091f58, + 800: #07194e, + 900: #030f3c, + A100: #7285ff, + A200: #3f59ff, + A400: #0c2dff, + A700: #0021f1, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #ffffff, + 400: #ffffff, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #ffffff, + A400: #ffffff, + A700: #ffffff, + ), +); + +$pi-color-primary-10: rgba(253, 253, 255, 1); // #fdfdff +$pi-color-primary-25: rgba(250, 251, 255, 1); // #fafbff +$pi-color-primary-50: rgba(240, 244, 255, 1); // #f0f4ff +$pi-color-primary-75: rgba(234, 239, 255, 1); // #eaefff +$pi-color-primary-100: rgba(215, 226, 251, 1); // #d7e2fb +$pi-color-primary-200-rgba: rgba(196, 213, 255, 0.15); //#c4d5ff +$pi-color-primary-200: rgba(195, 213, 255, 1); //#c3d5ff +$pi-color-primary-300: rgba(163, 192, 255, 1); //#A3C0FF +$pi-color-primary-400: rgba(140, 172, 242, 1); //#8CACF2 +$pi-color-primary: rgba(121, 158, 240, 1); // #799ef0 +$pi-color-primary-900: rgba(87, 114, 212, 1); // #5772D4 +// Accent +$pi-color-accent-10: rgba(255, 253, 249, 1); // #fffdf9 +$pi-color-accent-25: rgba(255, 250, 237, 1); // #fffaed +$pi-color-accent-50: rgba(253, 245, 229, 1); // #fdf5e5 +$pi-color-accent-75: rgba(250, 229, 191, 1); // #fae5bf +$pi-color-accent-100: rgba(252, 224, 173, 1); // #fce0ad +$pi-color-accent-200-rgba: rgba(252, 224, 173, 0.15); // #FCE0AD +$pi-color-accent-200: rgba(246, 212, 149, 1); // #f6d495 +$pi-color-accent-300: rgba(242, 194, 106, 1); // #f2c26a +$pi-color-accent-400: rgba(246, 188, 99, 1); // #f6bc63 +$pi-color-accent: rgba(237, 168, 42, 1); // #eda82a +$pi-color-accent-900: rgba(215, 143, 15, 1); // #d78f0f +// Highlighted +$pi-color-highlighted-10: rgba(249, 251, 255, 1); // #f9fbff +$pi-color-highlighted-25: rgba(242, 245, 255, 1); // #f2f5ff +$pi-color-highlighted-50: rgba(233, 238, 255, 1); // #E9EEFF +$pi-color-highlighted-75: rgba(218, 226, 255, 1); // #dae2ff +$pi-color-highlighted-100: rgba(188, 205, 250, 1); // #bccdfa +$pi-color-highlighted-200: rgba(100, 130, 215, 1); // #6482d7 +$pi-color-highlighted-200-rgba: rgba(168, 193, 255, 0.15); // #A8C1FF +$pi-color-highlighted-300: rgba(67, 106, 193, 1); // #436ac1 +$pi-color-highlighted-400: rgba(29, 63, 138, 1); // #1d3f8a +$pi-color-highlighted: rgba(13, 42, 107, 1); // #0d2a6b +$pi-color-highlighted-900: rgba(3, 15, 60, 1); // #030f3c +// Warn +$pi-color-warn-10: rgba(255, 249, 250, 1); // #fff9fa +$pi-color-warn-25: rgba(255, 237, 240, 1); // #ffedf0 +$pi-color-warn-50: rgba(252, 235, 235, 1); // #fcebeb +$pi-color-warn-75: rgba(255, 231, 234, 1); // #ffe7ea +$pi-color-warn-100: rgba(247, 205, 205, 1); // #f7cdcd +$pi-color-warn-200: rgba(242, 172, 172, 1); // #f2acac +$pi-color-warn-200-rgba: rgba(237, 129, 129, 0.149); // #ED8181 #ed818126 +$pi-color-warn-300: rgba(236, 139, 139, 1); // #ec8b8b +$pi-color-warn-400: rgba(232, 114, 114, 1); // #e87272 +$pi-color-warn: rgba(228, 89, 89, 1); // #e45959 +$pi-color-warn-900: rgba(193, 55, 55, 1); // #c13737 +// Success +$pi-color-success-10: rgba(247, 252, 249, 1); // #f7fcf9 +$pi-color-success-25: rgba(230, 246, 237, 1); // #e6f6ed +$pi-color-success-50: rgba(222, 245, 232, 1); // #DEF5E8 +$pi-color-success-75: rgba(199, 235, 220, 1); // #c7ebdc +$pi-color-success-100: rgba(187, 232, 213, 1); // #bbe8d5 +$pi-color-success-200: rgba(131, 219, 182, 1); // #83dbb6 +$pi-color-success-300: rgba(96, 201, 157, 1); // #60c99d +$pi-color-success-400: rgba(62, 190, 136, 1); // #3ebe88 +$pi-color-success: rgba(28, 178, 115, 1); // #1cb273 +$pi-color-success-900: rgba(0, 151, 88, 1); // #009758 +// Grey +$pi-color-grey-10: rgba(252, 252, 253, 1); // #fcfcfd +$pi-color-grey-25: rgba(250, 250, 251, 1); // #fafafb +$pi-color-grey-50: rgba(243, 244, 244, 1); // #f3f4f4 +$pi-color-grey-75: rgba(240, 241, 243, 1); // #f0f1f3 +$pi-color-grey-100: rgba(233, 235, 241, 1); // #e9ebf1 +$pi-color-grey-200: rgba(209, 211, 218, 1); // #d1d3da +$pi-color-grey-300: rgba(178, 178, 184, 1); // #b2b2b8 +$pi-color-grey-400: rgba(137, 136, 143, 1); // #89888f +$pi-color-grey: rgba(106, 106, 112, 1); // #6a6a70 +$pi-color-grey-900: rgba(61, 61, 61, 1); // #3d3d3d +// White +$pi-color-white: rgba(255, 255, 255, 1); // #FFFFFF +$pi-color-white-900: rgba(244, 244, 244, 1); // #f4f4f4 +$pi-color-white-rgba-03: rgba(255, 255, 255, 0.33); // #ffffff rgba +$pi-color-white-rgba-05: rgba(255, 255, 255, 0.5); // #ffffff rgba +$pi-color-white-rgba-06: rgba(255, 255, 255, 0.66); // #ffffff rgba +$pi-color-white-rgba-09: rgba(255, 255, 255, 0.88); // #ffffff rgba +// Black +$pi-color-black-200: rgba(50, 55, 72, 1); // #323748 +$pi-color-black: rgba(26, 26, 26, 1); // #1A1A1A +// Background +$pi-color-background-300: rgba(174, 174, 182, 1); // #F4F4F4 +$pi-color-background-400: rgba(252, 252, 252, 1); // ##FCFCFC +$pi-color-background-500: rgba(247, 247, 247, 1); //#f7f7f7 + +// Root variables +:root { + // Brand colors + // Primary + --pi-color-primary-10: #{$pi-color-primary-10}; + --pi-color-primary-25: #{$pi-color-primary-25}; + --pi-color-primary-50: #{$pi-color-primary-50}; + --pi-color-primary-75: #{$pi-color-primary-75}; + --pi-color-primary-100: #{$pi-color-primary-100}; + --pi-color-primary-200: #{$pi-color-primary-200}; + --pi-color-primary-200-rgba: #{$pi-color-primary-200-rgba}; + --pi-color-primary-300: #{$pi-color-primary-300}; + --pi-color-primary-400: #{$pi-color-primary-400}; + --pi-color-primary: #{$pi-color-primary}; + --pi-color-primary-900: #{$pi-color-primary-900}; + // Accent + --pi-color-accent-10: #{$pi-color-accent-10}; + --pi-color-accent-25: #{$pi-color-accent-25}; + --pi-color-accent-50: #{$pi-color-accent-50}; + --pi-color-accent-75: #{$pi-color-accent-75}; + --pi-color-accent-100: #{$pi-color-accent-100}; + --pi-color-accent-200: #{$pi-color-accent-200}; + --pi-color-accent-200-rgba: #{$pi-color-accent-200-rgba}; + --pi-color-accent-300: #{$pi-color-accent-300}; + --pi-color-accent-400: #{$pi-color-accent-400}; + --pi-color-accent: #{$pi-color-accent}; + --pi-color-accent-900: #{$pi-color-accent-900}; + // Warn + --pi-color-highlighted-10: #{$pi-color-highlighted-10}; + --pi-color-highlighted-25: #{$pi-color-highlighted-25}; + --pi-color-highlighted-50: #{$pi-color-highlighted-50}; + --pi-color-highlighted-75: #{$pi-color-highlighted-75}; + --pi-color-highlighted-100: #{$pi-color-highlighted-100}; + --pi-color-highlighted-200: #{$pi-color-highlighted-200}; + --pi-color-highlighted-200-rgba: #{$pi-color-highlighted-200-rgba}; + --pi-color-highlighted-300: #{$pi-color-highlighted-300}; + --pi-color-highlighted-400: #{$pi-color-highlighted-400}; + --pi-color-highlighted: #{$pi-color-highlighted}; + --pi-color-highlighted-900: #{$pi-color-highlighted-900}; + // Warn + --pi-color-warn-10: #{$pi-color-warn-10}; + --pi-color-warn-25: #{$pi-color-warn-25}; + --pi-color-warn-50: #{$pi-color-warn-50}; + --pi-color-warn-75: #{$pi-color-warn-75}; + --pi-color-warn-100: #{$pi-color-warn-100}; + --pi-color-warn-200: #{$pi-color-warn-200}; + --pi-color-warn-200-rgba: #{$pi-color-warn-200-rgba}; + --pi-color-warn-300: #{$pi-color-warn-300}; + --pi-color-warn-400: #{$pi-color-warn-400}; + --pi-color-warn: #{$pi-color-warn}; + --pi-color-warn-900: #{$pi-color-warn-900}; + // Success + --pi-color-success-10: #{$pi-color-success-10}; + --pi-color-success-25: #{$pi-color-success-25}; + --pi-color-success-50: #{$pi-color-success-50}; + --pi-color-success-75: #{$pi-color-success-75}; + --pi-color-success-100: #{$pi-color-success-100}; + --pi-color-success-200: #{$pi-color-success-200}; + --pi-color-success-300: #{$pi-color-success-300}; + --pi-color-success-400: #{$pi-color-success-400}; + --pi-color-success: #{$pi-color-success}; + --pi-color-success-900: #{$pi-color-success-900}; + // White + --pi-color-white: #{$pi-color-white}; + --pi-color-white-900: #{$pi-color-white-900}; + --pi-color-white-rgba-03: #{$pi-color-white-rgba-03}; + --pi-color-white-rgba-05: #{$pi-color-white-rgba-05}; + --pi-color-white-rgba-06: #{$pi-color-white-rgba-06}; + --pi-color-white-rgba-09: #{$pi-color-white-rgba-09}; + // Grey + --pi-color-grey-10: #{$pi-color-grey-10}; + --pi-color-grey-25: #{$pi-color-grey-25}; + --pi-color-grey-50: #{$pi-color-grey-50}; + --pi-color-grey-75: #{$pi-color-grey-75}; + --pi-color-grey-100: #{$pi-color-grey-100}; + --pi-color-grey-200: #{$pi-color-grey-200}; + --pi-color-grey-200: #{$pi-color-grey-200}; + --pi-color-grey-300: #{$pi-color-grey-300}; + --pi-color-grey-400: #{$pi-color-grey-400}; + --pi-color-grey: #{$pi-color-grey}; + --pi-color-grey-900: #{$pi-color-grey-900}; + // Black + --pi-color-black-200: #{$pi-color-black-200}; + --pi-color-black: #{$pi-color-black}; + // Background + --pi-color-white: #{$pi-color-white}; + --pi-color-background-300: #{$pi-color-background-300}; + --pi-color-background-400: #{$pi-color-background-400}; + --pi-color-background-400: #{$pi-color-background-400}; +} diff --git a/libs/ui/src/lib/styles/common/abstracts/_pi-font-sizes-variables.scss b/libs/ui/src/lib/styles/common/abstracts/_pi-font-sizes-variables.scss new file mode 100644 index 0000000..5ab57da --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/_pi-font-sizes-variables.scss @@ -0,0 +1,24 @@ +// Font sizes variables +$pi-font-size-xxx-large: 2.875rem; // 46px +$pi-font-size-xx-large: 2.375rem; // 38px +$pi-font-size-x-large: 1.6875rem; // 27px +$pi-font-size-large: 1.375rem; // 22px +$pi-font-size-medium: 1.125rem; // 18px +$pi-font-size-small: 0.9375rem; // 15px +$pi-font-size-x-small: 0.75rem; // 12px +$pi-font-size-xx-small: 0.625rem; // 10px +$pi-font-size-xxx-small: 0.5rem; // 8px + +// Root variables +:root { + // Font sizes + --pi-font-size-xxx-large: #{$pi-font-size-xxx-large}; // 46px + --pi-font-size-xx-large: #{$pi-font-size-xx-large}; // 38px + --pi-font-size-x-large: #{$pi-font-size-x-large}; // 27x + --pi-font-size-large: #{$pi-font-size-large}; // 22px + --pi-font-size-medium: #{$pi-font-size-medium}; // 18px + --pi-font-size-small: #{$pi-font-size-small}; // 15px + --pi-font-size-x-small: #{$pi-font-size-x-small}; // 12px + --pi-font-size-xx-small: #{$pi-font-size-xx-small}; // 10px + --pi-font-size-xxx-small: #{$pi-font-size-xxx-small}; // 8px +} diff --git a/libs/ui/src/lib/styles/common/abstracts/_pi-global-variables.scss b/libs/ui/src/lib/styles/common/abstracts/_pi-global-variables.scss new file mode 100644 index 0000000..777896e --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/_pi-global-variables.scss @@ -0,0 +1,120 @@ +// Spacings +$pi-spacing-xxxx-large: 3rem; // 48px +$pi-spacing-xxx-large: 2.5rem; // 40px +$pi-spacing-xx-large: 2rem; // 32px +$pi-spacing-x-large: 1.5rem; // 24px +$pi-spacing-large: 1.25rem; // 20px +$pi-spacing-medium: 1rem; // 16px +$pi-spacing-small: 0.75rem; // 12px +$pi-spacing-x-small: 0.625rem; // 10px +$pi-spacing-xx-small: 0.5rem; // 8px +$pi-spacing-xxx-small: 0.25rem; // 4px +$pi-spacing-xxxx-small: 0.125rem; // 2px +$pi-spacing-xxxxx-small: 0.0625rem; // 1px +// Global sizes +$pi-height-layout-header: 4rem; // 64px +$pi-height-header: 5rem; // 80px +$pi-height-header-tablet: 5.25rem; // 84px +$pi-height-header-mobile: 5rem; // 80px +$pi-height-footer: 4.75rem; // 76px +$pi-height-action-bar: 8rem; // 128px +$pi-height-header-aside: 4.75rem; // 76px +$pi-height-footer-aside: 6.75rem; // 108px +$pi-height-button-large: 3rem; // 48px +$pi-size-menu-item-expanded: 3.75rem; // 60px +$pi-size-menu-item-collapsed: 4.75rem; // 76px +$pi-width-container-l: 75%; +$pi-width-container: 62rem; // 992px +$pi-width-container-s: 57.5rem; // 920px +$pi-width-navbar-collapsed: 7.4375rem; // 119px +$pi-width-navbar-expanded: 16rem; // 256px +$pi-width-children-menu-expanded: 17rem; // 272px +$pi-width-aside: 22.125rem; // 354px; +// Cards +$pi-height-card: 22rem; // 352px +$pi-height-card-s: 19.375rem; // 310px +$pi-width-card-xs: 17.125rem; // 274px +$pi-width-card-s: 18.25rem; // 292px +$pi-width-card: 21.5rem; // 344px +// Radius +$pi-border-radius-xx-small: 0.125rem; // 2px +$pi-border-radius-x-small: 0.3125rem; // 5px +$pi-border-radius-small: 0.625rem; // 10px +$pi-border-radius-medium: 1rem; // 16px +$pi-border-radius-large: 1.375rem; // 22px +$pi-border-radius-x-large: 1.375rem; // 22px +// Box shadow +$pi-box-shadow: rgba(#c5c5ce, 0.1) 3px 6px 6px 0px; +$pi-box-shadow-hover: rgba(#c3d5ff, 0.14) 2px 2px 5px 0px; +$pi-box-shadow-double: rgba(197, 197, 206, 0.1) 3px 6px 6px 0px, + -1px 4px 16px 2px rgba(197, 197, 206, 0.05); +$pi-box-shadow-panel-double: 7px 7px 12px 0px rgba(#c5c5ce, 0.12), + -7px 7px 12 0px rgba(#c5c5ce, 0.06); +$pi-box-shadow-top: 0px -5px 5px 0px rgba(121, 158, 240, 0.02); +$pi-box-shadow-panel: 7px 7px 12px 0px rgba(#c5c5ce, 0.08); +$pi-box-shadow-panel-disabled: 7px 7px 12px 0px rgba(#c5c5ce, 0.04); +$pi-box-shadow-inset: 0 0 42px -5px rgba(#c3d5ff, 0.2) inset; +// Animation +$pi-animation: all 0.25s ease-out; +$pi-animation-fast: all 0.16s ease-out; +$pi-animation-slow: all 0.4s ease-out; + +// Root variables +:root { + // Spacings + --pi-spacing-xxxx-large: #{$pi-spacing-xxxx-large}; // 3rem; // 48px + --pi-spacing-xxx-large: #{$pi-spacing-xxx-large}; // 2.5rem; // 40px + --pi-spacing-xx-large: #{$pi-spacing-xx-large}; // 2rem; // 32px + --pi-spacing-x-large: #{$pi-spacing-x-large}; // 1.5rem; // 24px + --pi-spacing-large: #{$pi-spacing-large}; // 1.25rem; // 20px + --pi-spacing-medium: #{$pi-spacing-medium}; // 1rem; // 16px + --pi-spacing-small: #{$pi-spacing-small}; // 0.75rem; // 12px + --pi-spacing-x-small: #{$pi-spacing-x-small}; // 0.625rem; // 10px + --pi-spacing-xx-small: #{$pi-spacing-xx-small}; // 0.5rem; // 8px + --pi-spacing-xxx-small: #{$pi-spacing-xxx-small}; // 0.25rem; // 4px + --pi-spacing-xxxx-small: #{$pi-spacing-xxxx-small}; // 0.125rem; // 2px + --pi-spacing-xxxxx-small: #{$pi-spacing-xxxxx-small}; // 0.0625rem // 1px + // Global Sizes + --pi-height-layout-header: #{$pi-height-layout-header}; // 4rem; // 64px + --pi-height-header: #{$pi-height-header}; // 5rem; // 80px + --pi-height-header-tablet: #{$pi-height-header-tablet}; // 3.75rem;// 60px + --pi-height-header-mobile: #{$pi-height-header-mobile}; // 3.5rem;// 56px + --pi-height-footer: #{$pi-height-footer}; // 4.75rem; // 76px + --pi-height-header-aside: #{$pi-height-header-aside}; // 4.75rem; // 76px + --pi-height-footer-aside: #{$pi-height-footer-aside}; // 6.75rem; // 108px + --pi-height-button-large: #{$pi-height-button-large}; // 3rem // 48px + --pi-size-menu-item-expanded: #{$pi-size-menu-item-expanded}; // 3.75rem; // 60px + --pi-size-menu-item-collapsed: #{$pi-size-menu-item-collapsed}; // 4.75rem; // 76px + --pi-width-container-l: #{$pi-width-container-l}; // 71.25rem; // 1140px + --pi-width-container: #{$pi-width-container}; // 62rem; // 992px + --pi-width-container-s: #{$pi-width-container-s}; // 57.5rem; // 920px + --pi-width-navbar-collapsed: #{$pi-width-navbar-collapsed}; // 7.4375rem; // 119px + --pi-width-navbar-expanded: #{$pi-width-navbar-expanded}; // 16rem; // 256px + --pi-width-children-menu-expanded: #{$pi-width-children-menu-expanded}; // 21.25rem // 340px + --pi-width-aside: #{$pi-width-aside}; // 22.125rem; // 354px; + // Cards + --pi-height-card: #{$pi-height-card}; // 22rem; // 352px + --pi-height-card-s: #{$pi-height-card-s}; // 19.375rem // 310px + --pi-width-card-xs: #{$pi-width-card-s}; // 17.125rem // 274px + --pi-width-card-s: #{$pi-width-card-xs}; // 18.25rem // 2274px + --pi-width-card: #{$pi-width-card}; // 21.5rem // 344px + // Radius + --pi-border-radius-xx-small: #{$pi-border-radius-xx-small}; // 2px; // 0.125rem + --pi-border-radius-x-small: #{$pi-border-radius-x-small}; // 6px; // .375rem; + --pi-border-radius-small: #{$pi-border-radius-small}; // 10px; // 3.125rem + --pi-border-radius-medium: #{$pi-border-radius-medium}; // 0.875rem; // 14px + --pi-border-radius-large: #{$pi-border-radius-large}; // 1.25rem; // 20px + // Box shadow + --pi-box-shadow: #{$pi-box-shadow}; // rgba(#030f3c, 0.03) 3px 6px 6px 0px; + --pi-box-shadow-hover: #{$pi-box-shadow-hover}; //rgba(#030f3c, 0.06) 2px 2px 4px 0px; + --pi-box-shadow-double: #{$pi-box-shadow-double}; // rgba(197, 197, 206, 0.1) 3px 6px 6px 0px, -1px 4px 16px 2px rgba(197, 197, 206, 0.05); + --pi-box-shadow-panel-double: #{$pi-box-shadow-panel-double}; // 10px 10px 16px 0px rgba(#030f3c, 0.02), + // -10px 10px 16px 0px rgba(#030f3c, 0.02); + --pi-box-shadow-panel: #{$pi-box-shadow-panel}; // 10px 10px 16px 0px rgba(#030f3c, 0.08); + --pi-box-shadow-top: #{$pi-box-shadow-top}; // 0px -5px 5px 0px rgba(121, 158, 240, 0.02); + --pi-box-shadow-inset: #{$pi-box-shadow-inset}; // 0 0 42px -5px rgba(#c3d5ff, 0.14) inset; + // Animation + --pi-animation: #{$pi-animation}; // all 0.25s ease; + --pi-animation-fast: #{$pi-animation-fast}; // all 0.16s ease; + --pi-animation-slow: #{$pi-animation-slow}; // all 0.4s ease; +} diff --git a/libs/ui/src/lib/styles/common/abstracts/_pi-gradient-variables.scss b/libs/ui/src/lib/styles/common/abstracts/_pi-gradient-variables.scss new file mode 100644 index 0000000..d7b2d9d --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/_pi-gradient-variables.scss @@ -0,0 +1,240 @@ +// Gradients +// Primary +// Primary light +$pi-gradient-primary-light: linear-gradient( + 36.98deg, + var(--pi-color-primary-75) 0.75%, + var(--pi-color-primary-25) 99.2% +); + +$pi-gradient-primary-light-rgba: linear-gradient( + 131deg, + rgba(215, 226, 251, 0.33) 0%, + rgba(240, 244, 255, 0.125) 100% +); + +$pi-gradient-primary-light-rgba-hover: linear-gradient( + 131deg, + rgba(215, 226, 251, 0.66) 0%, + rgba(240, 244, 255, 0.33) 100% +); + +// Primary dark +$pi-gradient-primary-dark: linear-gradient( + 36.98deg, + var(--pi-color-primary-900) 0.75%, + var(--pi-color-primary-400) 99.2% +); + +$pi-gradient-primary-dark-rgba: linear-gradient( + 131deg, + rgba(121, 158, 240, 0.32) 0%, + rgba(0, 0, 0, 0.125) 100% +); + +$pi-gradient-primary-dark-rgba-hover: linear-gradient( + 131deg, + rgba(121, 158, 240, 0.42) 0%, + rgba(0, 0, 0, 0.25) 100% +); + +$pi-gradient-primary-dark-disabled: linear-gradient( + 36.98deg, + var(--pi-color-primary-900) 0.75%, + var(--pi-color-primary-400) 99.2% +); + +// Accent +// Accent light +$pi-gradient-accent-light: linear-gradient( + 36.98deg, + var(--pi-color-accent-75) 0.75%, + var(--pi-color-accent-25) 99.2% +); + +$pi-gradient-accent-light-rgba: linear-gradient( + 131deg, + rgba(253, 235, 202, 0.61) 0%, + rgba(253, 245, 229, 0.125) 100% +); + +$pi-gradient-accent-light-rgba-hover: linear-gradient( + 131deg, + rgba(253, 235, 202, 0.81) 0%, + rgba(253, 245, 229, 0.25) 100% +); + +// Accent dark +$pi-gradient-accent-dark: linear-gradient( + 36.98deg, + var(--pi-color-accent-900) 0.75%, + var(--pi-color-accent-400) 99.2% +); +$pi-gradient-accent-dark-rgba: linear-gradient( + 131deg, + rgba(253, 235, 202, 0.61) 0%, + rgba(0, 0, 0, 0.125) 100% +); + +$pi-gradient-accent-dark-rgba-hover: linear-gradient( + 131deg, + rgba(253, 235, 202, 0.81) 0%, + rgba(0, 0, 0, 0.25) 100% +); + +// Highlighted +// Highlighted light +$pi-gradient-highlighted-light: linear-gradient( + 36.98deg, + var(--pi-color-highlighted-75) 0.75%, + var(--pi-color-highlighted-25) 99.2% +); +$pi-gradient-highlighted-light-rgba: linear-gradient( + 131deg, + rgba(100, 130, 215, 0.24) 0%, + rgba(255, 255, 255, 0.125) 100% +); +$pi-gradient-highlighted-light-rgba-hover: linear-gradient( + 131deg, + rgba(100, 130, 215, 0.44) 0%, + rgba(255, 255, 255, 0.25) 100% +); + +// Highlighted dark +$pi-gradient-highlighted-dark: linear-gradient( + 36.98deg, + var(--pi-color-highlighted-900) 0.89%, + var(--pi-color-highlighted) 99.2% +); +$pi-gradient-highlighted-dark-rgba: linear-gradient( + 131deg, + rgba(100, 130, 215, 0.24) 0%, + rgba(0, 0, 0, 0.125) 100% +); +$pi-gradient-highlighted-dark-rgba-hover: linear-gradient( + 131deg, + rgba(100, 130, 215, 0.44) 0%, + rgba(0, 0, 0, 0.25) 100% +); +// Grey +// Grey light +$pi-gradient-grey-light: linear-gradient( + 36.98deg, + var(--pi-color-grey-75) 0.75%, + var(--pi-color-grey-25) 99.2% +); + +$pi-gradient-grey-light-rgba: linear-gradient( + 131deg, + rgba(233, 235, 241, 0.25) 0%, + rgba(243, 244, 244, 0.125) 100% +); + +$pi-gradient-grey-light-rgba-hover: linear-gradient( + 131deg, + rgba(233, 235, 241, 0.5) 0%, + rgba(243, 244, 244, 0.25) 100% +); +// Grey dark +$pi-gradient-grey-dark: linear-gradient( + 36.98deg, + var(--pi-color-grey-900) 0.89%, + var(--pi-color-grey) 99.2% +); + +$pi-gradient-grey-dark-rgba: linear-gradient( + 131deg, + rgba(61, 61, 61, 0.61) 0%, + rgba(0, 0, 0, 0.125) 100% +); + +$pi-gradient-grey-dark-rgba-hover: linear-gradient( + 131deg, + rgba(61, 61, 61, 0.81) 0%, + rgba(0, 0, 0, 0.25) 100% +); + +// White +$pi-gradient-white: linear-gradient( + 36.98deg, + var(--pi-color-white-900) 0.89%, + var(--pi-color-white) 99.2% +); +$pi-gradient-white-rgba-disabled: linear-gradient( + 131deg, + rgba(255, 255, 255, 0.25) 0%, + rgba(255, 255, 255, 0.33) 100% +); +$pi-gradient-white-rgba: linear-gradient( + 131deg, + rgba(255, 255, 255, 0.33) 0%, + rgba(255, 255, 255, 0.44) 100% +); + +$pi-gradient-white-rgba-hover: linear-gradient( + 131deg, + rgba(255, 255, 255, 0.5) 0%, + rgba(255, 255, 255, 0.7) 100% +); +// Disabled +// Light +$pi-gradient-light-disabled: linear-gradient( + 36.98deg, + rgba(192, 192, 192, 1) 0.75%, + rgba(242, 242, 242, 1) 99.2% +); +// Dark +$pi-gradient-dark-disabled: linear-gradient( + 36.98deg, + rgba(138, 138, 138, 1) 0.89%, + rgba(182, 182, 182, 1) 99.2% +); +:root { + // Gradient + // Primary + // Primary & Light + --pi-gradient-primary-light: #{$pi-gradient-primary-light}; + + --pi-gradient-primary-light-rgba: #{$pi-gradient-primary-light-rgba}; + --pi-gradient-primary-light-rgba-hover: #{$pi-gradient-primary-light-rgba-hover}; + // Primary & Dark + --pi-gradient-primary-dark: #{$pi-gradient-primary-dark}; + + --pi-gradient-primary-dark-rgba: #{$pi-gradient-primary-dark-rgba}; + --pi-gradient-primary-dark-rgba-hover: #{$pi-gradient-primary-dark-rgba-hover}; + // Accent + // Accent & Light + --pi-gradient-accent-light: #{$pi-gradient-accent-light}; + --pi-gradient-accent-light-rgba: #{$pi-gradient-accent-light-rgba}; + --pi-gradient-accent-light-rgba-hover: #{$pi-gradient-accent-light-rgba-hover}; + // Accent & Dark + --pi-gradient-accent-dark: #{$pi-gradient-accent-dark}; + --pi-gradient-accent-dark-rgba: #{$pi-gradient-accent-dark-rgba}; + --pi-gradient-accent-dark-rgba-hover: #{$pi-gradient-accent-dark-rgba-hover}; + // Highlighted + // Highlighted & Light + --pi-gradient-highlighted-light: #{$pi-gradient-highlighted-light}; + --pi-gradient-highlighted-light-rgba: #{$pi-gradient-highlighted-light-rgba}; + --pi-gradient-highlighted-light-rgba-hover: #{$pi-gradient-highlighted-light-rgba-hover}; + // Highlighted & Dark + --pi-gradient-highlighted-dark: #{$pi-gradient-highlighted-dark}; + --pi-gradient-highlighted-dark-rgba: #{$pi-gradient-highlighted-dark-rgba}; + --pi-gradient-highlighted-dark-rgba-hover: #{$pi-gradient-highlighted-dark-rgba-hover}; + // Grey + // Grey & Light + --pi-gradient-grey-light: #{$pi-gradient-grey-light}; + --pi-gradient-grey-light-rgba: #{$pi-gradient-grey-light-rgba}; + --pi-gradient-grey-light-rgba-hover: #{$pi-gradient-grey-light-rgba-hover}; + // Grey & Dark + --pi-gradient-grey-dark: #{$pi-gradient-grey-dark}; + --pi-gradient-grey-dark-rgba: #{$pi-gradient-grey-dark-rgba}; + --pi-gradient-grey-dark-rgba-hover: #{$pi-gradient-grey-dark-rgba-hover}; + // White + --pi-gradient-white: #{$pi-gradient-white}; + --pi-gradient-white-rgba: #{$pi-gradient-white-rgba}; + --pi-gradient-white-rgba-hover: #{$pi-gradient-white-rgba-hover}; + --pi-gradient-white-rgba-disabled: #{$pi-gradient-white-rgba-disabled}; + // Disabled + --pi-gradient-light-disabled: #{$pi-gradient-light-disabled}; + --pi-gradient-dark-disabled: #{$pi-gradient-dark-disabled}; +} diff --git a/libs/ui/src/lib/styles/common/abstracts/_pi-public-variables.scss b/libs/ui/src/lib/styles/common/abstracts/_pi-public-variables.scss new file mode 100644 index 0000000..b08320c --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/_pi-public-variables.scss @@ -0,0 +1,22 @@ +// Font sizes variables +$pi-public-font-size-xxx-large: 4.25rem; // 68px +$pi-public-font-size-xx-large: 3.875rem; // 62px + +// Global sizes +$pi-public-height-header-desktop: 4.5rem; // 72px +$pi-public-height-header-mobile: 4.25rem; // 68px +$pi-public-width-container: 72rem; // 1152px +$pi-public-width-container-l: 90rem; // 1440px + +// Root variables +:root { + // Font sizes + --pi-public-font-size-xxx-large: #{$pi-public-font-size-xxx-large}; // 4.5rem; // 72px + --pi-public-font-size-xx-large: #{$pi-public-font-size-xx-large}; // 3.875rem; // 62px + + // Global sizes + --pi-public-height-header-desktop: #{$pi-public-height-header-desktop}; // 5.25rem; // 84px + --pi-public-height-header-mobile: #{$pi-public-height-header-mobile}; // 4.125rem; // 66px + --pi-public-width-container: #{$pi-public-width-container}; // 72rem; // 1152px + --pi-public-width-container-l: #{$pi-public-width-container-l}; // 90rem; // 1440px +} diff --git a/libs/ui/src/lib/styles/common/abstracts/index.scss b/libs/ui/src/lib/styles/common/abstracts/index.scss new file mode 100644 index 0000000..e91205a --- /dev/null +++ b/libs/ui/src/lib/styles/common/abstracts/index.scss @@ -0,0 +1,5 @@ +@forward 'pi-color-variables'; +@forward 'pi-font-sizes-variables'; +@forward 'pi-gradient-variables'; +@forward 'pi-public-variables'; +@forward 'pi-global-variables'; diff --git a/libs/ui/src/lib/styles/common/base/_pi-reset.scss b/libs/ui/src/lib/styles/common/base/_pi-reset.scss new file mode 100644 index 0000000..71370a6 --- /dev/null +++ b/libs/ui/src/lib/styles/common/base/_pi-reset.scss @@ -0,0 +1,594 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + font-size: 1rem; + scroll-behavior: smooth; +} + +article, +aside, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section { + display: block; +} + +// Body +body { + margin: 0; + -ms-overflow-style: none; + line-height: 1.4; + -webkit-font-smoothing: antialiased; + color: var(--pi-color-grey-900); + scrollbar-width: none; + color: var(--pi-color-grey-900); + scroll-behavior: smooth; + overflow-x: hidden; + &::-webkit-scrollbar { + display: none; + } + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + size: 0.9375rem; + weight: 300; + } +} + +// EN +html[dir='ltr'] { + body { + text-align: left; + } +} + +// AR +html[dir='rtl'] { + body { + text-align: right; + } +} + +[tabindex='-1']:focus { + outline: 0 !important; +} + +// Content grouping +hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +// Typography +h1, +h2, +h3, +h4, +h5, +h6 { + margin-top: 0; + margin-bottom: 0; + font-size: inherit; + font-weight: inherit; + line-height: 133.33%; +} + +a { + color: var(--pi-color-primary); +} + +.pi-inner-blog { + & > div { + & > *:nth-child(n + 2):not(h1) { + display: block; + margin-top: var(--pi-spacing-x-large); + } + & > h1 { + margin-top: var(--pi-spacing-xxxx-large); + } + } + h1 { + & > span { + font: { + size: var(--pi-font-size-xx-large); + weight: 700; + } + } + } + ul { + li { + h3 { + & > span { + font: { + size: var(--pi-font-size-x-large); + } + } + } + } + } +} +// Fonts +.pi-layout, +.cdk-overlay-container { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + &::-webkit-scrollbar { + display: none; + } +} + +.pi-domain-component { + &.pi-component { + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-x-large); + } + } +} + +p { + margin-top: 0; + margin-bottom: 0; + line-height: normal; +} + +p + p { + margin-top: var(--pi-spacing-medium); +} + +address { + margin-bottom: 0; + font-style: normal; + line-height: inherit; +} + +ol, +ul, +dl { + margin-top: 0; + margin-bottom: 0; +} + +ul { + padding: 0; +} + +ol ol, +ul ul, +ol ul, +ul ol { + margin-bottom: 0; +} + +li { + list-style-position: inside; +} + +li + li { + margin-top: var(--pi-spacing-medium); +} + +dt { + font-weight: 700; +} + +dd { + margin-bottom: 0.5rem; +} + +// EN +html[dir='ltr'] { + dd { + margin-left: 0; + } +} + +// AR +html[dir='rtl'] { + dd { + margin-right: 0; + } +} + +blockquote { + margin: 0; +} + +b, +strong { + font-weight: 700; +} + +small { + font-size: 80%; +} + +// all browsers. +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +// Hr +hr { + height: var(--pi-spacing-xxx-small); + max-width: 4.5rem; + border-radius: 0.25rem; + border: unset; + &.pi-color__primary { + background-color: var(--pi-color-primary); + } + &.pi-color__accent { + background-color: var(--pi-color-accent); + } + &.pi-color__warn { + background-color: var(--pi-color-warn); + } +} + +// Links +a { + text-decoration: none; + background-color: transparent; + + &:hover { + text-decoration: underline; + } +} + +a:not([href]):not([tabindex]) { + text-decoration: none; + + &:hover, + &:focus { + color: inherit; + text-decoration: none; + } + + &:focus { + &:not(button) { + outline: 0; + } + } +} + +// Code +code, +kbd, +samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 1em; +} + +// Figures +figure { + margin: 0; +} + +// Images and content +img { + vertical-align: middle; + border-style: none; +} + +svg { + overflow: hidden; + vertical-align: middle; +} + +// Tables +table { + border-collapse: collapse; +} + +caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + caption-side: bottom; +} + +// EN +html[dir='ltr'] { + caption { + text-align: left; + } +} + +// AR +html[dir='rtl'] { + caption { + text-align: right; + } +} + +th { + text-align: inherit; +} + +// Forms +label { + display: inline-block; +} + +input, +button, +select, +optgroup, +textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input { + overflow: visible; +} + +:focus-visible, +:focus { + &:not(.pi-button__appearance--outline) { + outline: none !important; + } +} + +button, +select { + text-transform: none; +} + +select { + word-wrap: normal; +} + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; +} + +button::-moz-focus-inner, +[type='button']::-moz-focus-inner, +[type='reset']::-moz-focus-inner, +[type='submit']::-moz-focus-inner { + padding: 0; + border-style: none; +} + +input[type='radio'], +input[type='checkbox'] { + box-sizing: border-box; + padding: 0; +} + +input[type='date'], +input[type='time'], +input[type='datetime-local'], +input[type='month'] { + -webkit-appearance: listbox; +} + +input[type='search']::-webkit-search-decoration, +input[type='search']::-webkit-search-cancel-button, +input[type='search']::-webkit-search-results-button, +input[type='search']::-webkit-search-results-decoration { + display: none; +} + +textarea { + overflow: auto; + resize: vertical; +} + +input, +select, +textarea { + &:-webkit-autofill { + border: none; + -webkit-text-fill-color: var(--pi-color-grey); + box-shadow: none; + -webkit-box-shadow: none; + transition: background-color 5000s ease-in-out 0s; + background-color: white; + .mat-icon { + color: var(--pi-color-grey-200); + } + + &:hover, + &:focus { + border: none; + -webkit-text-fill-color: var(--pi-color-grey); + box-shadow: 0 0 0 62.5rem transparent inset; + -webkit-box-shadow: 0 0 0 62.5rem transparent inset; + transition: background-color 5000s ease-in-out 0s; + background-color: white; + .mat-icon { + color: var(--pi-color-grey); + } + } + } +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +textarea:-webkit-autofill, +textarea:-webkit-autofill:hover, +textarea:-webkit-autofill:focus, +select:-webkit-autofill, +select:-webkit-autofill:hover, +select:-webkit-autofill:focus { + -webkit-text-fill-color: var(--pi-color-grey); + box-shadow: 0 0 0 62.5rem white inset; + -webkit-box-shadow: 0 0 0 62.5rem white inset; + -webkit-transition: background-color 5000s ease-in-out 0s; + transition: background-color 5000s ease-in-out 0s; + background-color: white !important; +} + +fieldset { + all: unset; + width: -webkit-fill-available; +} + +progress { + vertical-align: baseline; +} + +[type='number']::-webkit-inner-spin-button, +[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +[type='search'] { + outline-offset: -0.125rem; + -webkit-appearance: none; +} + +[type='search']::-webkit-search-decoration { + -webkit-appearance: none; +} + +::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; +} + +template { + display: none; +} + +[hidden] { + display: none !important; +} + +// Wrapper common +.pi-wrapper { + display: block; +} +.pi-shell { + & > div { + &:not(.is-fullscreen) { + // Generic Scroll + &::-webkit-scrollbar { + width: var(--pi-spacing-xxx-small); + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba($color: #8695b5, $alpha: 0.4); + border-radius: var(--pi-border-radius-small); + + &:hover { + transition: var(--pi-animation); + background-color: rgba($color: #8695b5, $alpha: 0.6); + } + } + } + } +} + +// Generic Scroll +::-webkit-scrollbar { + width: var(--pi-spacing-xxx-small); +} + +::-webkit-scrollbar-track { + box-shadow: none; +} + +::-webkit-scrollbar-thumb { + background-color: rgba($color: #8695b5, $alpha: 0.3); + border-radius: var(--pi-border-radius-small); + + &:hover { + transition: var(--pi-animation); + background-color: rgba($color: #8695b5, $alpha: 0.5); + } +} + +button, +.pi-input, +[class*='mat-'] { + display: block; +} + +html { + .intercom-lightweight-app-launcher { + background: var(--pi-color-accent-300); + } + // Mobile + &.is-mobile { + .intercom-lightweight-app-launcher { + transform: scale(0.9); + } + } + // Small device + &:not(.is-desktop) { + @media screen and (max-width: 62rem) { + .intercom-lightweight-app-launcher { + bottom: 5.75rem; + left: 1rem; + } + } + } + &:not(.is-mobile) { + @media screen and (min-width: 62rem) { + &.is-tablet__large { + .intercom-lightweight-app-launcher { + left: 3.75rem; + bottom: 5.625rem; + } + } + &.is-desktop { + .is-fullscreen { + .intercom-lightweight-app-launcher { + left: 1.5rem; + bottom: 1.5rem; + } + } + :not(.is-fullscreen) { + .intercom-lightweight-app-launcher { + right: 1.5rem; + bottom: 1.5rem; + } + } + } + } + } +} + +@media screen and (max-width: 62rem) { + #hubspot-messages-iframe-container { + &.widget-align-right { + display: none !important; + } + } +} diff --git a/libs/ui/src/lib/styles/common/base/_pi-tipography.scss b/libs/ui/src/lib/styles/common/base/_pi-tipography.scss new file mode 100644 index 0000000..3825019 --- /dev/null +++ b/libs/ui/src/lib/styles/common/base/_pi-tipography.scss @@ -0,0 +1,119 @@ +// Light - 300 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.woff2') format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.woff') format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.ttf') format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht.svg#soehne-leicht') + format('svg'); + font-weight: 300; + font-style: normal; + font-display: swap; +} + +// Light italic - 300 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.woff2') + format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.woff') + format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.ttf') + format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-leicht-kursiv.svg#soehne-leicht-kursiv') + format('svg'); + font-weight: 300; + font-style: italic; + font-display: swap; +} + +// Regular - 400 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.woff2') format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.woff') format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.ttf') format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch.svg#soehne-buch') + format('svg'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +// Regular italic - 400 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.woff2') + format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.woff') format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.ttf') + format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-buch-kursiv.svg#soehne-buch-kursiv') + format('svg'); + font-weight: 400; + font-style: italic; + font-display: swap; +} + +// Medium - 600 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.woff2') format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.woff') format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.ttf') format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-kraftig.svg#soehne-kraftig') + format('svg'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +// Bold - 700 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.woff2') format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.woff') format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.ttf') format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-halbfett.svg#soehne-halbfett') + format('svg'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +// Black - 800 +@font-face { + font-family: 'Soehne'; + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.eot'); + src: url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.eot?#iefix') + format('embedded-opentype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.woff2') + format('woff2'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.woff') + format('woff'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.ttf') + format('truetype'), + url('https://d2m4265u67f2xu.cloudfront.net/assets/fonts/soehne-dreiviertelfett.svg#soehne-dreiviertelfett') + format('svg'); + font-weight: 800; + font-style: normal; + font-display: swap; +} diff --git a/libs/ui/src/lib/styles/common/base/index.scss b/libs/ui/src/lib/styles/common/base/index.scss new file mode 100644 index 0000000..35d8740 --- /dev/null +++ b/libs/ui/src/lib/styles/common/base/index.scss @@ -0,0 +1,2 @@ +@forward 'pi-reset'; +@forward 'pi-tipography'; diff --git a/libs/ui/src/lib/styles/common/components/_pi-accordion.scss b/libs/ui/src/lib/styles/common/components/_pi-accordion.scss new file mode 100644 index 0000000..335fa6f --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-accordion.scss @@ -0,0 +1,116 @@ +@mixin accordionHeaderDescription($white-space: wrap) { + .pi-layout { + &.pi-layout__02-full-width { + .pi-accordion { + & > .pi-accordion-item { + & > .pi-accordion-item-content { + &.pi-accordion-item-content__header { + & > .pi-text-viewer { + & > article { + & > span { + white-space: $white-space; + } + } + } + } + } + } + } + } + } + .pi-accordion { + & > .pi-accordion-item { + & > .pi-accordion-item-content { + &.pi-accordion-item-content__header { + & > .pi-text-viewer { + overflow: hidden; + & > article { + overflow: hidden; + & > span { + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + } + } + } +} + +.pi-accordion { + .pi-note-content__label { + font: { + size: var(--pi-font-size-x-small); + } + } + + // Horizontal + &.pi-accordion__direction--horizontal { + & > * { + & > .pi-accordion-item-content { + &.pi-accordion-item-content__header { + &.is-expanded { + .pi-list-item-content { + &.pi-list-item-content__left { + font: { + weight: 600; + } + } + } + } + &:not(.is-disabled) { + &:hover { + * { + cursor: pointer; + } + } + } + } + } + } + } + // Type + // Default + &:not(.pi-accordion__appearance--outline) { + // Footer + & > .pi-accordion-footer { + padding: var(--pi-spacing-x-large) var(--pi-spacing-xx-small) var(--pi-spacing-medium); + } + } +} + +html { + &.is-mobile { + @include accordionHeaderDescription; + .pi-layout { + &.pi-layout__main { + .pi-accordion { + // Default (Vertical) + &:not(.pi-accordion__direction--horizontal) { + // Accordion item + & > .pi-accordion-item { + & > .pi-accordion-item-content__header { + &.has-status { + flex-direction: column-reverse; + align-items: flex-start; + gap: var(--pi-spacing-x-large); + & > aside { + width: 100%; + justify-content: space-between; + & > span { + display: none; + } + } + } + } + } + } + } + } + } + } + &:not(.is-mobile) { + @include accordionHeaderDescription(nowrap); + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-card.scss b/libs/ui/src/lib/styles/common/components/_pi-card.scss new file mode 100644 index 0000000..b10f07f --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-card.scss @@ -0,0 +1,545 @@ +// Block conmtents +// Top +@mixin cardContentTop { + &.pi-card-content__top { + display: flex; + } +} +// Center +@mixin cardContentCenter { + &.pi-card-content__center { + display: flex; + flex-direction: column; + } +} +// Bottom +@mixin cardContentBottom { + &.pi-card-content__bottom { + display: flex; + flex-direction: column; + justify-content: space-between; + } +} + +// Flex +// Align items +@mixin cardAlignment($aligment: flex-start) { + align-items: $aligment; + .pi-card-content { + justify-content: $aligment; + } +} +// Gap +@mixin cardGap($gap) { + gap: var(--pi-spacing-#{$gap}); +} + +// Spacings +// Pading +@mixin cardPadding($padding) { + padding: var(--pi-spacing-#{$padding}); +} +// Inner Pading +@mixin cardInnerPadding($large-padding: unset, $small-padding) { + padding: unset; + + &:not(.has-equal-inner-padding) { + & > *:first-child { + padding-top: var(--pi-spacing-#{$large-padding}); + padding-left: var(--pi-spacing-#{$large-padding}); + padding-bottom: var(--pi-spacing-#{$large-padding}); + padding-right: var(--pi-spacing-#{$large-padding}); + border-radius: var(--pi-border-radius-large) var(--pi-border-radius-large) 0 0; + } + + & > *:nth-child(2) { + &:not(:last-child) { + padding-top: var(--pi-spacing-#{$small-padding}); + padding-left: var(--pi-spacing-#{$large-padding}); + padding-bottom: 0; + padding-right: var(--pi-spacing-#{$large-padding}); + } + } + + & > *:last-child { + padding-top: var(--pi-spacing-#{$small-padding}); + padding-bottom: var(--pi-spacing-#{$small-padding}); + border-radius: 0 0 var(--pi-border-radius-large) var(--pi-border-radius-large); + } + + & > * { + padding: 0 var(--pi-spacing-#{$large-padding}) var(--pi-spacing-#{$small-padding}); + } + } + &.has-equal-inner-padding { + & > *:first-child { + padding-left: var(--pi-spacing-#{$large-padding}); + padding-top: var(--pi-spacing-#{$large-padding}); + padding-bottom: var(--pi-spacing-#{$small-padding}); + padding-right: var(--pi-spacing-#{$large-padding}); + border-radius: var(--pi-border-radius-large) var(--pi-border-radius-large) 0 0; + } + + & > *:last-child { + padding-top: var(--pi-spacing-#{$large-padding}); + padding-bottom: var(--pi-spacing-#{$large-padding}); + border-radius: 0 0 var(--pi-border-radius-large) var(--pi-border-radius-large); + } + + & > * { + &:not(:first-child) { + padding: var(--pi-spacing-#{$small-padding}) var(--pi-spacing-#{$large-padding}) + var(--pi-spacing-#{$small-padding}); + } + } + } +} + +// Card +@mixin cardColor($color) { + // Mode Light + &.pi-color__mode--light { + // Light Translucent + &[class*='pi-color__opacity'] { + // Light Glass gradient + background-image: var(--pi-gradient-#{$color}-light-rgba); + &.is-clickable { + &:hover { + // Light Glass gradient hover + background-image: var(--pi-gradient-#{$color}-light-rgba-hover); + } + } + } + + // Light & Is stroked + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + &.is-clickable { + &:hover { + border-color: var(--pi-color-#{$color}-200); + } + } + } + } + + // Mode Dark + &.pi-color__mode--dark { + // Dark Translucent + &[class*='pi-color__opacity'] { + // Glass gradient + background-image: var(--pi-gradient-#{$color}-dark-rgba); + // Dark Translucent Gradient & Is Stroked + &.is-stroked { + border-color: var(--pi-color-#{$color}); + } + &.is-clickable { + &:hover { + // Light Glass gradient hover + background-image: var(--pi-gradient-#{$color}-dark-rgba-hover); + // Dark Translucent Gradient & Is Stroked Hover + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + } + } +} + +@mixin privateOpaqueCardColor($color) { + // Mode light + &.pi-color__mode--light { + // Light Opaque + &:not([class*='pi-color__opacity']):not([class*='pi-color__density']) { + // Solid light color + background-color: var(--pi-color-#{$color}-25); + } + } + // Mode dark + &.pi-color__mode--dark { + // Dark Opaque + &:not([class*='pi-color__opacity']):not([class*='pi-color__density']) { + background-color: var(--pi-color-#{$color}); + // Solid Dark Color & Is Stroked + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + // Density + // 10 + &.pi-color__density--10 { + // Solid light color 10 + background-color: var(--pi-color-#{$color}-10); + } + // 50 + &.pi-color__density--50 { + // Solid light color 50 + background-color: var(--pi-color-#{$color}-50); + } + // 75 + &.pi-color__density--75 { + // Solid light color 75 + background-color: var(--pi-color-#{$color}-75); + } + // 100 + &.pi-color__density--100 { + // Solid light color 100 + background-color: var(--pi-color-#{$color}-100); + } + // 200 + &.pi-color__density--200 { + // Solid light color 200 + background-color: var(--pi-color-#{$color}-200); + } + // 300 + &.pi-color__density--300 { + // Solid light color 300 + background-color: var(--pi-color-#{$color}-300); + } + // 400 + &.pi-color__density--400 { + // Solid light color 400 + background-color: var(--pi-color-#{$color}-400); + } + // 900 + &.pi-color__density--900 { + // Solid light color 900 + background-color: var(--pi-color-#{$color}-900); + } +} + +@mixin publicOpaqueCardColor($color) { + // Mode light + &.pi-color__mode--light { + // Light Opaque + &:not([class*='pi-color__opacity']) { + // Opaque light gradient + background-image: var(--pi-gradient-#{$color}-light); + } + } + // Mode dark + &.pi-color__mode--dark { + &:not([class*='pi-color__opacity']) { + // Opaque Light Gradient + background-image: var(--pi-gradient-#{$color}-dark); + // Dark Opaque Gradient & Is Stroked + &.is-stroked { + border-color: var(--pi-color-#{$color}); + } + } + } +} + +@mixin cardColorWhite { + // Light Translucent + &[class*='pi-color__opacity'] { + // Light Glass color + background-color: var(--pi-color-white-rgba-03); + &.is-clickable { + &:hover { + // Light Glass color hover + background-color: var(--pi-color-white-rgba-06); + } + } + } + // Light Opaque + &:not([class*='pi-color__opacity']) { + background-color: var(--pi-color-white); + } + // Light & Is stroked + &.is-stroked { + border-color: var(--pi-color-white-900); + } +} + +// Card contents +@mixin cardContentColor($background-color) { + &:not([class*='pi-color__density']) { + background-color: var(--pi-color-#{$background-color}); + } + + &[class*='pi-color__density'] { + &.pi-card-content__top { + border-bottom: 1px solid; + border-bottom-color: var(--pi-color-white); + } + &:not(.pi-color__density--10):not(.pi-color__density--25) { + background-color: var(--pi-color-#{$background-color}-50); + } + &.pi-color__density--10 { + background-color: var(--pi-color-#{$background-color}-10); + } + &.pi-color__density--25 { + background-color: var(--pi-color-#{$background-color}-25); + } + } +} + +// Card +.pi-card { + &:not(.pi-grid-item) { + display: flex; + flex-direction: column; + justify-content: space-between; + width: -webkit-fill-available; + padding: var(--pi-spacing-xx-large); + border-radius: var(--pi-border-radius-large); + transition: var(--pi-animation); + + &:hover { + transition: var(--pi-animation); + } + &:not(:hover) { + transition: var(--pi-animation); + } + + // Is Stroked + &.is-stroked { + border: 1px solid; + } + + // Is Clickable + &.is-clickable { + cursor: pointer; + } + + // Glass + &[class*='pi-color__opacity'] { + -webkit-backdrop-filter: blur(0.75rem); + backdrop-filter: blur(0.75rem); + } + + // Has Shadow + &.has-shadow { + box-shadow: var(--pi-box-shadow-panel); + } + &:not(.has-shadow) { + box-shadow: none; + } + + // Alignment + // Flex-start + &.pi-card__align-items--flex-start { + @include cardAlignment; + } + // Center + &.pi-card__align-items--center { + @include cardAlignment(center); + } + // Flex-end + &.pi-card__align-items--flex-end { + @include cardAlignment(flex-end); + } + + // Gap + // S + &.pi-card__gap--s { + @include cardGap(small); + } + // M + &.pi-card__gap--m { + @include cardGap(medium); + } + // L + &.pi-card__gap--l { + @include cardGap(large); + } + // XL + &.pi-card__gap--xl { + @include cardGap(x-large); + } + // XXL + &.pi-card__gap--xxl { + @include cardGap(xx-large); + } + // XXXL + &.pi-card__gap--xxxl { + @include cardGap(xxx-large); + } + // Padding + // XS + &.pi-card__padding--xxs { + @include cardPadding(xx-small); + } + // XS + &.pi-card__padding--xs { + @include cardPadding(x-small); + } + // S + &.pi-card__padding--s { + @include cardPadding(small); + } + // M + &.pi-card__padding--m { + @include cardPadding(medium); + } + // L + &.pi-card__padding--l { + @include cardPadding(large); + } + // XL + &.pi-card__padding--xl { + @include cardPadding(x-large); + } + // XXL + &.pi-card__padding--xxl { + @include cardPadding(xx-large); + } + + // Inner padding + // XS + &.pi-card__inner-padding--xxs { + @include cardInnerPadding(xx-small, xxxx-small); + } + // XS + &.pi-card__inner-padding--xs { + @include cardInnerPadding(x-small, xxx-small); + } + // S + &.pi-card__inner-padding--s { + @include cardInnerPadding(small, xx-small); + } + // M + &.pi-card__inner-padding--m { + @include cardInnerPadding(medium, x-small); + } + // L + &.pi-card__inner-padding--l { + @include cardInnerPadding(large, small); + } + // XL + &.pi-card__inner-padding--xl { + @include cardInnerPadding(x-large, medium); + } + // XXL + &.pi-card__inner-padding--xxl { + @include cardInnerPadding(xx-large, x-large); + } + + // Color + // Primary + &.pi-color__primary { + @include cardColor(primary); + } + // Accent + &.pi-color__accent { + @include cardColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include cardColor(highlighted); + } + // Grey + &.pi-color__grey { + @include cardColor(grey); + } + // White + &.pi-color__white { + @include cardColorWhite; + } + } + + &.is-public { + // Color + // Primary + &.pi-color__primary { + @include publicOpaqueCardColor(primary); + } + // Accent + &.pi-color__accent { + @include publicOpaqueCardColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include publicOpaqueCardColor(highlighted); + } + // Grey + &.pi-color__grey { + @include publicOpaqueCardColor(grey); + } + } + + &.is-private, + &:not(.is-public) { + // Color + // Primary + &.pi-color__primary { + @include privateOpaqueCardColor(primary); + } + // Accent + &.pi-color__accent { + @include privateOpaqueCardColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include privateOpaqueCardColor(highlighted); + } + // Grey + &.pi-color__grey { + @include privateOpaqueCardColor(grey); + } + } + & > .pi-card-content { + &.pi-color__primary { + @include cardContentColor(primary); + } + // Accent + &.pi-color__accent { + @include cardContentColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include cardContentColor(highlighted); + } + // Grey + &.pi-color__grey { + @include cardContentColor(grey); + } + } +} + +.pi-shell__deale-website, +.is-public { + .pi-card { + // Color + // Primary + &.pi-color__primary { + @include publicOpaqueCardColor(primary); + } + // Accent + &.pi-color__accent { + @include publicOpaqueCardColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include publicOpaqueCardColor(highlighted); + } + // Grey + &.pi-color__grey { + @include publicOpaqueCardColor(grey); + } + } +} + +.is-private { + .pi-card { + // Color + // Primary + &.pi-color__primary { + @include privateOpaqueCardColor(primary); + } + // Accent + &.pi-color__accent { + @include privateOpaqueCardColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include privateOpaqueCardColor(highlighted); + } + // Grey + &.pi-color__grey { + @include privateOpaqueCardColor(grey); + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-divider.scss b/libs/ui/src/lib/styles/common/components/_pi-divider.scss new file mode 100644 index 0000000..1026d16 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-divider.scss @@ -0,0 +1,105 @@ +@mixin dividerIconSize($size) { + .pi-icon { + .mat-icon { + height: var(--pi-spacing-#{$size}) !important; + width: var(--pi-spacing-#{$size}) !important; + } + } +} + +@mixin dividerIconColor($icon-color) { + .pi-icon { + &:not(.pi-icon__appearance--fill) { + .mat-icon { + color: var(--pi-color-#{$icon-color}); + } + } + &.pi-icon__appearance--fill { + .mat-icon { + color: var(--pi-color-#{$icon-color}); + } + &.is-stroked { + border-color: var(--pi-color-#{$icon-color}); + } + } + } +} + +.pi-divider { + // Icon Color + &.pi-color__primary { + @include dividerIconColor(primary); + } + &.pi-color__accent { + @include dividerIconColor(accent); + } + &.pi-color__warn { + @include dividerIconColor(warn); + } + &.pi-color__success { + @include dividerIconColor(success); + } + &.pi-color__white { + @include dividerIconColor(white); + } + &.pi-color__black { + @include dividerIconColor(black); + } + &.pi-color__grey { + @include dividerIconColor(grey-100); + } + &.pi-color__highlighted { + @include dividerIconColor(highlighted); + } + &.pi-color__black-primary { + @include dividerIconColor(primary); + } + &.pi-color__primary-black { + @include dividerIconColor(black); + } + &.pi-color__black-accent { + @include dividerIconColor(accent); + } + &.pi-color__accent-black { + @include dividerIconColor(black); + } + &.pi-color__dark-black { + @include dividerIconColor(black); + } + &.pi-color__black-dark { + @include dividerIconColor(grey-400); + } + &.pi-color__grey-black { + @include dividerIconColor(black); + } + &.pi-color__black-grey { + @include dividerIconColor(grey-200); + } + &.pi-color__highlighted-grey { + @include dividerIconColor(grey-200); + } + &.pi-color__grey-highlighted { + @include dividerIconColor(highlighted); + } + // Icon size + // X small + &.pi-divider__icon-size--x-small { + @include dividerIconSize(large); + } + // small + &.pi-divider__icon-size--small { + @include dividerIconSize(x-large); + } + // medium + &.pi-divider__icon-size--medium { + @include dividerIconSize(xx-large); + } + // large + &.pi-divider__icon-size--large { + @include dividerIconSize(xxx-large); + } + // X large + &.pi-divider__icon-size--x-large { + @include dividerIconSize(xxxx-large); + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-headline.scss b/libs/ui/src/lib/styles/common/components/_pi-headline.scss new file mode 100644 index 0000000..3d3107b --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-headline.scss @@ -0,0 +1,71 @@ +// Column +.pi-headline { + &.pi-headline__flex--column { + flex-direction: column; + &.pi-headline__align--center { + align-items: center; + text-align: center; + aside { + button { + margin: 1.75rem auto 0; + } + } + .pi-headline-content { + & > *:is(h1, h2, h3, h4, h5, p, span, strong, i) { + text-align: center; + } + } + } + } + figure { + width: 100%; + } +} + +// Headline Content Directives +.pi-headline-content { + h1 { + font: { + weight: unset; + } + } + &.pi-headline-content__description { + display: block; + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-large); + } + } +} +.pi-headline-content + .pi-headline-content { + margin-top: var(--pi-spacing-x-large); +} + +.pi-background-image__animation--infinite { + animation: opacityAnimation 2.5s infinite; +} + +.pi-layout { + &.pi-layout__02-full-width { + .pi-background-image__animation--infinite { + min-height: 20rem; + } + } +} + +@keyframes opacityAnimation { + 0% { + opacity: 1; + } + 25% { + opacity: 0.7; + } + 50% { + opacity: 0.8; + } + 75% { + opacity: 0.7; + } + 100% { + opacity: 1; + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-list-item.scss b/libs/ui/src/lib/styles/common/components/_pi-list-item.scss new file mode 100644 index 0000000..386a05b --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-list-item.scss @@ -0,0 +1,177 @@ +.pi-list-item { + gap: var(--pi-spacing-medium); + // Content + .pi-list-item-content { + flex: 1 1 auto; + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-xxx-small); + &:not(.pi-chip) { + display: block; + width: 100%; + } + } + & > .pi-slider { + &.pi-slider__flex-direction--column-reverse { + margin-top: var(--pi-spacing-x-large); + } + } + } + // Is Disabled + &.is-disabled { + .pi-list-item-content { + &:not(.pi-list-item-icon__right) { + opacity: 0.5; + } + } + } + + // Border + &.pi-list-item__border--middle { + .pi-list-item-content__left { + padding-right: var(--pi-spacing-medium); + border-right: 1px solid var(--pi-color-primary-50); + } + } + + // Icon + .pi-list-item-icon { + flex: 0 0 auto; + &.pi-list-item-icon__left { + text-align: left; + } + &.pi-list-item-icon__right { + text-align: right; + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-list-item { + // Is Responsive + &.is-responsive { + flex-direction: column; + align-items: flex-start; + // Content + .pi-list-item-content { + &.pi-wrapper__flex { + flex-direction: column; + align-items: flex-start; + } + } + // Icon + .pi-list-item-icon { + &.pi-list-item-icon__right { + display: flex; + justify-content: center; + align-items: center; + min-height: 3.375rem; + width: -webkit-fill-available; + text-align: unset; + } + } + } + } + .pi-panel { + .pi-list-item { + // Is Responsive + &.is-responsive { + align-items: center; + // Type Action + &.is-stroked { + border: 1px solid; + border-color: var(--pi-color-grey-75); + border-radius: var(--pi-border-radius-large); + padding: unset; + // Content + .pi-list-item-content { + padding: var(--pi-spacing-large); + } + // Icon + .pi-list-item-icon { + &.pi-list-item-icon__right { + background-color: var(--pi-color-primary-25); + border-top: 1px solid var(--pi-color-primary-50); + border-bottom-left-radius: calc(var(--pi-border-radius-large) - 1px); + border-bottom-right-radius: calc(var(--pi-border-radius-large) - 1px); + } + } + } + } + } + } + } + // Tablet + &.is-tablet { + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + } + } + // Tablet & Desktop + &:not(.is-mobile) { + .pi-panel { + .pi-list-item { + // Responsive + &.is-responsive { + // Icon + .pi-list-item-icon { + &.pi-list-item-icon__right { + & > :not(.pi-icon-button) { + display: flex; + align-items: center; + justify-content: center; + height: 3.4375rem; + width: 3.4375rem; + margin-left: var(--pi-spacing-xxxx-large); + border: 1px solid var(--pi-color-primary-50); + border-radius: var(--pi-border-radius-large); + transition: var(--pi-animation); + &:hover { + cursor: pointer; + background-color: var(--pi-color-primary-25); + } + } + } + } + } + // Max width + // Half + &.pi-list-item__max-width--half { + .pi-list-item-content { + &.pi-list-item-content__left { + max-width: calc(50% - var(--pi-spacing-xxx-large)); + } + } + } + // Third + &.pi-list-item__max-width--third { + .pi-list-item-content { + &.pi-list-item-content__left { + max-width: calc(33.3% - var(--pi-spacing-xxx-large)); + } + } + } + } + } + // Large device + @media screen and (min-width: 62rem) { + .pi-panel__expansion { + .pi-list-item { + gap: var(--pi-spacing-x-large); + } + } + } + } + .pi-text-viewer { + article { + label, + span { + line-height: 1; + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-note.scss b/libs/ui/src/lib/styles/common/components/_pi-note.scss new file mode 100644 index 0000000..f4f29b8 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-note.scss @@ -0,0 +1,116 @@ +@mixin fixedBottomNote( + $left: 0, + $bottom: calc(var(--pi-height-footer) - var(--pi-spacing-large) + env(safe-area-inset-bottom)), + $width: calc(100% - var(--pi-spacing-medium) - var(--pi-spacing-medium)), + $min-width: auto +) { + .pi-shell { + &.pi-shell__deale-saas { + & > div { + &.has-bottom-sticky-note { + & > section { + & > main { + & > footer { + position: fixed; + left: $left; + bottom: $bottom; + z-index: 9999; + width: 100%; + display: flex; + justify-content: center; + + .pi-note { + &.is-sticky { + &.pi-note__position--bottom { + width: $width; + min-width: $min-width; + } + } + } + } + } + } + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include fixedBottomNote; + .pi-note-content__action { + button { + &.pi-button__icon--single { + height: 1.75rem; + width: 1.75rem; + } + } + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-close-button):not(.pi-hamburger):not( + .pi-button__xx-small + ):not(.pi-button__x-large):not(.pi-button__toggle) { + height: var(--pi-spacing-xx-large); + padding-top: unset; + padding-bottom: unset; + } + } + } + // Note in panel + .pi-panel { + &.pi-panel__expansion--type-form { + .pi-note { + &.pi-note__status--error { + justify-content: center; + } + + .mat-icon { + height: var(--pi-spacing-xx-large); + width: 3.2rem; + } + } + } + } + } + // Tablet + &.is-tablet { + &.is-tablet__small { + @include fixedBottomNote( + 0, + calc(env(safe-area-inset-bottom) + var(--pi-height-footer) - var(--pi-spacing-x-large)), + calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)) + ); + } + &.is-tablet__large { + @include fixedBottomNote(0, var(--pi-spacing-x-large), auto); + } + } + // Tablet & Desktop + &:not(.is-mobile) { + .pi-note-content__action { + button { + &.pi-button__icon--single { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + } + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle) { + min-width: unset; + padding-left: var(--pi-spacing-small); + padding-right: var(--pi-spacing-small); + } + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-close-button):not(.pi-hamburger):not( + .pi-button__xx-small + ):not(.pi-button__x-large):not(.pi-button__toggle) { + height: var(--pi-spacing-xx-large); + } + } + } + } + + &.is-desktop { + @include fixedBottomNote(0, var(--pi-spacing-x-large), auto, 25%); + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-panel.scss b/libs/ui/src/lib/styles/common/components/_pi-panel.scss new file mode 100644 index 0000000..ae2cdad --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-panel.scss @@ -0,0 +1,465 @@ +@mixin panelInnerPadding($padding: unset) { + &.pi-panel__inner-padding { + padding: unset; + + &.pi-panel__flex-direction--row { + & > section { + & > * { + padding-top: var(--pi-spacing-#{$padding}); + padding-bottom: var(--pi-spacing-#{$padding}); + padding-left: var(--pi-spacing-#{$padding}); + padding-right: var(--pi-spacing-#{$padding}); + &:first-child { + border-radius: var(--pi-border-radius-large) 0 0 var(--pi-border-radius-large); + } + + &:last-child { + border-radius: 0 var(--pi-border-radius-large) var(--pi-border-radius-large) 0; + } + } + } + } + + &.pi-panel__flex-direction--column { + & > section { + & > * { + padding-left: var(--pi-spacing-#{$padding}); + padding-right: var(--pi-spacing-#{$padding}); + + &:first-child { + padding-top: var(--pi-spacing-#{$padding}); + border-radius: var(--pi-border-radius-large) var(--pi-border-radius-large) 0 0; + } + &:last-child { + padding-bottom: var(--pi-spacing-#{$padding}); + border-radius: 0 0 var(--pi-border-radius-large) var(--pi-border-radius-large); + } + } + } + } + } +} + +@mixin panelContentInnerPadding($padding) { + & > *:nth-child(n + 1) { + padding: var(--pi-spacing-#{$padding}); + } +} + +@mixin panelContentColor($color) { + // Mode light + &.pi-color__mode--light { + // Light Opaque + &:not([class*='pi-color__opacity']):not([class*='pi-color__density']) { + // Solid light color + background-color: var(--pi-color-#{$color}-25); + } + } + // Mode dark + &.pi-color__mode--dark { + // Dark Opaque + &:not([class*='pi-color__opacity']):not([class*='pi-color__density']) { + background-color: var(--pi-color-#{$color}); + // Solid Dark Color & Is Stroked + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + // Density + // 10 + &.pi-color__density--10 { + // Solid light color 10 + background-color: var(--pi-color-#{$color}-10); + } + // 50 + &.pi-color__density--50 { + // Solid light color 50 + background-color: var(--pi-color-#{$color}-50); + } + // 75 + &.pi-color__density--75 { + // Solid light color 75 + background-color: var(--pi-color-#{$color}-75); + } + // 100 + &.pi-color__density--100 { + // Solid light color 100 + background-color: var(--pi-color-#{$color}-100); + } + // 200 + &.pi-color__density--200 { + // Solid light color 200 + background-color: var(--pi-color-#{$color}-200); + } + // 300 + &.pi-color__density--300 { + // Solid light color 300 + background-color: var(--pi-color-#{$color}-300); + } + // 400 + &.pi-color__density--400 { + // Solid light color 400 + background-color: var(--pi-color-#{$color}-400); + } + // 900 + &.pi-color__density--900 { + // Solid light color 900 + background-color: var(--pi-color-#{$color}-900); + } +} + +// Panel +.pi-panel { + & > section { + & > *:not(.pi-button):not(.pi-panel-content):not(.pi-chip) { + &:nth-child(n + 2) { + width: 100%; + & > .pi-text-viewer__flex-direction--row + .pi-text-viewer__flex-direction--row { + margin-top: var(--pi-spacing-xx-small); + } + } + } + .pi-panel-content__left, + .pi-panel-content__right { + flex: 1 1 auto; + height: 100%; + } + } + &.is-disabled { + & > *:nth-child(n + 1) { + opacity: 0.33; + .pi-icon__animation--clickable { + &:hover { + cursor: unset; + .mat-icon { + transform: unset; + } + } + } + } + } + // Glass + &[class*='pi-color__opacity'] { + -webkit-backdrop-filter: blur(0.75rem); + backdrop-filter: blur(0.75rem); + } + // Expansion + &.pi-panel__expansion { + .pi-panel-content { + &.pi-panel-content__top { + margin-top: -0.2rem; + } + // Center + &.pi-panel-content__center, + &.pi-panel-content__expandable { + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-x-large); + } + .pi-note + .pi-note { + margin-top: var(--pi-spacing-medium); + } + } + } + } + // NOT Expansion + &:not(.pi-panel__expansion) { + // Panel flex direction column + &.pi-panel__flex-direction--column { + & > section { + & > header { + width: -webkit-fill-available; + } + } + // Panel space between + &.pi-panel__justify--space-between { + & > section { + display: flex; + flex-direction: column; + justify-content: space-between; + } + .pi-panel-content { + // Center + &.pi-panel-content__center { + display: flex; + & > *:nth-child(n + 1) { + margin-top: unset; + } + } + } + } + } + } + // Inner padding + // XS + &.pi-panel__inner-padding--xxs { + @include panelInnerPadding(xx-small); + } + // XS + &.pi-panel__inner-padding--xs { + @include panelInnerPadding(x-small); + } + // S + &.pi-panel__inner-padding--s { + @include panelInnerPadding(small); + } + // M + &.pi-panel__inner-padding--m { + @include panelInnerPadding(medium); + } + // L + &.pi-panel__inner-padding--l { + @include panelInnerPadding(large); + } + // XL + &.pi-panel__inner-padding--xl { + @include panelInnerPadding(x-large); + } + // XXL + &.pi-panel__inner-padding--xxl { + @include panelInnerPadding(xx-large); + } + // XXXL + &.pi-panel__inner-padding--xxxl { + @include panelInnerPadding(xxx-large); + } + // XXXXL + &.pi-panel__inner-padding--xxxxl { + @include panelInnerPadding(xxxx-large); + } +} + +// Content +.pi-panel-content { + display: block; + width: -webkit-fill-available; + + & > .pi-wrapper { + width: -webkit-fill-available; + } + + // Top + &.pi-panel-content__top { + .pi-list-item { + .pi-list-item-icon__left { + .pi-chip { + &:not(.pi-chip__single-icon) { + width: var(--pi-spacing-xxxx-large); + min-width: unset; + max-width: unset; + padding: unset; + } + } + } + } + } + + // Content Color + // Primary + &.pi-color__primary { + @include panelContentColor(primary); + } + // Accent + &.pi-color__accent { + @include panelContentColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include panelContentColor(highlighted); + } + // Grey + &.pi-color__grey { + @include panelContentColor(grey); + } + + // Content Padding + &.pi-panel-content__inner-padding--xxxs { + @include panelContentInnerPadding(xxx-small); + } + &.pi-panel-content__inner-padding--xxs { + @include panelContentInnerPadding(xx-small); + } + &.pi-panel-content__inner-padding--xs { + @include panelContentInnerPadding(x-small); + } + + &.pi-panel-content__inner-padding--s { + @include panelContentInnerPadding(small); + } + + &.pi-panel-content__inner-padding--m { + @include panelContentInnerPadding(medium); + } + + &.pi-panel-content__inner-padding--l { + @include panelContentInnerPadding(large); + } + + &.pi-panel-content__inner-padding--xl { + @include panelContentInnerPadding(x-large); + } + + &.pi-panel-content__inner-padding--xxl { + @include panelContentInnerPadding(xx-large); + } + + &.pi-panel-content__inner-padding--xxxl { + @include panelContentInnerPadding(xxxl-large); + } + + &.pi-panel-content__inner-padding--xxxxl { + @include panelContentInnerPadding(xxxx-large); + } +} + +.pi-layout__05-splitted { + // Not paywall + &:not(.pi-05a-splitted-paywall-layout) { + .pi-panel { + &.pi-panel__is-clickable { + &.is-active, + &:hover { + outline: 1px solid var(--pi-color-primary); + } + } + // Padding + &.has-padding { + padding: var(--pi-spacing-x-large); + } + } + } + // Grid + // Half + &.pi-panel__grid--half { + .pi-panel-content { + width: 50%; + } + } + // Quarter + &.pi-panel__grid--quarter { + .pi-panel-content__left { + width: 25%; + } + .pi-panel-content__right { + width: 75%; + } + } + // Quarter reverse + &.pi-panel__grid--quarter-reverse { + .pi-panel-content__left { + width: 75%; + } + .pi-panel-content__right { + width: 25%; + } + } +} + +.pi-layout__main { + .pi-panel { + &:not(.is-disabled) { + box-shadow: var(--pi-box-shadow-panel); + } + &.is-disabled { + box-shadow: var(--pi-box-shadow-panel-disabled); + } + .pi-panel { + box-shadow: unset; + } + } +} + +html { + // Mobile + &.is-mobile { + // Panel + .pi-panel { + &.pi-panel__expansion { + .pi-panel-content__expandable { + button:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle) { + min-width: 100%; + } + } + } + .pi-saas-panel-subtitle { + .pi-panel-subtitle-content { + width: 100%; + } + } + } + } + // Small device + @media screen and (max-width: 62rem) { + // Panel + .pi-panel { + &.pi-panel__expansion { + .pi-panel-content__center, + .pi-panel-content__expandable { + margin-top: var(--pi-spacing-medium); + } + .pi-panel-content__top { + .pi-list-item { + flex-direction: column; + align-items: flex-start; + gap: var(--pi-spacing-xx-small); + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Panel + .pi-panel { + &.pi-panel__expansion { + .pi-panel-content__expandable { + button:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle) { + min-width: 9.25rem; + } + } + } + } + // Large device + @media screen and (min-width: 62rem) { + // Panel + .pi-panel { + &.pi-panel__expansion { + .pi-panel-content__center, + .pi-panel-content__expandable { + margin-top: var(--pi-spacing-xx-large); + } + // Data grid in expansion panels + .pi-data-grid { + &.pi-data-grid__data-direction--column { + padding-left: var(--pi-spacing-xx-small); + padding-right: var(--pi-spacing-xx-small); + } + } + } + .pi-panel-content { + // Top + &.pi-panel-content__top { + .pi-list-item { + .pi-list-item-icon__left { + padding-right: var(--pi-spacing-medium); + border-right: 1px solid var(--pi-color-primary-50); + } + } + } + // Expandable + &.pi-panel-content__expandable { + .pi-list-item { + &.pi-list-item__indent { + padding-left: calc(5rem + 1px); + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-spinner.scss b/libs/ui/src/lib/styles/common/components/_pi-spinner.scss new file mode 100644 index 0000000..cce36b7 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-spinner.scss @@ -0,0 +1,22 @@ +.pi-spinner { + // Type + // Loader + &.pi-spinner__loader { + display: flex !important; + justify-content: center; + align-items: center; + flex-direction: column; + gap: var(--pi-spacing-large); + margin-top: unset !important; + + &.pi-spinner__loader--fullscreen { + position: fixed; + z-index: 9999; + left: 0; + top: 0; + height: 100vh; + width: 100%; + background-color: var(--pi-color-primary-200-rgba); + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-tab.scss b/libs/ui/src/lib/styles/common/components/_pi-tab.scss new file mode 100644 index 0000000..0154c23 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-tab.scss @@ -0,0 +1,22 @@ +.pi-tab__content { + & > * { + display: block; + form { + margin-top: var(--pi-spacing-large); + } + & > *:nth-child(n + 2) { + display: block; + } + } +} + +.pi-tab__element { + &:not(.is-disabled) { + &:hover { + & > .pi-icon { + transition: var(--pi-animation); + transform: scale(1.2); + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-text-viewer.scss b/libs/ui/src/lib/styles/common/components/_pi-text-viewer.scss new file mode 100644 index 0000000..9c4b22c --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-text-viewer.scss @@ -0,0 +1,35 @@ +// Text group +// colors +$color1: #799ef0; +$color2: #99b3ec; +$color3: #afc1ea; +$color4: #b9c8e9; +$color5: #dbdfe7; +$colors: $color1, $color2, $color3, $color4, $color5; +.pi-text-group { + &.pi-text-group__value-indicator { + width: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + @for $i from 1 through length($colors) { + .pi-text-viewer { + display: flex; + gap: var(--pi-spacing-xx-small); + &:nth-child(#{length($colors)}n + #{$i}) { + &::before { + content: ''; + margin-bottom: var(--pi-spacing-xxxx-small); + width: var(--pi-spacing-small); + height: var(--pi-spacing-small); + background-color: nth($colors, $i); + border-radius: 50%; + } + } + } + } + .pi-text-viewer + .pi-text-viewer { + margin-top: var(--pi-spacing-small); + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/_pi-text.scss b/libs/ui/src/lib/styles/common/components/_pi-text.scss new file mode 100644 index 0000000..1ed03b4 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/_pi-text.scss @@ -0,0 +1,517 @@ +@mixin fontSize($font-size) { + font: { + size: var(--pi-font-size-#{$font-size}); + } +} + +.pi-text__centered { + &:is(h1, h2, h3, h4, h5, span, p, i, figcaption, label, small, legend) { + display: block; + width: 100%; + text-align: center; + } + & > *:nth-child(n + 1) { + &:is(h1, h2, h3, h4, h5, p, i, figcaption, label, small, legend) { + display: block; + text-align: center; + } + } +} + +* { + &:is(h1) { + line-height: 116.5%; + font: { + weight: 700; + } + } + &:is(h2) { + line-height: 125%; + } + &:is(h3, h4, h5) { + line-height: 130%; + } + & > *:nth-child(n + 1) { + &:is(h1) { + line-height: 116.5%; + } + &:is(h2) { + line-height: 125%; + } + } + & > *:nth-child(n + 1) { + &:is(h3, h4, h5) { + line-height: 130%; + } + } +} + +.pi-text__is-covered { + &:is(h1, h2, h3, h4, h5, span, p, i, figcaption, label, small, legend) { + position: relative; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + &::before { + content: ''; + display: block; + width: 100%; + height: 100%; + background: var(--pi-color-grey-75); + position: absolute; + top: 0; + border-radius: var(--pi-border-radius-xx-small); + } + } +} + +.pi-text__group { + & > * + * { + margin-left: var(--pi-spacing-xxx-small); + } +} + +.pi-shell__deale-saas { + h3 { + font: { + size: var(--pi-font-size-medium); + } + } + h4 { + font: { + size: var(--pi-font-size-small); + } + } + h5 { + font: { + size: var(--pi-font-size-small); + } + } + p { + font: { + size: var(--pi-font-size-small); + } + } + figcaption { + font: { + size: var(--pi-font-size-small); + } + } + strong { + font-family: 'Soehne', Helvetica, Arial, sans-serif; + font-weight: 700; + } + i { + font: { + size: var(--pi-font-size-small); + } + } + label { + font: { + size: var(--pi-font-size-small); + } + } + + small { + font: { + size: var(--pi-font-size-xx-small); + } + } + + // Mobile + @media screen and (max-width: 40rem) { + h1 { + font: { + size: var(--pi-font-size-x-large); + } + } + h2 { + font: { + size: var(--pi-font-size-large); + } + } + } + // Tablet & Desktop + @media screen and (min-width: 40rem) { + h1 { + font: { + size: var(--pi-font-size-x-large); + } + } + h2 { + font: { + size: var(--pi-font-size-large); + } + } + } +} + +mark { + all: unset; + text-decoration: none; + position: relative; + &:after { + content: ''; + position: absolute; + top: 100%; + left: 0; + height: 0.2rem; + width: 100%; + border-radius: 0.2rem; + } + &.pi-color__primary { + &:after { + background: var(--pi-color-primary); + } + } + &.pi-color__accent, + &:not(.pi-color__primary):not(.pi-color__warn) { + &:after { + background: var(--pi-color-accent); + } + } + &.pi-color__warn { + &:after { + background: var(--pi-color-warn); + } + } +} + +@mixin textColor($color) { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}); + } + // (50) + &.pi-color__density--50 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-50); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-50); + } + } + // (100) + &.pi-color__density--100 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-100); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-100); + } + } + // (200) + &.pi-color__density--200 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-200); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-200); + } + } + // (300) + &.pi-color__density--300 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-300); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-300); + } + } + // (400) + &.pi-color__density--400 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-400); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-400); + } + } + // (900) + &.pi-color__density--900 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-900); + } + &:not(button):not([class*='content']) + > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-#{$color}-900); + } + } +} + +// Primary +.pi-color__primary { + @include textColor(primary); +} + +// Accent +.pi-color__accent { + @include textColor(accent); +} + +// Warn +.pi-color__warn { + @include textColor(warn); +} + +// Success +.pi-color__success { + @include textColor(success); +} + +// Highlighted +.pi-color__highlighted { + @include textColor(highlighted); +} + +// Grey +.pi-color__grey { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey); + } + // Grey (50) + &.pi-color__density--50 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-75); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-75); + } + } + // Grey (100) + &.pi-color__density--100 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-100); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-100); + } + } + // Grey (200) + &.pi-color__density--200 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-200); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-200); + } + } + // Grey (300) + &.pi-color__density--300 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-300); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-300); + } + } + // Grey (400) + &.pi-color__density--400 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-300); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-300); + } + } + // Grey (900) + &.pi-color__density--900 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + } +} + +// White +.pi-color__white { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white); + } + // White (200) + &.pi-color__density--200 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white-rgba-06); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white-rgba-06); + } + } + // White (900) + &.pi-color__density--900 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-white); + } + } +} + +// Black +.pi-color__black { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + // Black (200) + &.pi-color__density--200 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + } + // Black (900) + &.pi-color__density--900 { + &:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + &:not(button) > *:is(h1, h2, h3, h4, h5, p, span, i, strong, label, figcaption, legend, small) { + color: var(--pi-color-grey-900); + } + } +} + +// Blockquote +blockquote { + display: flex; + // Mobile + @media screen and (max-width: 40rem) { + gap: var(--pi-spacing-xx-small); + } + // Tablet & Desktop + @media screen and (min-width: 40rem) { + gap: var(--pi-spacing-x-large); + } + &::before { + content: open-quote; + } + + &::after { + content: close-quote; + } + &::before, + &::after { + position: relative; + margin-top: -1rem; + font: { + size: 300%; + } + } + // Primary + &.pi-color__primary { + &::before, + &::after { + color: var(--pi-color-primary); + } + } + // Accent + &.pi-color__accent { + &::before, + &::after { + color: var(--pi-color-accent); + } + } + // Warn + &.pi-color-warn { + &::before, + &::after { + color: var(--pi-color-warn); + } + } +} + +.pi-text { + // xxs - 0.625rem - 10px + &.pi-text__size--xx-small { + @include fontSize(xx-small); + } + // xs - 0.75rem - 12px + &.pi-text__size--x-small { + @include fontSize(x-small); + } + // s - 0.9375rem - 15px + &.pi-text__size--small { + @include fontSize(small); + } + // m - 1.125rem - 18px + &.pi-text__size--medium { + @include fontSize(medium); + } + // l - 1.375rem - 22px + &.pi-text__size--large { + @include fontSize(large); + } + // xl - 1.6875rem - 27px + &.pi-text__size--x-large { + @include fontSize(x-large); + } + // xxl - 2.375rem - 38px + &.pi-text__size--xx-large { + @include fontSize(xx-large); + } + // xxxl - 2.875rem - 46px + &.pi-text__size--xxx-large { + @include fontSize(xxx-large); + } +} + +// Form field labels +.pi-label { + display: flex; + align-items: center; + gap: var(--pi-spacing-xxx-small); +} + +@mixin textCenteredMaxWidth($max-width) { + .pi-domain-component { + .pi-text__centered { + &:not(.pi-text__size--x-small) { + max-width: $max-width; + } + } + } +} + +html { + &.is-desktop { + .pi-domain-component { + .pi-text__centered { + margin: 0 auto; + } + } + } + &.is-desktop__small { + @include textCenteredMaxWidth(80%); + } + &.is-desktop__medium { + @include textCenteredMaxWidth(75%); + } + &.is-desktop__large { + @include textCenteredMaxWidth(66.66%); + } +} + +.pi-text__letter-spacing { + letter-spacing: var(--pi-spacing-xx-small); +} diff --git a/libs/ui/src/lib/styles/common/components/index.scss b/libs/ui/src/lib/styles/common/components/index.scss new file mode 100644 index 0000000..6129ced --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/index.scss @@ -0,0 +1,15 @@ +@forward 'pi-accordion'; +@forward 'pi-card'; +@forward 'pi-divider'; +@forward 'pi-headline'; +@forward 'pi-list-item'; +@forward 'pi-note'; +@forward 'pi-panel'; +@forward 'pi-spinner'; +@forward 'pi-tab'; +@forward 'pi-text-viewer'; +@forward 'pi-text'; + +@use 'pi-button'; +@use 'pi-data-grid'; +@use 'pi-field'; diff --git a/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-private.scss b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-private.scss new file mode 100644 index 0000000..37103c0 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-private.scss @@ -0,0 +1,284 @@ +@mixin privateButtonsSizes { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__xx-small):not(.pi-button__x-large):not( + .pi-button__toggle + ):not(.pi-button__trimmed):not(.ch2-btn):not(.pi-button__icon--single) { + &:not(.pi-button__x-small):not(.pi-button__xx-small) { + height: var(--pi-spacing-xxx-large); + } + } + + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__toggle):not(.pi-button__trimmed):not( + .ch2-btn + ):not(.pi-button__icon--single) { + &.pi-button__x-small { + height: var(--pi-spacing-xx-large); + } + } + + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + // Sizes + // X small + &.pi-button { + &:not(.pi-button__icon):not(.pi-button__x-small):not(.pi-button__small):not( + .pi-button__large + ):not(.pi-button__large):not(.pi-button__trimed):not(.pi-button__xx-small):not( + .pi-button__trimmed + ) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xxx-large); + } + } + } +} + +// Fill +@mixin privateButtonFillColor($color) { + // Default color + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not(.pi-color__white) { + background-color: var(--pi-color-#{$color}); + color: var(--pi-color-white); + &:disabled { + background-color: var(--pi-color-#{$color}-200); + } + &:hover { + &:enabled { + background-color: var(--pi-color-#{$color}-900); + } + } + } + // Default color & White + &.pi-button__white { + color: var(--pi-color-#{$color}); + &:hover { + &:enabled { + background-color: var(--pi-color-#{$color}); + } + } + } +} + +// Ghost +@mixin privateButtonGhostColor($color) { + color: var(--pi-color-#{$color}); + &:hover { + &:enabled { + color: var(--pi-color-#{$color}-900); + } + } + &:disabled { + color: var(--pi-color-#{$color}-200); + } +} + +// Outline +@mixin privateButtonOutlineColor($color) { + background-color: transparent; + box-sizing: border-box; + transition: all 0.16s ease-in; + border: 1px solid; + color: var(--pi-color-#{$color}); + &:hover { + &:enabled { + color: var(--pi-color-white); + border-color: var(--pi-color-#{$color}); + background-color: var(--pi-color-#{$color}); + } + } + &:disabled { + color: var(--pi-color-#{$color}-200); + } + // Is Active + &.is-active { + color: var(--pi-color-white); + border-color: var(--pi-color-#{$color}); + background-color: var(--pi-color-#{$color}); + } +} + +@mixin privateButtonsColors { + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + // Color + // White + &.pi-color__white { + background-color: var(--pi-color-white); + &:disabled { + background-color: rgba($color: white, $alpha: 0.6); + } + &:hover { + color: var(--pi-color-white); + background-color: transparent; + border: 2px solid var(--pi-color-white); + &:enabled { + background-color: transparent; + } + } + // White default + &.pi-color__white:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__highlighted + ):not(.pi-color__white) { + &:not(:hover) { + color: var(--pi-color-primary); + } + } + } + + // Fill + // Primary + &.pi-color__primary { + @include privateButtonFillColor(primary); + } + // Accent + &.pi-color__accent { + @include privateButtonFillColor(accent); + } + // Warn + &.pi-color__warn { + @include privateButtonFillColor(warn); + } + + // Highlighted + &.pi-color__highlighted { + @include privateButtonFillColor(highlighted); + } + + // Ghost + &.pi-button__appearance--ghost { + background-color: transparent; + text-decoration: underline; + // Ghost & Primary + &.pi-color__primary, + &:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__white + ):not(.pi-color__black):not(.pi-color__highlighted):not(.pi-color__grey) { + @include privateButtonGhostColor(primary); + } + // Ghost & Accent + &.pi-color__accent { + @include privateButtonGhostColor(accent); + } + // Ghost & Warn + &.pi-color__warn { + @include privateButtonGhostColor(warn); + } + // Ghost & Highlighted + &.pi-color__highlighted { + @include privateButtonGhostColor(Highlighted); + } + // Ghost & White + &.pi-color__white { + color: var(--pi-color-white); + &:hover { + border: unset; + &:enabled { + color: var(--pi-color-white); + text-decoration: underline; + } + } + &:disabled { + color: rgba($color: white, $alpha: 0.6); + } + } + // Ghost & Black + &.pi-color__black { + color: var(--pi-color-grey-900); + &:hover { + &:enabled { + color: var(--pi-color-grey-900); + text-decoration: underline; + } + } + &:disabled { + color: rgba($color: #3d3d3d, $alpha: 0.6); + } + } + // Ghost & Grey + &.pi-color__grey { + color: var(--pi-color-grey-300); + &:hover { + &:enabled { + color: var(--pi-color-grey-400); + text-decoration: underline; + } + } + &:disabled { + color: var(--pi-color-grey-100); + } + } + } + + // Outline + &.pi-button__appearance--outline { + // Outline & Primary + &.pi-color__primary, + &:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__white + ):not(.pi-color__highlighted):not(.pi-color__black) { + @include privateButtonOutlineColor(primary); + } + // Outline & Accent + &.pi-color__accent { + @include privateButtonOutlineColor(accent); + } + // Outline & Warn + &.pi-color__warn { + @include privateButtonOutlineColor(warn); + } + + // Outline & Highlighted + &.pi-color__highlighted { + @include privateButtonOutlineColor(highlighted); + } + + &.pi-color__black { + @include privateButtonOutlineColor(grey); + } + // Outline & White + &.pi-color__white { + color: var(--pi-color-white); + &:hover { + &:enabled { + color: var(--pi-color-primary); + background-color: var(--pi-color-white); + border-color: var(--pi-color-white); + } + } + &:disabled { + color: rgba($color: #ffffff, $alpha: 0.6); + } + // Is Active + &.is-active { + color: var(--pi-color-primary); + background-color: var(--pi-color-white); + border-color: var(--pi-color-white); + } + } + } + } +} + +.pi-shell { + &.pi-shell__deale-saas { + .is-private { + button { + // Colors + @include privateButtonsColors; + // Sizes + @include privateButtonsSizes; + } + } + } +} + +.cdk-overlay-container { + button { + // Colors + @include privateButtonsColors; + // Sizes + @include privateButtonsSizes; + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-public.scss b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-public.scss new file mode 100644 index 0000000..a0d99f0 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button-public.scss @@ -0,0 +1,737 @@ +@mixin publicGhostButtonsColors( + $background-image: linear-gradient(30deg, var(--pi-color-primary), var(--pi-color-primary-900)), + $color: primary +) { + color: var(--pi-color-#{$color}); + background-image: $background-image; + &:hover { + &:enabled { + text-decoration: underline; + } + } +} + +@mixin publicButtonsBackgroundColors( + $color-disabled: var(--pi-color-primary-200), + $color-enabled: var(--pi-color-primary-900) +) { + background-size: 100%; + background-repeat: repeat; + color: var(--pi-color-white); + &:disabled { + background-color: $color-disabled; + } + &:hover { + &:enabled { + background-color: $color-enabled; + } + } +} + +@mixin publicFillButtonsGradients($color) { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not(.pi-color__white) { + background-size: 100%; + background-repeat: repeat; + color: var(--pi-color-white); + // Light + &.pi-color__mode--light { + &:not(:disabled) { + background-image: var(--pi-gradient-#{$color}-light); + } + &:disabled { + color: var(--pi-color-grey-200); + background-image: var(--pi-gradient-light-disabled); + } + } + // Dark + &:not(.pi-color__mode--light) { + &:not(:disabled) { + background-image: var(--pi-gradient-#{$color}-dark); + } + &:disabled { + background-image: var(--pi-gradient-dark-disabled); + } + } + } + // Primary & White + &.pi-button__white { + color: var(--pi-color-#{$color}); + &:hover { + &:enabled { + background-color: var(--pi-color-#{$color}); + } + } + } +} + +// Sizes +@mixin publicButtonsSizes { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__xx-small):not(.pi-button__x-large):not( + .pi-button__toggle + ):not(.pi-button__trimmed):not(.ch2-btn):not(.pi-button__icon--single):not( + .pi-button__type--menu-item + ) { + height: 2.875rem; + &.pi-button__icon--single { + width: var(--pi-spacing-xxxx-large); + } + } + + &:not(.pi-button__toggle):not(.pi-button__x-large):not(.pi-button__trimmed):not( + .pi-button__type--menu-item + ) { + height: 2.875rem; + &.pi-button__icon--single { + width: var(--pi-spacing-xxxx-large); + } + } + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + // Sizes + // X small + &.pi-button { + &:not(.pi-button__icon--single):not(.pi-button__x-small):not(.pi-button__small):not( + .pi-button__large + ):not(.pi-button__trimed):not(.pi-button__xx-small):not(.pi-button__trimmed) { + min-height: 2.875rem; + } + &:not(.pi-button__icon):not(.pi-button__x-small):not(.pi-button__small):not( + .pi-button__large + ):not(.pi-button__large):not(.pi-button__trimed):not(.pi-button__xx-small):not( + .pi-button__trimmed + ) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xxx-large); + } + } + } +} + +// Shine hover effect +@mixin publicButtonsShineHoverEffect { + // Hover shine effect + &:not(.pi-button__appearance--ghost):not(.pi-button__type--menu-item):not(.pi-color__black) { + position: relative; + overflow: hidden; + &::after { + content: ''; + position: absolute; + top: -50%; + right: 0; + bottom: -50%; + left: -50%; + transform: rotateZ(75deg) translate(-5rem, 7.5rem); + &:hover { + cursor: pointer; + } + } + &:hover, + &:focus { + &::after { + animation: sheen 0.75s forwards; + } + } + + &:active { + transform: translate(0, -0.125rem); + } + + // Mode Light + &.pi-color__mode--light { + &::after { + background: linear-gradient( + to bottom, + rgba(229, 172, 142, 0), + rgba(255, 255, 255, 0.25) 50%, + rgba(229, 172, 142, 0) + ); + } + &:active { + box-shadow: 0px 0px 0px white; + -webkit-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + } + } + // Mode Dark + &.pi-color__mode--dark { + &::after { + background: linear-gradient( + to bottom, + rgba(229, 172, 142, 0), + rgba(255, 255, 255, 0.125) 50%, + rgba(229, 172, 142, 0) + ); + } + &:active { + box-shadow: 0px 0px 0px white; + -webkit-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.125); + -moz-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.125); + box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.125); + } + } + &:focus { + outline: 0; + } + + @keyframes sheen { + 100% { + transform: rotateZ(30deg) translate(1rem, -9rem); + } + } + } +} + +// Colors +@mixin publicButtonsColors { + position: relative; + overflow: hidden; + + // Shine hover effect + &:not(:disabled) { + @include publicButtonsShineHoverEffect; + } + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + font: { + weight: 600; + } + // Color + // White + // TODO: Add customized white 25 density color gradient for backgrounds + // TODO: Add text color gradients + &.pi-color__white { + background-image: var(--pi-gradient-white-rgba); + &:disabled { + background-color: rgba($color: white, $alpha: 0.6); + } + &:hover { + color: var(--pi-color-primary-900); + background-color: transparent; + border: 1px solid var(--pi-color-primary-100); + &:enabled { + background-color: transparent; + } + } + // White default + &.pi-color__white:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__highlighted + ) { + &:not(:hover) { + color: var(--pi-color-primary); + } + } + } + // Primary + &.pi-color__primary { + @include publicFillButtonsGradients(primary); + } + // Accent + &.pi-color__accent { + @include publicFillButtonsGradients(accent); + } + // Highlighted + &.pi-color__highlighted { + @include publicFillButtonsGradients(highlighted); + } + + // Black + &.pi-color__black { + background-image: linear-gradient( + 300deg, + hsl(0deg 0% 62%) 0%, + hsl(344deg 0% 57%) 21%, + hsl(344deg 0% 53%) 30%, + hsl(344deg 0% 49%) 39%, + hsl(344deg 0% 44%) 46%, + hsl(344deg 0% 40%) 54%, + hsl(344deg 0% 36%) 61%, + hsl(344deg 0% 32%) 69%, + hsl(344deg 0% 28%) 79%, + hsl(0deg 0% 24%) 100% + ); + color: var(--pi-color-white); + &:disabled { + background-color: rgba($color: #3d3d3d, $alpha: 0.6); + } + &:hover { + &:enabled { + background-color: var(--pi-color-black); + } + } + // White default + &.pi-color__white:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__highlighted + ) { + &:not(:hover) { + color: var(--pi-color-black); + } + } + } + + // Warn + &.pi-color__warn { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + @include publicButtonsBackgroundColors(var(--pi-color-warn-200), var(--pi-color-warn-900)); + } + // Mode + // Default + &:not(.pi-color__mode--light) { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + background-image: linear-gradient( + 300deg, + hsl(0deg 74% 71%) 0%, + hsl(0deg 71% 69%) 21%, + hsl(0deg 68% 66%) 30%, + hsl(1deg 65% 64%) 39%, + hsl(1deg 62% 61%) 46%, + hsl(1deg 60% 59%) 54%, + hsl(1deg 58% 56%) 61%, + hsl(1deg 56% 54%) 69%, + hsl(0deg 54% 51%) 79%, + hsl(0deg 56% 49%) 100% + ); + } + } + // Light + &.pi-color__mode--light { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + background-image: linear-gradient( + 300deg, + hsl(350deg 100% 93%) 0%, + hsl(351deg 95% 91%) 21%, + hsl(352deg 92% 89%) 30%, + hsl(353deg 89% 86%) 39%, + hsl(354deg 87% 84%) 46%, + hsl(356deg 84% 82%) 54%, + hsl(357deg 81% 79%) 61%, + hsl(358deg 79% 77%) 69%, + hsl(359deg 76% 74%) 79%, + hsl(0deg 74% 71%) 100% + ); + } + } + // Warn & White + &.pi-color__white { + &:not(:hover) { + color: var(--pi-color-warn); + } + } + } + + // Success + &.pi-color__success { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + @include publicButtonsBackgroundColors( + var(--pi-color-success-200), + var(--pi-color-success-900) + ); + } + // Mode + // Default + &:not(.pi-color__mode--light) { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + background-image: linear-gradient( + 300deg, + hsl(155deg 48% 79%) 0%, + hsl(154deg 45% 74%) 21%, + hsl(153deg 43% 69%) 30%, + hsl(152deg 42% 64%) 39%, + hsl(151deg 40% 60%) 46%, + hsl(151deg 39% 55%) 54%, + hsl(150deg 39% 50%) 61%, + hsl(150deg 48% 45%) 69%, + hsl(151deg 61% 39%) 79%, + hsl(155deg 100% 30%) 100% + ); + } + } + // Light + &.pi-color__mode--light { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-color__white + ) { + background-image: linear-gradient( + 300deg, + hsl(155deg 62% 91%) 0%, + hsl(155deg 60% 88%) 21%, + hsl(155deg 59% 84%) 30%, + hsl(155deg 58% 81%) 39%, + hsl(155deg 57% 78%) 46%, + hsl(155deg 57% 75%) 54%, + hsl(155deg 56% 72%) 61%, + hsl(155deg 56% 68%) 69%, + hsl(155deg 56% 65%) 79%, + hsl(155deg 56% 61%) 100% + ); + } + } + // Highlighted & White + &.pi-color__white { + &:not(:hover) { + color: var(--pi-color-success); + } + } + } + + // Ghost + &.pi-button__appearance--ghost { + background-color: transparent; + text-decoration: underline; + background-size: 100%; + background-repeat: repeat; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-background-clip: text; + -moz-text-fill-color: transparent; + + // Ghost & Primary + &.pi-color__primary, + &:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__white + ):not(.pi-color__black):not(.pi-color__highlighted):not(.pi-color__grey):not( + .pi-color__success + ) { + @include publicGhostButtonsColors; + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-primary-100), var(--pi-color-primary-200)), + primary-200 + ); + } + } + // Ghost & Accent + &.pi-color__accent { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-accent), var(--pi-color-accent-900)), + accent + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-accent-100), var(--pi-color-accent-100)), + accent-200 + ); + } + } + // Ghost & Warn + &.pi-color__warn { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-warn), var(--pi-color-warn-900)), + warn + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-warn-100), var(--pi-color-warn-200)), + warn-200 + ); + } + } + // Ghost & White + &.pi-color__white { + @include publicGhostButtonsColors; + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-white), rgba($color: white, $alpha: 0.6)), + white + ); + } + } + // Ghost & Black + &.pi-color__black { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-black), var(--pi-color-grey-900)), + grey + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-black), rgba($color: #3d3d3d, $alpha: 0.6)), + grey-200 + ); + } + } + // Ghost & Highlighted + &.pi-color__highlighted { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-highlighted), var(--pi-color-highlighted-900)), + highlighted + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient( + 30deg, + var(--pi-color-highlighted-100), + var(--pi-color-highlighted-200) + ), + highlighted-200 + ); + } + } + // Ghost & Grey + &.pi-color__grey { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-grey-100), var(--pi-color-grey-400)), + grey-400 + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-grey-25), var(--pi-color-grey-75)), + grey-50 + ); + } + } + // Ghost & Success + &.pi-color__success { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-success), var(--pi-color-success-900)), + success + ); + &:disabled { + @include publicGhostButtonsColors( + linear-gradient(30deg, var(--pi-color-success-100), var(--pi-color-success-200)), + success-200 + ); + } + } + } + + // Outline + &.pi-button__appearance--outline { + background-color: transparent; + box-sizing: border-box; + transition: all 0.16s ease-in; + border: 1px solid; + + // Outline & Primary + &.pi-color__primary, + &:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__white + ):not(.pi-color__highlighted) { + color: var(--pi-color-primary); + &:hover { + &:enabled { + color: var(--pi-color-white); + border-color: var(--pi-color-primary); + background-color: var(--pi-color-primary); + } + } + &:disabled { + color: var(--pi-color-primary-200); + } + // Is Active + &.is-active { + color: var(--pi-color-white); + border-color: var(--pi-color-primary); + background-color: var(--pi-color-primary); + } + } + // Outline & Accent + &.pi-color__accent { + color: var(--pi-color-accent); + &:hover { + &:enabled { + color: var(--pi-color-white); + border-color: var(--pi-color-accent); + background-color: var(--pi-color-accent); + } + } + &:disabled { + color: var(--pi-color-accent-200); + border-color: var(--pi-color-accent-200); + } + // Is Active + &.is-active { + color: var(--pi-color-white); + background-color: var(--pi-color-accent); + border-color: var(--pi-color-accent); + } + } + // Outline & Warn + &.pi-color__warn { + color: var(--pi-color-warn); + &:hover { + &:enabled { + color: var(--pi-color-white); + background-color: var(--pi-color-warn); + border-color: var(--pi-color-warn); + } + } + &:disabled { + color: var(--pi-color-warn-200); + } + // Is Active + &.is-active { + color: var(--pi-color-white); + background-color: var(--pi-color-warn); + border-color: var(--pi-color-warn); + } + } + + // Outline & Highlighted + &.pi-color__highlighted { + color: var(--pi-color-highlighted); + &:hover { + &:enabled { + color: var(--pi-color-white); + background-color: var(--pi-color-highlighted); + border-color: var(--pi-color-highlighted); + } + } + &:disabled { + color: var(--pi-color-highlighted-200); + } + // Is Active + &.is-active { + color: var(--pi-color-white); + background-color: var(--pi-color-highlighted); + border-color: var(--pi-color-highlighted); + } + } + + // Outline & White + &.pi-color__white { + color: var(--pi-color-white); + &:hover { + &:enabled { + color: var(--pi-color-primary); + background-color: var(--pi-color-white); + border-color: var(--pi-color-white); + } + } + &:disabled { + color: rgba($color: #ffffff, $alpha: 0.6); + } + // Is Active + &.is-active { + color: var(--pi-color-primary); + background-color: var(--pi-color-white); + border-color: var(--pi-color-white); + } + } + } + } +} + +@mixin publicFixedButtonSideSpacing( + $top-spacing: calc(var(--pi-spacing-medium) + env(safe-area-inset-top)), + $side-spacing: var(--pi-spacing-medium), + $bottom-left-spacing: calc(var(--pi-spacing-medium) + env(safe-area-inset-bottom)), + $bottom-right-spacing: calc(5.75rem + var(--pi-spacing-large) + env(safe-area-inset-bottom)) +) { + .pi-shell { + &.pi-shell__deale-website, + &.is-public { + // Fixed + button, + .pi-icon-button, + .pi-action-button { + &.is-fixed { + position: fixed; + z-index: 999; + &.is-fixed--top-left, + &.is-fixed--top-right { + top: $top-spacing; + right: $side-spacing; + } + &.is-fixed--bottom-left { + bottom: $bottom-left-spacing; + left: $side-spacing; + } + &.is-fixed--bottom-right { + bottom: $bottom-right-spacing; + right: $side-spacing; + } + } + } + } + } +} + +.pi-shell { + &.pi-shell__deale-website, + .is-public { + button { + // Colors + @include publicButtonsColors; + // Sizes + @include publicButtonsSizes; + } + // Common + button { + &.pi-button { + // X Large + &.pi-button__x-large { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xxx-large); + border-radius: 1.4rem; + // Mobile + @media screen and (max-width: 40rem) { + height: 3.125rem; + min-width: 10.6875rem; + font: { + size: var(--pi-font-size-small); + } + } + // Tablet & Desktop + @media screen and (min-width: 40rem) { + height: 3.75rem; + min-width: 11.25rem; + font: { + size: var(--pi-font-size-medium); + } + } + } + } + } + } +} +html, +.cdk-overlay-container { + // Mobile + &.is-mobile { + @include publicFixedButtonSideSpacing( + calc(var(--pi-spacing-medium) + env(safe-area-inset-top)), + var(--pi-spacing-medium), + calc(var(--pi-spacing-medium) + env(safe-area-inset-bottom)), + calc(5.75rem + var(--pi-spacing-large) + env(safe-area-inset-bottom)) + ); + } + &.is-tablet { + &.is-tablet__small { + @include publicFixedButtonSideSpacing( + calc(var(--pi-spacing-x-large) + env(safe-area-inset-top)), + var(--pi-spacing-x-large), + calc(var(--pi-spacing-x-large) + env(safe-area-inset-bottom)), + calc(3.75rem + var(--pi-spacing-large) + env(safe-area-inset-bottom)) + ); + } + &.is-tablet__large { + @include publicFixedButtonSideSpacing( + calc(var(--pi-spacing-x-large) + env(safe-area-inset-top)), + var(--pi-spacing-x-large), + calc(var(--pi-spacing-x-large) + env(safe-area-inset-bottom)), + calc(5.75rem + var(--pi-spacing-large) + env(safe-area-inset-bottom)) + ); + } + } + // Tablet & Desktop + &.is-desktop { + @include publicFixedButtonSideSpacing( + calc(var(--pi-spacing-x-large) + env(safe-area-inset-top)), + var(--pi-spacing-x-large), + calc(var(--pi-spacing-x-large) + env(safe-area-inset-bottom)), + calc(5.75rem + var(--pi-spacing-large) + env(safe-area-inset-bottom)) + ); + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-button/_pi-button.scss b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button.scss new file mode 100644 index 0000000..e3f8c46 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-button/_pi-button.scss @@ -0,0 +1,204 @@ +@mixin defaultButtonsProperties { + line-height: 100%; + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + } + &:hover { + &:not(:disabled):not(.pi-button__toggle) { + cursor: pointer; + } + } + &.is-active { + cursor: default; + } + + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + border: none; + + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__xx-small):not( + .pi-button__x-large + ):not(.pi-button__toggle):not(.pi-button__trimmed):not(.ch2-btn):not( + .pi-button__icon--single + ):not(.pi-button__x-small) { + padding-top: var(--pi-spacing-xx-small); + padding-bottom: var(--pi-spacing-xx-small); + } + &:not(.pi-button__x-large):not(.pi-button__trimmed):not(.pi-button__toggle) { + &:not(.pi-button__x-small) { + border-radius: var(--pi-border-radius-medium); + -webkit-border-radius: var(--pi-border-radius-medium); + -moz-border-radius: var(--pi-border-radius-medium); + } + &.pi-button__x-small { + border-radius: 0.75rem; + -webkit-border-radius: 0.75rem; + -moz-border-radius: 0.75rem; + } + } + + &:not(.pi-button__icon--single):not(.pi-button__xx-small):not(.pi-button__trimmed):not( + .pi-button__toggle + ):not(.ch2-btn) { + &:not(.pi-button__x-small) { + min-width: 5.5rem; + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + &.pi-button__x-small { + min-width: 4.5rem; + padding-left: var(--pi-spacing-xx-small); + padding-right: var(--pi-spacing-xx-small); + } + } + + &:not(.pi-button__appearance--outline) { + transition: all 0.25s ease-in-out; + } + } +} + +@mixin buttonsSizes { + // Default + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ) { + &.pi-button__x-small { + font: { + size: 0.8125rem; + } + &:not(.pi-button__trimmed) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-medium); + } + } + &.pi-button__trimmed { + padding: 0; + } + &.pi-button__xx-small { + display: flex; + justify-content: center; + align-items: center; + gap: var(--pi-spacing-x-small); + height: 1.375rem; + line-height: 100%; + width: -moz-fit-content; + width: fit-content; + min-width: 4.5rem; + max-width: 12.5rem; + padding: 0 var(--pi-spacing-small); + -webkit-border-radius: var(--pi-border-radius-small); + -moz-border-radius: var(--pi-border-radius-small); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font: { + size: 0.75rem; + } + + font: { + size: 0.8125rem; + } + } + &.pi-button__trimed { + min-width: unset; + padding: var(--pi-spacing-xxxx-small) 0; + } + &:not(.pi-button__trimmed):not(.pi-button__icon--single) { + // Small + &.pi-button__small { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-small); + } + // Medium + &.pi-button__medium { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xxx-large); + } + // Large + &.pi-button__large { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xxx-large); + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + min-width: 10.6875rem; + } + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + min-width: 11.25rem; + } + } + } + + // W 100 + &.pi-button__w100 { + width: 100%; + } + } +} +html, +.cdk-overlay-container { + button { + // Default properties + @include defaultButtonsProperties; + + // Sizes + @include buttonsSizes; + } +} + +.pi-filter-button, +.pi-expand-button { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + &.is-open { + .pi-big-icon, + figure { + transform: rotate(180deg); + } + } + } +} + +// Close button in modals +.pi-modal { + .pi-close-button { + &::after, + &::before { + width: var(--pi-spacing-x-large); + } + } +} + +.pi-layout { + &.pi-layout__05-splitted { + .pi-wrapper__action { + &.pi-wrapper__flex-direction--column { + .pi-button { + display: flex; + align-items: center; + justify-content: center; + margin: auto; + } + } + } + } +} +.cdk-overlay-container, +html { + &.is-mobile { + .pi-icon-button { + &.is-responsive { + & > button { + // Text + &.has-text { + padding: unset; + min-width: unset; + width: var(--pi-spacing-xxx-large); + & > span { + display: none; + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-button/index.scss b/libs/ui/src/lib/styles/common/components/pi-button/index.scss new file mode 100644 index 0000000..cb9779c --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-button/index.scss @@ -0,0 +1,3 @@ +@forward 'pi-button-private'; +@forward 'pi-button-public'; +@forward 'pi-button'; diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/_appearance.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/_appearance.scss new file mode 100644 index 0000000..10e7b5c --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/_appearance.scss @@ -0,0 +1,75 @@ +/** +* APPEARANCE: +* +* 1. Fill: +* +* Similar look and feel to the Form Fields Fill Appearance. +* Data Grid with inner stroke between each grid item. +* +**/ +@mixin dataGridAppearanceFill() { +} + +/** +* +* 2. Outline: +* +* A Data Grid with outer border and a inner stroke between each grid item appart from the last one. +* +**/ + +@mixin dataGridAppearanceOutline() { + background-color: var(--pi-color-white); + + &.pi-data-grid__appearance--outline { + // Inner Stroke between grid items + // Data Direction: + // Row + &.pi-data-grid__data-direction--row { + .pi-grid-item { + &:not(:last-of-type) { + border-bottom: 1px solid; + border-color: var(--pi-color-grey-50); + } + } + } + + // Column + &.pi-data-grid__data-direction--column { + .pi-grid-item { + .pi-cell { + &:not(.pi-cell__type--sum) { + border-bottom: 1px solid; + border-color: var(--pi-color-grey-50); + } + } + } + } + + // Outline Appearance in mode === Grid + // Card Type: List + &.pi-data-grid__card-type--list { + .pi-cell { + &:not(.pi-cell__type--sum) { + border-bottom: 1px solid; + border-color: var(--pi-color-grey-50); + } + } + } + } +} + +/** +* +* 3. Ghost: +* +* A Data Grid without outer and inner strokes between the grid items. +* +**/ +@mixin dataGridAppearanceGhost() { +} + +// Data grid +.pi-data-grid { + @include dataGridAppearanceOutline; +} diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/_base-data-grid.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/_base-data-grid.scss new file mode 100644 index 0000000..ea21762 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/_base-data-grid.scss @@ -0,0 +1,261 @@ +@mixin cellWidth($default-width, $action-width, $icon-width, $description-width) { + .pi-cell { + display: flex; + // Cell type default + &:not(.pi-cell__type--icon):not(.pi-cell__type--action):not(.pi-cell__type--checkbox):not( + .pi-cell__type--date + ):not(.pi-cell__type--description) { + width: $default-width; + flex-grow: 1; + } + &.pi-cell__type--action, + &.pi-cell__type--date { + width: $action-width; + } + &.pi-cell__type--checkbox, + &.pi-cell__type--icon { + width: $icon-width; + } + &.pi-cell__type--description { + width: $description-width; + } + } +} + +@mixin dataGridHasBoxShadow() { + // MODE List (Table) + .is-mode-list { + // Data grid + .pi-data-grid { + &.has-box-shadow { + box-shadow: var(--pi-box-shadow-panel); + } + } + } +} + +// MODE List (Table) +.is-mode-list { + // Data grid + .pi-data-grid { + background-color: var(--pi-color-white); + + // Grid item (*Row with data direction === column | *Column with data direction === row) + .pi-grid-item { + display: flex; + + & > .pi-grid-item__content { + display: flex; + align-items: center; + gap: var(--pi-spacing-x-small); + min-height: 3.25rem; + width: 100%; + } + & > aside { + min-height: 3.25rem; + } + } + + // Data direction column (1 grid item = 1 column) + &.pi-data-grid__data-direction--row { + display: flex; + flex-direction: column; + + .pi-grid-item { + align-items: center; + gap: var(--pi-spacing-medium); + min-height: var(--pi-spacing-xxxx-large); + width: 100%; + } + // Appearacne Outline + + &:not(.pi-data-grid__appearance--ghost) { + .pi-grid-item { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + } + + // Cell + .pi-cell { + align-items: center; + gap: var(--pi-spacing-xx-small); + + &.pi-cell__type--action, + &.pi-cell__type--icon { + justify-content: flex-end; + .pi-text-viewer { + article { + display: none; + } + } + } + } + // 3 Columns + &.pi-data-grid__columns--3 { + @include cellWidth(35%, 30%, 25%, 45%); + } + + // 4 Columns + &.pi-data-grid__columns--4 { + @include cellWidth(40%, 35%, 30%, 50%); + } + // 5 Columns + &.pi-data-grid__columns--5 { + @include cellWidth(20%, 15%, 10%, 30%); + } + // 6 Columns + &.pi-data-grid__columns--6 { + @include cellWidth(18.66%, 13%, 10%, 28%); + } + // 7 Columns + &.pi-data-grid__columns--7 { + @include cellWidth(20.28%, 6.5%, 10%, 33%); + } + // 8 Columns + &.pi-data-grid__columns--8 { + @include cellWidth(12.5%, 12.5%, 10%, 22%); + } + // 9 Columns + &.pi-data-grid__columns--9 { + @include cellWidth(11.11%, 11.11%, 10%, 18%); + } + // 10 Columns + &.pi-data-grid__columns--10 { + @include cellWidth(10%, 10%, 10%, 15%); + } + // 11 Columns + &.pi-data-grid__columns--11 { + @include cellWidth(9%, 9%, 9%, 12%); + } + // 12 Columns + &.pi-data-grid__columns--12 { + @include cellWidth(8.33%, 8.33%, 8.33%, 11%); + } + } + } +} + +// MODE Grid (Card) (*infinite scroll) +.is-mode-grid { + .pi-data-grid { + display: grid; + gap: var(--pi-spacing-medium); + } +} + +html { + // Mobile + &.is-mobile { + .is-mode-grid { + .pi-data-grid { + &.pi-data-grid__data-direction--column { + .pi-grid-item { + flex-direction: column; + .pi-cell { + &.pi-text-viewer, + & > .pi-text-viewer { + &.pi-text-viewer__font-size--s { + label { + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small Device + @media screen and (max-width: 62rem) { + // MODE List (Table) + + .is-mode-list { + // Data grid + .pi-data-grid { + // Data direction column (1 grid item = 1 column) + + &.pi-data-grid__data-direction--column { + .pi-grid-item { + &.pi-grid-item__context--keys { + .pi-cell { + min-width: 20rem; + &.pi-text-viewer { + &.pi-text-viewer__align--center { + article { + margin-top: var(--pi-spacing-medium); + } + } + } + } + } + &:not(.is-active) { + display: none; + } + } + .pi-cell { + &.is-active { + display: flex; + } + &:not(.is-active) { + display: none; + } + } + } + } + } + .is-mode-grid { + .pi-data-grid { + &.pi-data-grid__data-direction--column { + .pi-grid-item { + &:not(.is-active) { + display: none; + } + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large Device + @media screen and (min-width: 62rem) { + // MODE List (Table) + .is-mode-list { + // Data grid + .pi-data-grid { + // Data direction column (1 grid item = 1 column) + &.pi-data-grid__data-direction--column { + .pi-grid-item { + .pi-cell { + .pi-text-viewer { + &.pi-text-viewer__justify--flex-end { + height: 100%; + } + } + &:not(.pi-cell__type--sum) + .pi-cell__type--sum { + &:not(:last-of-type) { + margin-bottom: var(--pi-spacing-medium); + } + } + } + } + // 4 rows + &.pi-data-grid__rows--4 { + .pi-grid-item { + width: 25%; + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/_card-type.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/_card-type.scss new file mode 100644 index 0000000..dd8aa4a --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/_card-type.scss @@ -0,0 +1,154 @@ +/** +* +* Card Type +* +**/ + +/** +* +* Card Type: LIST +* +**/ +@mixin dataGridCardTypeList { + &.pi-data-grid__card-type--list { + // Data Grid Columns: + // 7 + @include dataGridCardTypeList7; + // 11 + @include dataGridCardTypeList11; + + .pi-grid-item { + &.pi-card { + display: grid; + .pi-cell { + min-height: calc(var(--pi-spacing-xxxx-large) + var(--pi-spacing-xx-small) + 0.0625rem); + margin-top: var(--pi-spacing-xx-small); + + & > .pi-text-viewer, + &.pi-text-viewer { + article { + transform: translateY(var(--pi-spacing-xx-small)); + } + label { + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } +} + +/** +* +* Card Type: LIST +* +* Columns: 7 +* +**/ +@mixin dataGridCardTypeList7() { + &.pi-data-grid__columns--7 { + .pi-grid-item { + &.pi-card { + @for $i from 1 through 14 { + & > *:nth-child(#{$i}) { + grid-area: item#{$i}; + align-self: center; + } + } + + .pi-cell { + width: -webkit-fill-available; + } + + @for $i from 8 through 14 { + & > *:nth-child(#{$i}) { + justify-content: flex-end; + } + } + + grid-template-rows: repeat(7, 1fr); + grid-template-areas: + 'item1 item8' + 'item2 item9' + 'item3 item10' + 'item4 item11' + 'item5 item12' + 'item6 item13' + 'item7 item14'; + } + } + } +} + +/** +* +* Card Type: LIST +* +* Columns: 11 +* +**/ +@mixin dataGridCardTypeList11() { + &.pi-data-grid__columns--11 { + .pi-grid-item { + &.pi-card { + @for $i from 1 through 22 { + & > *:nth-child(#{$i}) { + grid-area: item#{$i}; + align-self: center; + } + } + + @for $i from 12 through 22 { + & > *:nth-child(#{$i}) { + justify-content: flex-end; + } + } + + grid-template-rows: repeat(11, 1fr); + grid-template-areas: + 'item1 item12' + 'item2 item13' + 'item3 item14' + 'item4 item15' + 'item5 item16' + 'item6 item17' + 'item7 item18' + 'item8 item19' + 'item9 item20' + 'item10 item21' + 'item11 item22'; + } + } + } +} + +/** +* +* Card Type: HORIZONTAL +* +**/ +@mixin dataGridCardTypeHorizontal() { +} + +/** +* +* Card Type: SQUARED +* +**/ +@mixin dataGridCardTypeSquared() { +} + +/** +* +* Card Type: VERTICAL +* +**/ +@mixin dataGridCardTypeVertical() { +} + +.pi-data-grid { + @include dataGridCardTypeList; +} diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/_data-direction.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/_data-direction.scss new file mode 100644 index 0000000..2f2c1b0 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/_data-direction.scss @@ -0,0 +1,152 @@ +/** +* DATA DIRECTION: +* +* Data Grid Mode: LIST. +* This will be the case when the data grid's mode is set to List which means that the data Grid emulates a Data Table. +* +* 1. Column: +* +* Any grid item is a column. The data projection will be horizontal. +* +**/ + +@mixin dataGridDataDirectionColumn($key-cell-width: 15rem, $value-cell-width: 6rem) { + // MODE List (Table) + + .pi-data-grid { + // Data direction: + // Column (1 grid item = 1 column) + &.pi-data-grid__data-direction--column { + display: flex; + + .pi-grid-item { + flex-direction: column; + + // Cell + .pi-cell { + min-height: calc(var(--pi-spacing-xxxx-large) + var(--pi-spacing-xx-small) + 0.0625rem); + margin-top: var(--pi-spacing-xx-small); + // Vertival align + &.pi-cell__vertical-align--center { + display: flex; + align-items: center; + } + // Override the Vertical Align Center visualize cells (text viewers) + // with the editable ones (text fields) in form data grids + &.pi-text-viewer, + & > .pi-text-viewer { + & > * { + transform: translateY(var(--pi-spacing-x-small)); + } + } + } + // Content Type: + // Keys (HEADINGS) + &.pi-grid-item__context--keys { + min-width: $key-cell-width; + + // Cell in Keys context + .pi-cell { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + + & > *:nth-child(1) { + margin-left: var(--pi-spacing-medium); + } + + & > *:nth-child(n + 2) { + justify-content: center; + } + } + } + // Values + &:not(.pi-grid-item__context--keys) { + min-width: $value-cell-width; + width: -webkit-fill-available; + } + } + + // Justify Content: + // Space Between + &.pi-data-grid__justify-content--space-between { + justify-content: space-between; + } + // Flex Start + &.pi-data-grid__justify-content--flex-start { + justify-content: flex-start; + } + } + } +} + +/** +* +* 2. Row: +* +* Any grid item is a row. The data projection will be vertical. +* +**/ +@mixin dataGridDataDirectionRow() { + // MODE List (Table) + .is-mode-list { + .pi-data-grid { + // Data direction row (1 grid item = 1 row) + &.pi-data-grid__data-direction--column { + // Scrollable area + .pi-data-grid__content { + margin: 0 auto; + width: -webkit-fill-available; + overflow-y: auto; + overflow-x: hidden; + + // Generic Scroll + &::-webkit-scrollbar { + width: 0.1875rem; + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-grey-25); + border-radius: var(--pi-border-radius-x-small); + + &:hover { + transition: var(--pi-animation); + background-color: var(--pi-color-grey-50); + } + } + } + } + } + } +} + +html { + // Tablet + &.is-tablet { + // Tablet L + &.is-tablet__large { + @include dataGridDataDirectionColumn; + } + } + + // Desktop + &.is-desktop { + // Desktop S + &.is-desktop__small { + @include dataGridDataDirectionColumn(13.5rem, 8rem); + } + // Desktop M + &.is-desktop__medium { + @include dataGridDataDirectionColumn(15rem, 9.125rem); + } + // Desktop L + &.is-desktop__large { + @include dataGridDataDirectionColumn(18rem, 8.5rem); + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/_mode.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/_mode.scss new file mode 100644 index 0000000..d9b8cfb --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/_mode.scss @@ -0,0 +1,23 @@ +/** +* MODE: +* The following properties defines when the Data Grid Component emulates a Data Table or a Cards Lidst, +* depending on Mode binded by the List Configs. +* Note that in small devices, the Mode will be always set as Grid. +* +* 1. Grid: +* +* A cards list in a grid. +* +**/ +@mixin dataGridModeGrid() { +} + +/** +* +* 2. List: +* +* A cards list in a grid. +* +**/ +@mixin dataGridModeList() { +} diff --git a/libs/ui/src/lib/styles/common/components/pi-data-grid/index.scss b/libs/ui/src/lib/styles/common/components/pi-data-grid/index.scss new file mode 100644 index 0000000..b937db0 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-data-grid/index.scss @@ -0,0 +1,5 @@ +@forward 'appearance'; +@forward 'card-type'; +@forward 'base-data-grid'; +@forward 'data-direction'; +@forward 'mode'; diff --git a/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-incremental.scss b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-incremental.scss new file mode 100644 index 0000000..049152d --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-incremental.scss @@ -0,0 +1,285 @@ +@mixin incrementalCellField( + $max-width: calc(100% - var(--pi-spacing-x-large)), + $margin-left: var(--pi-spacing-x-large) +) { + .pi-shell, + .cdk-overlay-container { + .pi-cell { + .pi-field { + // Incremental field + &.pi-form-field__incremental { + &:not(.pi-form-field__incremental--range) { + .mat-mdc-form-field { + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-text-field-wrapper { + max-width: $max-width; + margin-left: $margin-left; + } + } + } + } + } + } + } + } +} + +.pi-shell, +.cdk-overlay-container { + .pi-field { + // Incremental field + &.pi-form-field__incremental { + .mat-mdc-form-field { + width: -webkit-fill-available; + &.mat-focused { + .mat-mdc-form-field-placeholder { + color: var(--pi-color-primary); + } + } + + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-underline { + display: none; + } + } + .mat-mdc-form-field-flex { + justify-content: space-between; + align-items: center; + height: calc(var(--pi-spacing-xxxx-large) + var(--pi-spacing-xx-small)); + .mat-mdc-form-field-infix { + text-align: center; + } + .mat-mdc-form-field-suffix { + padding-right: var(--pi-spacing-medium); + } + .pi-affix { + display: flex; + align-items: center; + gap: var(--pi-spacing-xx-small); + + &.pi-affix__suffix { + flex-direction: row-reverse; + } + } + } + input { + &.mat-mdc-input-element { + text-align: right !important; + } + } + // Add & Substract icons + .pi-icon { + &:not(.pi-icon__appearance--fill), + &.pi-icon__small { + border: 1px solid var(--pi-color-primary); + border-radius: var(--pi-border-radius-x-small); + } + } + } + + // NOT Has error + &:not(.has-error) { + &:not(.mat-focused) { + .mdc-text-field { + .mdc-text-field__input { + color: var(--pi-color-black); + } + &:not(.mdc-text-field--disabled) { + .mdc-text-field__input { + color: var(--pi-color-black); + } + } + } + } + + &.mat-focused { + .mdc-text-field { + .mdc-text-field__input { + color: var(--pi-color-primary); + } + &:not(.mdc-text-field--disabled) { + .mdc-text-field__input { + color: var(--pi-color-primary); + } + } + } + } + } + + // has error + &.has-error { + .mdc-text-field { + .mdc-text-field__input { + color: var(--pi-color-warn); + } + &:not(.mdc-text-field--disabled) { + .mdc-text-field__input { + color: var(--pi-color-warn); + } + } + } + } + + // Disabled + &.is-disabled { + color: var(--pi-color-grey-200); + } + + // Incremental range + &.pi-form-field__incremental--range { + & > form { + display: flex; + gap: var(--pi-spacing-medium); + + & > div { + width: -webkit-fill-available; + } + + .pi-label { + span { + color: var(--pi-color-primary); + font: { + size: var(--pi-font-size-x-small); + } + } + } + + .pi-form-field__incremental { + width: 50%; + } + } + .mat-mdc-form-field-flex { + width: 99%; + } + } + } + } + + .pi-dynamic-menu, + .pi-card { + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-input-control { + height: 3.45rem; + } + } + } + + // Cells with incrementals inputs: suffix light weight + .pi-cell { + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-icon-suffix { + .pi-affix__suffix { + & > span { + font-weight: 300; + } + } + } + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-shell, + .cdk-overlay-container { + .pi-field { + // Incremental field + &.pi-form-field__incremental { + // Add & Substract icons + .pi-icon { + &:not(.pi-icon__appearance--fill), + &.pi-icon__small { + height: var(--pi-spacing-large); + width: var(--pi-spacing-large); + border-radius: 42%; + .mat-icon { + height: var(--pi-spacing-medium); + width: var(--pi-spacing-medium); + } + } + } + // Incremental range + &.pi-form-field__incremental--range { + .mat-mdc-text-field-wrapper { + .mat-mdc-form-field-input-control { + width: calc(50vw - 9rem); + } + } + } + } + } + } + @include incrementalCellField; + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .pi-shell, + .cdk-overlay-container { + .pi-field { + // Incremental field + &.pi-form-field__incremental { + // Add & Substract icons + .pi-icon { + &:not(.pi-icon__appearance--fill), + &.pi-icon__small { + height: var(--pi-spacing-medium); + width: var(--pi-spacing-medium); + border-radius: 42%; + .mat-icon { + height: var(--pi-spacing-small); + width: var(--pi-spacing-small); + } + } + } + } + } + } + } + } + + &.is-tablet { + &.is-tablet__small { + @include incrementalCellField( + calc(100% - var(--pi-spacing-xx-large) - 25%), + calc(var(--pi-spacing-xx-large) + 25%) + ); + } + &.is-tablet__large { + @include incrementalCellField( + calc(100% - var(--pi-spacing-xx-large) - 12.5%), + calc(var(--pi-spacing-xx-large) + 12.5%) + ); + } + } + // Desktop + &.is-desktop { + // Desktop S + &.is-desktop__small { + @include incrementalCellField( + calc(100% - var(--pi-spacing-xx-large)), + calc(var(--pi-spacing-xx-large)) + ); + } + // Desktop M + &.is-desktop__medium { + @include incrementalCellField( + calc(100% - var(--pi-spacing-xx-large) - 33.33%), + calc(var(--pi-spacing-xx-large) + 33.33%) + ); + } + // Desktop L + &.is-desktop__large { + @include incrementalCellField( + calc(100% - var(--pi-spacing-xx-large) - 40%), + calc(var(--pi-spacing-xx-large) + 40%) + ); + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-password.scss b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-password.scss new file mode 100644 index 0000000..2de2f29 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-password.scss @@ -0,0 +1,19 @@ +.pi-shell, +.cdk-overlay-container { + .pi-field { + // Password + &.pi-form-field__password { + button { + &.pi-button__icon { + color: var(--pi-color-grey-200); + &:hover { + &:enabled { + color: var(--pi-color-grey-200); + text-decoration: underline; + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-textarea.scss b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-textarea.scss new file mode 100644 index 0000000..e148abf --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field-textarea.scss @@ -0,0 +1,82 @@ +.pi-shell, +.cdk-overlay-container { + .pi-field { + // Textarea + &.pi-form-field__textarea { + .mat-mdc-text-field-wrapper { + min-height: 7rem; + &.mdc-text-field--outlined { + .mdc-text-field__input { + padding: var(--pi-spacing-xx-small) 0; + } + } + } + + .mat-mdc-form-field { + height: unset; + textarea { + &.mat-mdc-input-element { + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + size: var(--pi-font-size-small); + weight: 300; + } + } + } + // Outline + &.mat-mdc-form-field-appearance-outline { + [class*='mat-form-field-outline-'] { + height: auto; + } + .mat-mdc-form-field-infix { + padding: var(--pi-spacing-small) 0; + textarea { + &.mat-mdc-input-element { + padding: var(--pi-spacing-xxx-small) 0 var(--pi-spacing-x-small) 0; + } + } + } + } + // fill + &.mat-form-field-appearance-fill { + textarea { + &.mat-mdc-input-element { + // max-width: calc(100% - var(--pi-spacing-medium)); + margin-top: var(--pi-spacing-x-small); + background-color: var(--pi-color-background-400); + transition: var(--pi-animation-fast); + } + } + + &.mat-form-field-disabled { + textarea { + &.mat-mdc-input-element { + background-color: var(--pi-color-white); + } + } + & + span { + display: none; + } + } + &:not(.mat-form-field-disabled) { + textarea { + &.mat-mdc-input-element { + padding: var(--pi-spacing-x-small); + background-color: var(--pi-color-background-400); + } + } + } + & + span { + display: flex; + justify-content: flex-end; + width: 100%; + margin-top: var(--pi-spacing-xx-small); + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-field/_pi-field.scss b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field.scss new file mode 100644 index 0000000..31a30e8 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-field/_pi-field.scss @@ -0,0 +1,181 @@ +.pi-shell, +.cdk-overlay-container { + .pi-field { + &:not(.pi-form-field__incremental) { + // Icon colors + .mat-mdc-form-field { + &:not(.mat-form-field-invalid) { + &.mat-focused { + .mat-icon { + color: var(--pi-color-primary); + } + } + } + &:not(.mat-form-field-invalid):not(.mat-focused) { + .mat-icon { + color: var(--pi-color-grey-300); + } + } + &.mat-form-field-invalid { + .mat-icon { + color: var(--pi-color-warn); + } + } + } + } + .mat-mdc-form-field-input-control { + font: { + family: 'Soehne', Helvetica, Arial, sans-serif !important; + weight: 300; + } + } + .mdc-text-field { + .mdc-floating-label { + font: { + family: 'Soehne', Helvetica, Arial, sans-serif !important; + weight: 300; + } + } + } + .mdc-text-field--disabled { + .mdc-floating-label { + color: var(--pi-color-grey-300); + } + } + } +} + +.pi-shell, +.cdk-overlay-container { + // Cell Fields + .pi-grid-item { + .pi-cell { + .pi-field { + width: 100%; + .mat-mdc-form-field { + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-form-field-underline { + transform: translateY(calc(var(--pi-spacing-medium) + var(--pi-spacing-xxx-small))); + } + + input { + &.mat-mdc-input-element { + height: var(--pi-spacing-xxx-large); + margin-top: unset; + text-align: right !important; + } + } + .mat-mdc-form-field-icon-prefix, + .mat-mdc-form-field-icon-suffix { + transform: translateY(var(--pi-spacing-xx-small)); + } + } + } + &:not(.pi-form-field__incremental) { + input { + &.mat-mdc-input-element { + transform: translateY(var(--pi-spacing-medium)); + } + } + } + &.pi-form-field__incremental { + .mat-mdc-form-field { + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-form-field-infix { + min-height: unset; + } + input { + &.mat-mdc-input-element { + transform: translateY(var(--pi-spacing-xx-small)); + } + } + } + } + } + } + .pi-text-viewer__font-weight--600 { + height: calc(var(--pi-spacing-xxxx-large) - 0.0625rem); + } + } + } +} + +.cdk-overlay-container, +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-shell, + .cdk-overlay-container { + // Cell Fields + .pi-grid-item { + .pi-cell { + .pi-field { + .mat-mdc-form-field { + .mat-mdc-form-field-flex { + min-height: 3.25rem; + } + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-form-field-underline { + transform: translateY( + calc(var(--pi-spacing-medium) + var(--pi-spacing-xxx-small)) + ); + } + + input { + &.mat-mdc-input-element { + height: var(--pi-spacing-xxx-large); + margin-top: unset; + text-align: right !important; + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } + } + } + } + } + + &:not(.is-mobile) { + .pi-shell, + .cdk-overlay-container { + // Cell Fields + .pi-grid-item { + .pi-cell { + .pi-field { + .mat-mdc-form-field { + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-text-field-wrapper { + margin-left: var(--pi-spacing-xx-large); + } + } + } + &:not(.pi-form-field__incremental) { + .mat-mdc-form-field { + // Fill + &.mat-form-field-appearance-fill { + .mat-mdc-text-field-wrapper { + max-width: calc(100% - var(--pi-spacing-xx-large)); + } + } + } + } + } + .pi-text-viewer__font-weight--600 { + padding-left: var(--pi-spacing-x-large); + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/components/pi-field/index.scss b/libs/ui/src/lib/styles/common/components/pi-field/index.scss new file mode 100644 index 0000000..63aa0a9 --- /dev/null +++ b/libs/ui/src/lib/styles/common/components/pi-field/index.scss @@ -0,0 +1,4 @@ +@forward 'pi-field-incremental'; +@forward 'pi-field-password'; +@forward 'pi-field-textarea'; +@forward 'pi-field'; diff --git a/libs/ui/src/lib/styles/common/index.scss b/libs/ui/src/lib/styles/common/index.scss new file mode 100644 index 0000000..0f4523b --- /dev/null +++ b/libs/ui/src/lib/styles/common/index.scss @@ -0,0 +1,32 @@ +@use 'abstracts' as *; +@use 'base' as *; +@use 'components' as *; +@use 'layouts' as *; +@use 'vendors' as *; +@use 'wrappers' as *; + +@use '@angular/material' as mat; +@include mat.checkbox-density(0); + +// TODO: Change import file after checking with Daniel +@import '@typeform/embed/build/css/widget.css'; +@import '@typeform/embed/build/css/popup.css'; + +@include mat.core(); + +// Palette +$pi-palette-accent: mat.define-palette($pi-color-theme-accent); +$pi-palette-primary: mat.define-palette($pi-color-theme-primary); +$pi-palette-warn: mat.define-palette($pi-color-theme-warn); +// Theme +$pi-theme: mat.define-light-theme( + ( + color: ( + accent: $pi-palette-accent, + primary: $pi-palette-primary, + warn: $pi-palette-warn, + ), + density: 0, + ) +); +@include mat.all-component-themes($pi-theme); diff --git a/libs/ui/src/lib/styles/common/layouts/_pi-09-pricing-layout.scss b/libs/ui/src/lib/styles/common/layouts/_pi-09-pricing-layout.scss new file mode 100644 index 0000000..b06282b --- /dev/null +++ b/libs/ui/src/lib/styles/common/layouts/_pi-09-pricing-layout.scss @@ -0,0 +1,921 @@ +// Shine hover effect +@mixin layoutPricingButtonsShineHoverEffect { + // Hover shine effect + &:not(.pi-button__appearance--ghost):not(.pi-button__type--menu-item) { + position: relative; + overflow: hidden; + &::after { + content: ''; + position: absolute; + top: -50%; + right: 0; + bottom: -50%; + left: -50%; + transform: rotateZ(75deg) translate(-5rem, 7.5rem); + background: linear-gradient( + to bottom, + rgba(229, 172, 142, 0), + rgba(255, 255, 255, 0.25) 50%, + rgba(229, 172, 142, 0) + ); + &:hover { + cursor: pointer; + } + } + &:hover, + &:focus { + &::after { + animation: sheen 0.75s forwards; + } + } + + &:active { + transform: translate(0, -0.125rem); + box-shadow: 0px 0px 0px white; + -webkit-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + -moz-box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + box-shadow: inset -0.125rem -0.125rem -0.125rem rgba(255, 255, 255, 0.25); + } + } + + &:focus { + outline: 0; + } + + @keyframes sheen { + 100% { + transform: rotateZ(30deg) translate(1rem, -9rem); + } + } +} + +@mixin billingCycleContentColor($text-color, $theming-color-light, $theming-color) { + color: var(--pi-color-#{$text-color}); + background-color: var(--pi-color-#{$theming-color}); + border-color: var(--pi-color-#{$theming-color}); + &:hover { + background-image: unset; + } +} + +@mixin pricingLayoutDiscountChip( + $border-color: var(--pi-color-grey-50), + $label-color: var(--pi-color-grey-900), + $background-color: var(--pi-gradient-grey-light) +) { + & > span { + & > label { + color: $label-color; + } + } + border: 1px solid; + border-color: $border-color; + background: $background-color; +} + +@mixin pricingLayoutGrid( + $grid-template-columns--regular-cards: repeat(3, 1fr), + $grid-template-columns--sticky-cards: 1fr 3fr +) { + .pi-layout { + &.pi-layout__09-pricing { + // Pricing cards section + & > *:nth-child(2) { + display: grid; + grid-template-columns: $grid-template-columns--regular-cards; + } + // Feature cards section + & > *:nth-child(4) { + // Sticky pricing card headers + & > div { + &:first-child { + & > *:nth-child(1) { + display: grid; + grid-template-columns: $grid-template-columns--sticky-cards; + align-items: end; + grid-gap: var(--pi-spacing-medium); + + & > *:nth-child(2) { + display: grid; + grid-template-columns: $grid-template-columns--regular-cards; + gap: var(--pi-spacing-small); + } + } + } + } + // Feature card wrapper + & > div { + &:last-child { + display: grid; + grid-template-columns: $grid-template-columns--regular-cards; + } + } + } + } + } +} + +@mixin checkIconBackgroundColor($background-color: var(--pi-color-grey-50)) { + .pi-icon { + &.pi-icon__border-radius--large { + &.pi-icon__appearance--fill { + background-color: $background-color; + } + } + } +} + +@mixin cardInternalBorders($border-color: var(--pi-color-grey-50)) { + .pi-card-content__bottom { + border-top: 1px solid; + border-color: $border-color; + } + .pi-list-item { + &.pi-list-item__border--footer { + border-color: $border-color; + } + } +} + +@mixin pricingCardContentColor($color, $button-text-color, $button-gradient) { + color: var(--pi-color-#{$color}); + border: 1px solid; + border-color: var(--pi-color-#{$color}-75); + // Card content + .pi-card-content { + // Content Center + &.pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-#{$color}); + } + } + } + // Content Bottom + &.pi-card-content__bottom { + button { + color: var(--pi-color-#{$button-text-color}); + background-image: var(--pi-gradient-#{$button-gradient}); + @include layoutPricingButtonsShineHoverEffect; + } + } + } +} + +@mixin companyPricing { + .pi-layout { + &.pi-layout__09-pricing { + fieldset { + .pi-list-item { + &.is-active { + @include billingCycleContentColor(primary-900, primary, primary-100); + } + &:hover { + &:not(.is-active) { + @include billingCycleContentColor(primary, primary, primary-50); + } + } + } + } + .pi-card { + &.pi-color__primary { + &.pi-color__mode--light { + @include checkIconBackgroundColor(var(--pi-color-primary-50)); + @include cardInternalBorders(var(--pi-color-primary-200)); + @include pricingCardContentColor(primary, white, primary-dark); + } + &.pi-color__mode--dark { + @include checkIconBackgroundColor(var(--pi-color-primary-400)); + @include cardInternalBorders(var(--pi-color-white)); + @include pricingCardContentColor(white, primary, primary-light); + } + } + } + } + } +} + +@mixin investorPricing { + .pi-layout { + &.pi-layout__09-pricing { + fieldset { + .pi-list-item { + &.is-active { + @include billingCycleContentColor(accent-900, accent, accent-100); + } + &:hover { + &:not(.is-active) { + @include billingCycleContentColor(accent, accent, accent-50); + } + } + } + } + .pi-card { + &.pi-color__accent { + &.pi-color__mode--light { + @include checkIconBackgroundColor(var(--pi-color-accent-50)); + @include cardInternalBorders(var(--pi-color-accent-200)); + @include pricingCardContentColor(accent, white, accent-dark); + } + &.pi-color__mode--dark { + @include checkIconBackgroundColor(var(--pi-color-accent-300)); + @include cardInternalBorders(var(--pi-color-white)); + @include pricingCardContentColor(white, accent-900, accent-light); + // Bottom + .pi-card-content__bottom { + & > div { + & > span { + color: var(--pi-color-white); + } + } + } + } + } + } + } + } +} + +@mixin pricingLayoutSpacings($bottom-blocks-top-spacing, $bottom-blocks-height) { + .pi-layout { + &.pi-layout__09-pricing { + & > *:nth-child(4) { + // Feature card wrapper + & > div { + &:last-child { + .pi-card { + margin: #{$bottom-blocks-top-spacing} auto auto auto; + } + } + } + .pi-card { + .pi-card-content__center { + & > article { + &:nth-child(n + 2) { + margin-top: calc(#{$bottom-blocks-top-spacing} + 2 * var(--pi-spacing-xx-large)); + } + & > .pi-list-item { + height: calc(#{$bottom-blocks-height} + 2 * var(--pi-spacing-small)); + } + } + } + } + } + } + } +} +@mixin advisorPricing { + .pi-layout { + &.pi-layout__09-pricing { + fieldset { + .pi-list-item { + &.is-active { + @include billingCycleContentColor(highlighted-900, highlighted, highlighted-100); + } + &:hover { + &:not(.is-active) { + @include billingCycleContentColor(highlighted, highlighted, highlighted-50); + } + } + } + } + .pi-card { + &.pi-color__highlighted { + &.pi-color__mode--light { + @include checkIconBackgroundColor(var(--pi-color-highlighted-50)); + @include cardInternalBorders(var(--pi-color-highlighted-100)); + @include pricingCardContentColor(highlighted, white, highlighted-dark); + } + } + } + } + } +} + +@mixin pricingLayoutWidth($container-width: calc(100% - var(--pi-spacing-xx-large))) { + .pi-layout { + &.pi-layout__09-pricing { + max-width: $container-width; + margin: auto; + & > *:nth-child(4) { + & > *:nth-child(1) { + & > header { + max-width: $container-width; + margin: auto; + } + } + } + } + } +} + +.pi-layout { + &.pi-layout__09-pricing { + // Billing cycle selector + & > *:nth-child(1) { + & > *:nth-child(2) { + margin-top: var(--pi-spacing-xx-large); + & > * { + &:last-child { + margin-top: var(--pi-spacing-x-small); + } + } + } + } + + fieldset { + .pi-list-item { + &:not(.is-active) { + &:hover { + cursor: pointer; + } + } + .pi-list-item-content__right { + flex: unset; + .pi-chip { + min-width: unset; + &.pi-chip__appearance--fill { + max-height: 1.8125rem; + height: unset; + padding: var(--pi-spacing-x-small) var(--pi-spacing-xx-small); + &.pi-color__grey { + @include pricingLayoutDiscountChip; + } + &.pi-color__accent { + @include pricingLayoutDiscountChip( + var(--pi-color-accent-200), + var(--pi-color-white), + var(--pi-gradient-accent-dark) + ); + } + } + } + } + } + } + + // Pricing cards section + & > *:nth-child(2) { + .pi-card { + // Center + .pi-card-content__center { + min-height: 6rem; + margin-top: var(--pi-spacing-x-large); + padding: 0 var(--pi-spacing-xx-small); + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-small); + } + } + // Bottom + .pi-card-content__bottom { + margin-top: var(--pi-spacing-x-large); + padding-top: var(--pi-spacing-x-large); + text-align: center; + & > div { + & > span { + color: var(--pi-color-accent); + } + } + button { + margin-top: var(--pi-spacing-x-large); + } + } + // Card grey + &.pi-color__grey { + // Mode Light + &.pi-color__mode--light { + // Footer + .pi-card-content__bottom { + border-color: var(--pi-color-grey-100); + } + } + } + } + } + & > *:nth-child(4) { + // Feature card wrapper + & > div { + &:last-child { + width: 100%; + gap: var(--pi-spacing-small); + } + } + } + // Default mode card + .pi-card { + &.pi-color__grey { + &.pi-color__mode--light { + @include checkIconBackgroundColor; + @include cardInternalBorders(var(--pi-color-grey-100)); + @include pricingCardContentColor(grey, white, grey-dark); + } + } + } + } +} + +.is-company { + @include companyPricing; +} + +.is-investor { + @include investorPricing; +} +.is-advisor { + @include advisorPricing; +} + +html { + // Mobile + &.is-mobile { + @include pricingLayoutWidth; + .pi-layout { + &.pi-layout__09-pricing { + max-width: calc(100% - var(--pi-spacing-xx-large)); + margin: auto; + & > *:nth-child(1) { + fieldset + fieldset { + margin-top: var(--pi-spacing-medium); + } + } + } + } + } + + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__09-pricing { + & > *:nth-child(1) { + .pi-text-viewer { + margin: auto; + } + } + & > *:nth-child(2) { + margin-top: var(--pi-spacing-xx-large); + } + // Features + & > *:nth-child(3), + & > *:nth-child(4) { + display: none; + } + .pi-main-wrapper-content.is-sticky { + width: calc(100% + 3rem); + margin-left: -1.5rem; + padding: var(--pi-spacing-x-large); + } + } + } + + // Small device + @media screen and (max-width: 62rem) { + .pi-layout { + &.pi-layout__09-pricing { + // Pricing cards section + & > *:nth-child(2) { + display: flex; + flex-direction: column-reverse; + align-items: center; + gap: var(--pi-spacing-xx-large); + } + } + } + } + } + + // Tablet + &.is-tablet { + @include pricingLayoutWidth(calc(100% - 3rem)); + .pi-layout { + &.pi-layout__09-pricing { + & > *:nth-child(1) { + & > *:nth-child(2) { + display: flex; + gap: var(--pi-spacing-medium); + align-items: center; + justify-content: center; + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + fieldset { + max-width: 16rem; + + .pi-list-item { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-template-areas: + 'item1 item2' + 'item3 .'; + height: 6.6rem; + box-shadow: var(--pi-box-shadow-panel); + .pi-list-item-icon__left { + grid-area: item1; + .pi-icon { + justify-content: flex-start; + } + } + .pi-list-item-content__left { + grid-area: item3; + } + .pi-list-item-content__right { + grid-area: item2; + text-align: -webkit-right; + } + } + } + } + } + & > *:nth-child(2) { + gap: var(--pi-spacing-medium); + max-width: calc(100% - 3rem); + margin: var(--pi-spacing-xx-large) auto; + } + } + } + // Tablet S + &.is-tablet__small { + .pi-layout { + &.pi-layout__09-pricing { + // Pricing cards section + & > *:nth-child(2) { + .pi-card { + width: 30.125rem; + max-width: 30.125rem; + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .is-company { + @include pricingLayoutGrid; + } + .is-investor { + @include pricingLayoutGrid; + } + .is-advisor { + @include pricingLayoutGrid(repeat(2, 1fr), repeat(1, 1fr 3fr)); + } + .pi-layout { + &.pi-layout__09-pricing { + // Pricing cards section + & > *:nth-child(2) { + .pi-card { + height: 100%; + margin: auto; + } + } + } + } + } + } + + // Desktop + &.is-desktop { + // Desktop S & M + &:not(.is-desktop__large) { + @include pricingLayoutWidth(var(--pi-public-width-container)); + } + // Desktop L + &.is-desktop__large { + @include pricingLayoutWidth(var(--pi-public-width-container-l)); + } + .pi-layout { + &.pi-layout__09-pricing { + display: grid; + grid-template-columns: 1fr 3fr; + grid-column-gap: var(--pi-spacing-medium); + grid-row-gap: var(--pi-spacing-xx-large); + margin: 0 auto; + // Billing cycle selector + & > *:nth-child(1) { + & > *:nth-child(2) { + fieldset + fieldset { + margin-top: var(--pi-spacing-medium); + } + } + } + // Pricing cards section + & > *:nth-child(2) { + gap: var(--pi-spacing-small); + } + // Features per phase + & > *:nth-child(3) { + // List of features inside each phase + article { + & > .pi-list-item { + height: calc(4.65rem + 2 * var(--pi-spacing-small)); + } + } + } + & > *:nth-child(4) { + // Sticky pricing card headers + & > div { + &:first-child { + gap: var(--pi-spacing-small); + transition: var(--pi-animation); + position: fixed; + left: 0; + top: 0; + z-index: 99; + width: 100%; + &.is-visible { + opacity: 1; + visibility: visible; + background-color: rgba($color: white, $alpha: 0.66); + @include cardInternalBorders(transparent); + .pi-card { + gap: var(--pi-spacing-x-small); + .pi-card-content__top { + header { + display: flex; + justify-content: space-between; + gap: var(--pi-spacing-xx-small); + align-items: baseline; + width: -webkit-fill-available; + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-x-small); + .pi-text-viewer { + article { + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + } + } + } + } + } + } + &:not(.is-visible) { + opacity: 0; + visibility: hidden; + height: 0; + } + + fieldset { + padding-bottom: var(--pi-spacing-xx-large); + } + } + } + // Feature card wrapper + & > div { + &:last-child { + width: 100%; + gap: var(--pi-spacing-small); + .pi-card { + border: none; + } + } + } + .pi-card { + min-height: unset; + .pi-card-content__center { + margin-top: 0; + & > article { + & > .pi-list-item { + justify-content: center; + } + } + } + + // Top + .pi-card-content__top { + align-items: flex-start; + justify-content: flex-start; + gap: var(--pi-spacing-small); + .pi-icon { + &.pi-icon__type--custom { + padding: var(--pi-spacing-xx-small); + } + } + h3 { + font: { + size: var(--pi-font-size-x-large); + weight: 700; + } + } + h4 { + font: { + size: var(--pi-font-size-small); + weight: 300; + } + } + } + // Center + .pi-card-content__center { + padding: 0 var(--pi-spacing-xx-small); + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-small); + } + + // Footer + .pi-card-content__footer { + button { + width: 100%; + font-weight: 300; + &:not(.mdc-switch):not(.pi-button__toggle) { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__xx-small):not( + .pi-button__x-large + ):not(.pi-button__toggle):not(.pi-button__trimmed) { + height: var(--pi-spacing-xxxx-large); + } + } + } + } + + // Demo + &.pi-color__grey { + // Mode Light + &.pi-color__mode--light { + // Top + .pi-card-content__top { + h4 { + color: var(--pi-color-grey-300); + } + } + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-grey-900); + } + } + } + + // Footer + .pi-card-content__footer { + border-color: var(--pi-color-grey-100); + } + + button { + color: var(--pi-color-white); + background-color: var(--pi-color-grey-900); + } + } + } + // Company pricing + &.pi-color__primary { + // Basic plan + &.pi-color__mode--light { + color: var(--pi-color-primary); + // Header + .pi-card-content__header { + h4 { + color: var(--pi-color-primary-300); + } + } + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-primary); + } + } + } + + button { + color: var(--pi-color-white); + background: var(--pi-gradient-primary-500-900); + } + } + // Premium plan + &.pi-color__mode--dark { + color: var(--pi-color-white); + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-white); + } + } + } + + button { + color: var(--pi-color-primary-1000); + background-color: var(--pi-color-white); + } + } + } + // Investor pricing + &.pi-color__accent { + // Basic plan + &.pi-color__mode--light { + color: var(--pi-color-accent); + // Top + .pi-card-content__top { + h4 { + color: var(--pi-color-accent-300); + } + } + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-accent); + } + } + } + .pi-card-content__bottom { + button { + color: var(--pi-color-white); + background-image: var(--pi-gradient-accent-200-300); + } + } + } + // Premium plan + &.pi-color__mode--dark { + color: var(--pi-color-white); + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-white); + } + } + } + + button { + color: var(--pi-color-accent); + background: var(--pi-color-white); + } + } + } + // Advisor pricing + &.pi-color__highlighted { + // Basic plan + &.pi-color__mode--light { + color: var(--pi-color-highlighted); + // Top + .pi-card-content__top { + h4 { + color: var(--pi-color-highlighted-300); + } + } + // Center + .pi-card-content__center { + .pi-text-viewer { + label, + span { + color: var(--pi-color-highlighted); + } + } + } + + button { + color: var(--pi-color-white); + background: var(--pi-gradient-highlighted-300-500); + } + } + } + } + + &.pi-color__grey { + .pi-card-content__center { + .pi-list-item { + .pi-icon { + .mat-icon { + color: var(--pi-color-grey-400); + } + } + border-color: var(--pi-color-grey-100); + } + } + } + &.pi-color__mode--dark { + .pi-card-content__center { + .pi-list-item { + .pi-icon { + .mat-icon { + color: var(--pi-color-white); + } + } + } + } + } + } + } + } + } + + // Advisor + .is-advisor { + @include pricingLayoutSpacings(3.25rem, 4.65rem); + } + // Investor & Company + .is-investor, + .is-company { + @include pricingLayoutSpacings(4.0125rem, 4.65rem); + } + } +} diff --git a/libs/ui/src/lib/styles/common/layouts/_pi-container-layout.scss b/libs/ui/src/lib/styles/common/layouts/_pi-container-layout.scss new file mode 100644 index 0000000..6ca77ee --- /dev/null +++ b/libs/ui/src/lib/styles/common/layouts/_pi-container-layout.scss @@ -0,0 +1,64 @@ +@mixin containerWidth($width: calc(100% - var(--pi-spacing-xx-large)), $no-padding-width: 100%) { + .pi-shell { + &.pi-shell__deale-website { + .pi-container-layout { + .pi-layout { + &:not(.pi-layout__06-edge-to-edge):not(.pi-layout__09-pricing) { + & > *:nth-child(n + 1) { + width: $width; + max-width: $width; + margin: 0 auto; + } + } + &.pi-layout__06-edge-to-edge { + max-width: -webkit-fill-available; + } + &.pi-layout__09-pricing + .pi-layout, + &.pi-layout + .pi-layout__09-pricing { + & > *:nth-child(n + 1) { + max-width: $no-padding-width; + } + } + } + } + } + } +} + +.pi-container-layout { + display: block; +} + +html { + // Mobile + &.is-mobile { + @include containerWidth; + } + + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include containerWidth(calc(100% - 3rem), 100%); + } + // Tablet L + &.is-tablet__large { + @include containerWidth(calc(100% - var(--pi-spacing-xxxx-large)), 100%); + } + } + + // Desktop + &.is-desktop { + // Desktop S & M + &:not(.is-desktop__large) { + @include containerWidth(var(--pi-public-width-container), var(--pi-public-width-container)); + } + // Desktop L + &.is-desktop__large { + @include containerWidth( + var(--pi-public-width-container-l), + var(--pi-public-width-container-l) + ); + } + } +} diff --git a/libs/ui/src/lib/styles/common/layouts/_pi-layout.scss b/libs/ui/src/lib/styles/common/layouts/_pi-layout.scss new file mode 100644 index 0000000..d95f74b --- /dev/null +++ b/libs/ui/src/lib/styles/common/layouts/_pi-layout.scss @@ -0,0 +1,65 @@ +@mixin layoutHeights( + $small-height: 33.33vh, + $medium-height: 40vh, + $large-height: 75vh, + $full-height: 100vh +) { + .pi-container-layout { + .pi-layout { + // 33.33vh + &.pi-layout__small { + min-height: $small-height; + } + // 33.33vh + &.pi-layout__medium { + min-height: $medium-height; + } + // 75vh + &.pi-layout__large { + min-height: $large-height; + } + // 100vh + &.pi-layout__full { + min-height: $full-height; + } + } + } +} + +.pi-container-layout { + .pi-layout { + display: block; + background-size: cover; + + & > .pi-background-image { + width: 100%; + } + + & > *:nth-child(-2n + 2) { + margin-top: var(--pi-spacing-xx-large); + } + + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-large); + } + + &.pi-layout__no-padding { + padding: 0; + } + } +} + +html { + // Mobile + &.is-mobile { + @include layoutHeights; + } + // Tablet + &.is-tablet { + @include layoutHeights(30vh, 40vh, 66.66vh, 100vh); + } + // Desktop + &.is-desktop { + @include layoutHeights(33.33vh, 50vh, 62vh, 100vh); + } +} diff --git a/libs/ui/src/lib/styles/common/layouts/index.scss b/libs/ui/src/lib/styles/common/layouts/index.scss new file mode 100644 index 0000000..9279604 --- /dev/null +++ b/libs/ui/src/lib/styles/common/layouts/index.scss @@ -0,0 +1,3 @@ +@forward 'pi-09-pricing-layout'; +@forward 'pi-container-layout'; +@forward 'pi-layout'; diff --git a/libs/ui/src/lib/styles/common/vendors/_ch2-button.scss b/libs/ui/src/lib/styles/common/vendors/_ch2-button.scss new file mode 100644 index 0000000..3fd72e8 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_ch2-button.scss @@ -0,0 +1,271 @@ +.ch2 { + .ch2-settings-scan { + .ch2-settings-tabs { + height: 3.3125rem; + } + } + + .ch2-theme-bar { + .ch2-dialog { + overflow-x: hidden; + padding: var(--pi-spacing-x-large); + } + } + // Override wrong primary + .ch2-style-light, + .ch2-style-dark { + .ch2-btn-primary { + all: unset; + min-width: 4.125rem; + text-align: center; + &:hover { + background: unset; + box-shadow: unset; + } + } + } + button:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single) { + &.ch2-btn { + min-height: var(--pi-spacing-xxx-large) !important; + &:not(.ch2-btn-primary) { + padding: var(--pi-spacing-xxxx-small) 0; + } + &.ch2-btn-primary { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-small); + } + } + } + + .ch2-btn { + border: 0; + padding: var(--pi-spacing-xxxx-small) 0; + font-size: 15px; + letter-spacing: unset; + text-transform: unset; + min-height: unset !important; + box-shadow: unset; + &:hover { + box-shadow: unset; + } + &.ch2-btn-secondary { + &.ch2-allow-all-btn { + background-color: var(--pi-color-primary); + &:hover { + background-color: var(--pi-color-primary-900); + } + } + &.ch2-deny-all-btn { + background-color: var(--pi-color-warn); + &:hover { + background-color: var(--pi-color-warn-900); + } + } + } + } + .ch2-btn + .ch2-btn { + margin-left: var(--pi-spacing-x-small); + } + + .ch2-btn-primary { + &.ch2-deny-all-btn { + color: var(--pi-color-warn); + background-color: transparent; + text-decoration: underline; + &:hover { + &:enabled { + color: var(--pi-color-warn-900); + text-decoration: underline; + } + } + } + &.ch2-allow-all-btn { + width: 9rem; + color: var(--pi-color-white); + background-color: var(--pi-color-primary); + &:hover { + &:enabled { + background-color: var(--pi-color-primary-900); + } + } + } + } + .ch2-style-light { + .ch2-btn-secondary { + &:hover { + background: unset; + } + } + } + .ch2-btn-text { + color: var(--pi-color-black); + background-color: transparent; + text-decoration: underline; + &:hover { + &:enabled { + color: var(--pi-color-black); + text-decoration: underline; + } + } + &:disabled { + color: rgba($color: #3d3d3d, $alpha: 0.6); + } + } + .ch2-icon { + display: none; + position: fixed; + bottom: 2.5rem; + left: 2.5rem; + z-index: 9999; + } + .ch2-btn { + .ch2-open-settings-btn { + .ch2-btn-text { + color: var(--pi-color-warn-900); + } + } + } + .ch2-theme-bar { + .ch2-dialog-actions { + @media screen and (min-width: 30rem) { + gap: var(--pi-spacing-small); + } + + button { + &:first-child { + margin-right: unset; + } + &.ch2-btn-text-xxs { + font: { + size: var(--pi-font-size-small); + } + } + } + } + .ch2-dialog-actions-vertical { + div { + gap: var(--pi-spacing-small); + &:last-child { + button { + margin-bottom: unset !important; + margin-left: unset; + padding: var(--pi-spacing-xx-small) var(--pi-spacing-x-large); + } + } + } + } + } + .ch2-settings-content { + .ch2-settings-tab-container { + & > button { + margin-right: unset; + } + } + } + .ch2-theme-bar { + .ch2-dialog { + p { + strong { + color: var(--pi-color-grey); + font: { + size: var(--pi-font-size-small); + weight: 700; + } + } + } + } + } + .ch2-style-light { + &.ch2-icon { + a { + color: var(--pi-color-primary); + svg { + border: 0.125rem solid var(--pi-color-primary); + fill: var(--pi-color-primary); + background: var(--pi-color-white); + } + } + } + .ch2-btn-text { + color: var(--pi-color-grey); + } + .ch2-dialog-description { + margin-top: var(--pi-spacing-xxx-small); + } + .ch2-dialog-content { + p { + color: var(--pi-color-grey); + line-height: 150%; + } + } + } + .ch2-icon { + bottom: 0.375rem; + left: 0.375rem; + a { + font: { + size: var(--pi-font-size-small); + weight: unset; + } + text-transform: unset; + } + } + + @media screen and (max-width: 700px) { + .ch2-theme-bar { + .ch2-dialog-content { + text-align: center; + } + .ch2-dialog-actions { + button { + margin-top: unset; + } + } + .ch2-dialog-actions { + button { + margin-top: unset; + } + } + .ch2-dialog-actions-vertical { + display: flex; + @media (max-width: 30rem) { + flex-direction: column; + } + & > div { + display: flex; + justify-content: center; + align-items: center; + margin-top: var(--pi-spacing-large); + + &:nth-child(1) { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single) { + min-width: unset; + width: 50%; + } + } + } + } + } + } + } + @media (max-width: 700px) { + .ch2-theme-bar { + .ch2-dialog-actions { + margin-top: var(--pi-spacing-xxx-small); + } + } + } + + @media screen and (min-width: 700px) { + .ch2-theme-bar { + .ch2-dialog-actions-vertical { + & > div { + &:nth-child(2) { + justify-content: flex-end; + width: -webkit-fill-available; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-checkbox.scss b/libs/ui/src/lib/styles/common/vendors/_mat-checkbox.scss new file mode 100644 index 0000000..ad2ccd3 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-checkbox.scss @@ -0,0 +1,177 @@ +@mixin checkboxColor($color) { + .mat-mdc-checkbox { + .mdc-checkbox__native-control { + // Colors by state + &:enabled { + &:not(:checked):not(:indeterminate):not([data-indeterminate='true']) { + & ~ .mdc-checkbox__background { + border-color: var(--pi-color-#{$color}) !important; + } + } + } + } + } +} + +.pi-shell, +.cdk-overlay-container { + .pi-select-option { + @include checkboxColor(primary-200); + } + @include checkboxColor(grey-100); + + // Checkbox + .mdc-checkbox__background { + border-radius: var(--pi-border-radius-x-small); + } + .mdc-checkbox__background { + border: 1px solid; + } + + .mat-mdc-checkbox { + &.mat-accent { + --mdc-checkbox-selected-checkmark-color: #3d3d3d; + --mdc-checkbox-selected-focus-icon-color: #799ef0; + --mdc-checkbox-selected-hover-icon-color: #799ef0; + --mdc-checkbox-selected-icon-color: #799ef0; + --mdc-checkbox-selected-pressed-icon-color: #799ef0; + --mdc-checkbox-unselected-focus-icon-color: #3d3d3d; + --mdc-checkbox-unselected-hover-icon-color: #3d3d3d; + --mdc-checkbox-disabled-selected-icon-color: rgba(196, 213, 255, 0.15); + --mdc-checkbox-disabled-unselected-icon-color: rgba(196, 213, 255, 0.15); + --mdc-checkbox-unselected-icon-color: rgba(196, 213, 255, 0); + --mdc-checkbox-unselected-pressed-icon-color: rgba(196, 213, 255, 0.15); + --mdc-checkbox-selected-checkmark-color: rgba(227, 246, 249, 1); + } + .mdc-checkbox__ripple { + background: var(--pi-color-primary-200-rgba); + } + .mdc-form-field { + gap: var(--pi-spacing-xx-small); + color: var(--pi-color-black); + } + .mdc-checkbox { + padding: calc( + (var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - 1.125rem) / 2 + ); + margin: calc( + ( + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) + ) / 2 + ); + .mdc-checkbox__background { + top: calc((var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - 1.125rem) / 2); + left: calc( + (var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - 1.125rem) / 2 + ); + } + .mdc-checkbox__native-control { + top: calc( + ( + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) + ) / 2 + ); + right: calc( + ( + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) + ) / 2 + ); + left: calc( + ( + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) - + var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)) + ) / 2 + ); + width: var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)); + height: var(--mdc-checkbox-state-layer-size, var(--pi-spacing-x-large)); + + // Colors by state + &:enabled { + &:not(:checked):not(:indeterminate):not([data-indeterminate='true']) { + & ~ .mdc-checkbox__background { + border: 1px solid; + } + } + &:checked { + & ~ .mdc-checkbox__background { + border-color: var(--pi-color-primary); + background-color: var(--pi-color-white); + } + } + & ~ .mdc-checkbox__background { + .mdc-checkbox__checkmark { + color: var(--pi-color-primary); + transform: scale(0.7); + .mdc-checkbox__checkmark-path { + stroke-width: 0.3125rem; + } + } + } + } + } + &:hover { + .mdc-checkbox__native-control { + &:enabled { + &:not(:checked):not(:indeterminate):not([data-indeterminate='true']) { + & ~ .mdc-checkbox__background { + border-color: var(--pi-color-primary); + } + } + &:checked { + & ~ .mdc-checkbox__background { + border-color: var(--pi-color-primary); + background-color: var(--pi-color-white); + } + } + } + } + } + } + // Checked + &.mat-mdc-checkbox-checked { + .mdc-checkbox { + background-color: var(--pi-color-primary-50); + border-radius: 0.4375rem; + } + } + } + + // Pseudo checkbox + .mat-pseudo-checkbox { + border-radius: var(--pi-border-radius-x-small); + } + .mat-pseudo-checkbox-full { + border: 0.0625rem solid; + } + .mat-pseudo-checkbox-full { + &.mat-pseudo-checkbox-checked { + &::after { + content: ''; + top: -2px; + height: 0.2188rem; + width: 0.4rem; + } + &::before { + content: ''; + position: absolute; + top: -0.25rem; + left: -0.25rem; + z-index: -1; + height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + padding: var(--pi-spacing-xxx-small); + border-radius: 0.4375rem; + border-color: var(--pi-color-primary); + background-color: var(--pi-color-primary-50); + } + } + } + + .mat-mdc-checkbox-ripple, + .mdc-checkbox__ripple { + display: none; + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-chip.scss b/libs/ui/src/lib/styles/common/vendors/_mat-chip.scss new file mode 100644 index 0000000..6efb17e --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-chip.scss @@ -0,0 +1,49 @@ +.pi-layout { + .mat-mdc-chip { + &.mat-mdc-standard-chip { + color: var(--pi-color-primary); + padding: var(--pi-spacing-xx-small) var(--pi-spacing-xxx-small) var(--pi-spacing-xx-small) + var(--pi-spacing-medium); + gap: var(--pi-spacing-xxxx-small); + background-color: transparent; + border: 1px solid; + border-color: var(--pi-color-primary); + .pi-button__icon { + max-height: 0.3125rem; + figure { + width: var(--pi-spacing-small); + transition: all 0.2s ease-in-out; + } + } + &:hover { + cursor: pointer; + &::after { + opacity: 0; + } + .pi-button__icon { + figure { + transform: rotate(90deg); + } + } + } + } + + button { + &.pi-button { + &.pi-button__primary { + &:not(.pi-button__appearance--ghost):not(.pi-button__appearance--outline):not( + .pi-button__white + ) { + background-color: transparent; + } + } + } + } + } + .mat-mdc-chip-ripple { + font: { + size: var(--pi-font-size-medium); + weight: 300; + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-dialog.scss b/libs/ui/src/lib/styles/common/vendors/_mat-dialog.scss new file mode 100644 index 0000000..1f15cb7 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-dialog.scss @@ -0,0 +1,292 @@ +@mixin mainContentHeight { + .pi-modal__main-content { + display: block; + & > *:first-child { + display: flex; + flex-direction: column; + justify-content: space-between; + overflow: hidden; + padding: 0; + } + // Custom + &.pi-modal__main-content--custom-template { + & > *:first-child { + min-height: 60vh; + } + } + // Dialog + &.pi-modal__main-content--dialog-template { + } + } +} + +@mixin modalScrollBar { + &::-webkit-scrollbar { + width: var(--pi-spacing-xxxx-small); + } + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba($color: #aeaeb6, $alpha: 0.3); + border-radius: var(--pi-border-radius-small); + &:hover { + background-color: darken($color: #aeaeb6, $amount: 0.5); + } + } +} +.cdk-overlay-container, +.pi-shell { + .mdc-dialog__container { + display: flex; + align-items: stretch; + justify-content: center; + padding: 0; + border-radius: var(--pi-border-radius-medium); + box-shadow: unset; + overflow: hidden; + overflow-y: auto; + .mdc-dialog__surface { + display: flex; + flex-direction: column; + flex-grow: 1; + height: 100%; + box-shadow: unset; + } + } +} +.pi-modal__panel { + .pi-modal { + &:not(.pi-modal__custom) { + .pi-headline { + figure { + max-width: 10rem; + } + } + } + &.pi-modal__dialog { + .pi-panel__grey { + max-width: 75%; + margin: 0 auto; + } + } + + .pi-modal__main-content { + // Main DIALOG content + &.pi-modal__main-content--dialog-template { + & > .pi-modal__custom-template { + display: flex; + flex-direction: column; + height: 100%; + & > *:nth-child(1) { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-x-large); + flex-grow: 1; + overflow-y: auto; + @include modalScrollBar; + } + & > *:nth-child(2) { + height: var(--pi-height-footer); + } + } + } + + // Main CUSTOM content + &.pi-modal__main-content--custom-template { + & > .pi-modal__custom-template { + display: flex; + flex-direction: column; + height: 100%; + & > *:nth-child(1) { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-x-large); + flex-grow: 1; + overflow-y: auto; + + @include modalScrollBar; + + & > *:nth-child(n + 1) { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + } + & > *:nth-child(2) { + height: 5.5rem; + } + } + } + } + } + + .mat-mdc-checkbox-layout { + white-space: unset; + } + // Full + &.pi-modal__panel--full { + width: 90vw; + height: 90vh; + .mat-mdc-dialog-container { + border-radius: var(--pi-border-radius-large); + } + } +} + +// Backdrop +.pi-backdrop { + background: rgba(0, 0, 0, 0.4); +} + +html { + // Mobile + &.is-mobile { + .pi-modal { + & > .pi-headline { + gap: var(--pi-spacing-medium); + } + @include mainContentHeight; + } + // Panel + .pi-modal__panel { + // Small | Medium + &.pi-modal__panel--small, + &.pi-modal__panel--medium { + width: 83vw; + + .pi-modal__main-content { + // Main CUSTOM content + &.pi-modal__main-content--custom-template { + & > .pi-modal__custom-template { + & > *:nth-child(1) { + max-height: calc(85vh - var(--pi-height-footer) - 5rem); + } + } + } + } + } + // Action wrapper + .pi-wrapper { + &.pi-wrapper__action { + background-color: rgba(255, 255, 255, 0.8); + box-shadow: 0 -6px 34px -17px rgba(191, 191, 191, 0.4); + } + } + } + .pi-modal__main-content { + &.pi-modal__main-content--custom-template { + video { + max-width: 85%; + margin: auto; + } + } + } + } + + // Tablet + &.is-tablet { + .pi-modal__panel { + @include mainContentHeight; + // Small + &.pi-modal__panel--small { + width: 50vw; + } + + // Medium + &.pi-modal__panel--medium { + width: 70vw; + } + } + + &.is-tablet__small { + .pi-modal__main-content { + &.pi-modal__main-content--custom-template { + video { + max-width: 75%; + margin: auto; + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + .pi-modal { + // Type Custom + &.pi-modal__custom { + // Is Closable + &.pi-modal__is-closable { + } + + // Is Not Closable + &:not(.pi-modal__is-closable) { + .pi-modal__custom--inner { + main { + & > *:nth-child(2) { + display: flex; + flex-direction: column; + justify-content: center; + } + } + } + } + } + // Type Dialog + &.pi-modal__dialog { + // Is Closable + &.pi-modal__is-closable { + } + // Is Not Closable + &:not(.pi-modal__is-closable) { + & > main { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + } + } + } + } + // Large device + @media screen and (min-width: 62rem) { + .pi-modal__main-content { + &.pi-modal__main-content--custom-template { + height: 100%; + + video { + max-width: 66.66%; + margin: auto; + } + } + } + } + } + + // Desktop + &.is-desktop { + .pi-modal { + // Type Dialog + &.pi-modal__dialog { + & > main { + & > .pi-headline + section { + margin-top: var(--pi-spacing-x-large); + } + } + } + @include mainContentHeight; + } + .pi-modal__panel { + // Small + &.pi-modal__panel--small { + width: 28vw; + } + + // Medium + &.pi-modal__panel--medium { + width: 37vw; + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-expansion-panel.scss b/libs/ui/src/lib/styles/common/vendors/_mat-expansion-panel.scss new file mode 100644 index 0000000..7f14a24 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-expansion-panel.scss @@ -0,0 +1,13 @@ +.pi-shell__deale-saas { + .mat-mdc-expansion-panel { + &:not([class*='mat-elevation-z']) { + box-shadow: unset; + } + } + .mat-mdc-expansion-panel-header { + border-bottom: 1px solid var(--pi-color-background-400); + } + .mat-mdc-expansion-panel-body { + padding: var(--pi-spacing-xx-large); + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-form-field.scss b/libs/ui/src/lib/styles/common/vendors/_mat-form-field.scss new file mode 100644 index 0000000..3b03a1e --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-form-field.scss @@ -0,0 +1,428 @@ +// Appearance outline +@mixin matFormFieldOutline() { + // Appearance Outline + .mat-form-field-appearance-outline { + .mat-mdc-form-field-infix { + transform: translateX(-0.75rem); + } + .mat-mdc-form-field-infix { + min-height: unset; + } + // Focused + &.mat-focused { + [class*='mdc-notched-outline__'] { + max-width: unset; + width: var(--pi-spacing-small); + border-width: 1px; + } + } + + // NOT Invalid + &:not(.mat-form-field-invalid) { + &:not(.mat-focused) { + .mat-mdc-form-field-icon-suffix, + .mat-mdc-form-field-icon-prefix { + color: var(--pi-color-grey-300); + } + } + } + + .mdc-notched-outline { + .mdc-floating-label { + height: var(--pi-spacing-xxxx-large); + } + } + .mdc-text-field { + &:not(.mat-form-field-no-animations) { + .mdc-floating-label { + transform: translate(0, -3rem) scale(1); + } + } + } + + .mat-mdc-form-field-flex { + padding: 0 var(--pi-spacing-small); + } + + .mdc-notched-outline__leading { + border-radius: 0.875rem 0 0 0.875rem; + } + .mdc-notched-outline__trailing { + border-radius: 0 0.875rem 0.875rem 0; + } + + .mat-mdc-form-field-label { + margin-left: var(--pi-spacing-xxxx-small); + margin-right: var(--pi-spacing-xxxx-small); + } + + .mat-mdc-form-field-prefix, + .mat-mdc-form-field-suffix { + top: unset; + height: 100%; + .pi-icon__small { + .mat-icon { + height: var(--pi-spacing-x-large); + } + } + } + + .mat-mdc-form-field-infix { + flex: auto; + min-width: 0; + width: 11.25rem; + position: relative; + box-sizing: border-box; + height: var(--pi-spacing-xxxx-large); + } + + input { + &.mat-mdc-input-element { + height: calc(var(--pi-spacing-xxxx-large) - 0.125rem); + margin-top: 0.0625rem; + } + } + + &.mdc-text-field--disabled { + .mat-mdc-input-element, + .mdc-notched-outline, + .mat-icon { + color: var(--pi-color-grey-300); + } + } + + // Invalid + &.mat-form-field-invalid { + .mat-mdc-form-field-subscript-wrapper { + height: var(--pi-spacing-x-large); + } + } + // NOT Invalid + &:not(.mat-form-field-invalid) { + .mat-mdc-form-field-subscript-wrapper { + height: var(--pi-spacing-x-small); + } + } + + // Outline & default + &:not(.mat-focused):not(.mat-form-field-invalid) { + &:not(.mdc-text-field--disabled) { + [class*='mdc-notched-outline__'] { + border-color: var(--pi-color-grey-200); + } + } + &:hover { + &:not(.mdc-text-field--disabled) { + [class*='mdc-notched-outline__'] { + border-color: var(--pi-color-grey-300); + } + } + } + } + // Outline & Invalid + &.mat-focused { + &:not(.mdc-text-field--disabled) { + [class*='mdc-notched-outline__'] { + border-color: var(--pi-color-primary); + } + } + } + // Outline & Invalid + &.mat-form-field-invalid { + &:not(.mdc-text-field--disabled) { + [class*='mdc-notched-outline__'] { + border-color: var(--pi-color-warn); + } + } + } + } +} + +// Appearance fill +@mixin matFormFieldFill() { + .mat-form-field-appearance-fill { + .mat-mdc-text-field-wrapper { + height: unset; + flex: unset; + &:not(.mdc-text-field--outlined) { + .mat-mdc-form-field-infix { + padding-top: unset; + padding-bottom: unset; + } + } + } + // NOT Invalid + &:not(.mat-form-field-invalid) { + &:not(.mat-focused) { + .mat-mdc-form-field-icon-suffix, + .mat-mdc-form-field-icon-prefix { + color: var(--pi-color-grey-900); + } + } + } + .mdc-text-field { + padding: unset; + } + .mat-mdc-form-field-infix { + width: 100%; + } + .mdc-text-field--filled { + .mdc-line-ripple { + &::after { + border-bottom-width: 1px; + } + } + + &.mdc-text-field--disabled { + .mdc-line-ripple { + &::before { + border-bottom-color: transparent; + } + } + } + .mdc-floating-label--float-above { + transform: translateY(-100%) scale(1); + } + } + + .mdc-text-field--filled { + background-color: var(--pi-color-white); + &:not(.mdc-text-field--disabled) { + background-color: transparent; + } + } + + &.mdc-text-field--disable { + .mdc-line-ripple { + background-image: unset; + background-size: unset; + background-repeat: unset; + } + } + .mat-mdc-floating-label { + font: { + size: var(--pi-font-size-x-small); + } + } + + .mat-mdc-form-field-subscript-wrapper { + display: none; + } + } +} + +// State colors +@mixin matFormFieldStateColors { + .mdc-text-field { + &:not(.mdc-text-field--disabled) { + .mdc-text-field__input { + color: var(--pi-color-black); + } + } + &:not(.mdc-text-field--disabled):not(.mdc-text-field--focused) { + .mdc-floating-label { + color: var(--pi-color-grey-300); + } + } + &.mdc-text-field--invalid { + &:not(.mdc-text-field--disabled) { + .mdc-floating-label { + color: var(--pi-color-warn); + } + } + } + } + + // Invalid + &.mat-form-field-invalid { + .mat-mdc-form-field-icon-suffix, + .mat-mdc-form-field-icon-prefix, + .mat-mdc-form-field-infix { + color: var(--pi-color-warn); + } + // Focused + &.mat-focused { + .mat-mdc-form-field-icon-suffix, + .mat-mdc-form-field-icon-prefix, + .mat-mdc-form-field-infix { + color: var(--pi-color-warn); + } + } + } + // NOT Invalid + &:not(.mat-form-field-invalid) { + // Focused + &.mat-focused { + .mat-mdc-form-field-icon-suffix, + .mat-mdc-form-field-icon-prefix, + .mat-mdc-form-field-infix { + color: var(--pi-color-primary); + } + } + } + + .mat-mdc-form-field-error { + color: var(--pi-color-warn); + } + // NOT Has Error + &:not(.has-error) { + // Type Fill + .mat-form-field-appearance-fill { + .mdc-text-field--filled { + &:not(.mdc-text-field--disabled) { + .mdc-line-ripple { + &::before { + border-bottom-color: var(--pi-color-grey-100); + } + } + &:hover { + .mdc-line-ripple { + &::before { + border-bottom-color: var(--pi-color-grey-200); + } + } + } + } + } + } + } + // Has Error + &.has-error { + // Type Fill + .mat-form-field-appearance-fill { + .mdc-text-field--filled { + &:not(.mdc-text-field--disabled) { + .mdc-line-ripple { + &::before { + border-bottom-color: var(--pi-color-warn-200); + } + } + &:hover { + .mdc-line-ripple { + &::before { + border-bottom-color: var(--pi-color-warn); + } + } + } + } + } + } + } + // Placeholder + .mat-mdc-form-field-infix { + .mat-mdc-input-element { + &::placeholder { + color: var(--pi-color-grey-300); + -webkit-text-fill-color: var(--pi-color-grey-300); + } + } + } +} + +.pi-shell, +.cdk-overlay-container { + .pi-field { + // Common + .mat-mdc-form-field { + width: 100%; + line-height: 1.65; + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + } + &:focus, + &:hover { + background: none; + } + + [class*='mdc-notched-outline__'] { + background-color: var(--pi-color-white); + z-index: -99; + } + + .mat-mdc-form-field-focus-overlay { + background-color: unset; + } + + // Outlined + .mdc-text-field--outlined { + .mdc-notched-outline { + z-index: -99; + } + .mdc-notched-outline--upgraded { + .mdc-floating-label--float-above { + font : { + size: var(--pi-font-size-x-small); + } + } + } + } + + .mdc-notched-outline--notched { + margin-left: unset; + -webkit-clip-path: unset; + clip-path: unset; + .mdc-notched-outline__notch { + border-top: 1px solid; + } + } + + .mat-mdc-form-field-subscript-wrapper { + display: flex; + } + + .mat-mdc-text-field-wrapper { + &.mdc-text-field--outlined { + .mat-mdc-form-field-infix { + padding-top: 0; + padding-bottom: 0; + } + } + } + } + + // Appearance + // Outline + @include matFormFieldOutline; + + // Fill + @include matFormFieldFill; + + // State colors + @include matFormFieldStateColors; + + // Affix + .mat-mdc-form-field-prefix, + .mat-mdc-form-field-suffix { + .mat-icon { + line-height: 100%; + } + } + + // Error + .mat-mdc-form-field-error { + font : { + size: var(--pi-font-size-x-small); + } + } + + // Hint & Error + .mat-mdc-form-field-hint-wrapper, + .mat-mdc-form-field-error-wrapper { + padding: 0 var(--pi-spacing-xx-small); + } + // Exclude incremental + &:not(.pi-form-field__incremental) { + // Type Fill + .mat-form-field-appearance-fill { + input { + &.mat-mdc-input-element { + all: unset; + height: unset; + width: -webkit-fill-available; + margin-top: 1.875rem; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-menu.scss b/libs/ui/src/lib/styles/common/vendors/_mat-menu.scss new file mode 100644 index 0000000..e68bde4 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-menu.scss @@ -0,0 +1,52 @@ +.pi-shell, +.cdk-overlay-container { + .mat-mdc-menu-panel { + border-radius: var(--pi-border-radius-large); + background-color: rgba($color: white, $alpha: 0.96); + box-shadow: var(--pi-box-shadow-panel); + transition: var(--pi-animation); + transition-duration: 0.15s; + } + .mat-mdc-menu-item { + line-height: 3.4375rem; + height: 3.4375rem; + padding: 0 var(--pi-spacing-x-large); + color: var(--pi-color-highlighted); + transition: var(--pi-animation); + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + size: var(--pi-font-size-small); + } + &:hover:not([disabled]), + &.cdk-program-focused, + &.cdk-keyboard-focused, + .mat-mdc-menu-item-highlighted { + &:not([disabled]) { + color: var(--pi-color-primary); + background: var(--pi-color-primary-50); + } + } + } + .mat-mdc-menu-panel { + min-height: unset; + } + .mat-mdc-menu-content { + &:not(:empty) { + padding-top: unset; + padding-bottom: unset; + } + } +} + +html { + &.is-mobile { + .mat-mdc-menu-item { + width: 10rem; + } + } + &:not(.is-mobile) { + .mat-mdc-menu-item { + width: 12rem; + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-progress-bar.scss b/libs/ui/src/lib/styles/common/vendors/_mat-progress-bar.scss new file mode 100644 index 0000000..6d08cb7 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-progress-bar.scss @@ -0,0 +1,26 @@ +.pi-shell { + // Common + .mat-mdc-progress-bar { + height: 0.1875rem; + border-radius: 0.0938rem; + // Type + // Fill + &.mat-mdc-progress-bar-fill { + &::after { + background: var(--pi-color-primary); + } + } + } + // Progress bar in dynamic menu + .pi-dynamic-menu { + .mat-mdc-progress-bar { + height: 0.375rem; + border-radius: 0.125rem; + } + .mdc-linear-progress__bar-inner { + border-style: solid; + border-image: linear-gradient(45deg, var(--pi-color-primary), var(--pi-color-highlighted)) 1; + border-top-width: var(--mdc-linear-progress-track-height, .375rem); + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-progress-spinner.scss b/libs/ui/src/lib/styles/common/vendors/_mat-progress-spinner.scss new file mode 100644 index 0000000..c3b446e --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-progress-spinner.scss @@ -0,0 +1,5 @@ +.pi-shell { + .mat-mdc-progress-spinner { + --mdc-circular-progress-active-indicator-color: #799ef0; + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-radio-button.scss b/libs/ui/src/lib/styles/common/vendors/_mat-radio-button.scss new file mode 100644 index 0000000..08f4f09 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-radio-button.scss @@ -0,0 +1,239 @@ +.pi-layout { + .mat-mdc-radio-group { + display: flex; + gap: var(--pi-spacing-x-large); + & > * { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--pi-spacing-xxx-small); + } + .mat-mdc-radio-button { + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + } + } + } + + .mdc-radio__outer-circle { + border-width: 0.0625rem; + } + + .mat-mdc-radio-button { + &.mat-mdc-radio-checked { + .mat-ripple-element { + background-color: var(--pi-color-primary); + } + } + .mdc-radio { + // Default + .mdc-radio__native-control { + &:enabled { + &:not(:checked) { + & + .mdc-radio__background { + .mdc-radio__outer-circle { + border-color: var(--pi-color-grey-100); + } + } + } + &:checked { + & + .mdc-radio__background { + .mdc-radio__outer-circle, + .mdc-radio__inner-circle { + border-color: var(--pi-color-primary); + } + } + } + } + } + // Hover + &:hover { + .mdc-radio__native-control { + &:enabled { + &:checked { + & + .mdc-radio__background { + .mdc-radio__outer-circle { + border-color: var(--pi-color-primary); + } + } + } + &:not(:checked) { + & + .mdc-radio__background { + .mdc-radio__outer-circle { + border-color: var(--pi-color-grey-200); + } + } + } + } + } + } + // Not disabled + &:not(:disabled) { + &:active { + .mdc-radio__native-control { + &:enabled { + &:checked { + & + .mdc-radio__background { + .mdc-radio__outer-circle, + .mdc-radio__inner-circle { + border-color: var(--pi-color-primary-900); + } + } + } + } + } + } + } + } + &.mat-accent { + --mdc-radio-disabled-selected-icon-color: #3d3d3d; + --mdc-radio-disabled-unselected-icon-color: #3d3d3d; + --mdc-radio-unselected-focus-icon-color: #799ef0; + --mdc-radio-unselected-hover-icon-color: #799ef0; + --mdc-radio-unselected-icon-color: rgba(196, 213, 255, 0.15); + --mdc-radio-unselected-pressed-icon-color: rgba(196, 213, 255, 0.15); + --mdc-radio-selected-focus-icon-color: #799ef0; + --mdc-radio-selected-hover-icon-color: #799ef0; + --mdc-radio-selected-icon-color: #799ef0; + --mdc-radio-selected-pressed-icon-color: #799ef0; + --mat-mdc-radio-ripple-color: #3d3d3d; + --mat-mdc-radio-checked-ripple-color: #799ef0; + } + &.mat-mdc-accent { + &.mat-mdc-radio-checked { + .mdc-radio__outer-circle { + border-color: var(--pi-color-primary); + } + } + } + &.mat-mdc-radio-checked { + .mdc-form-field { + font: { + weight: 700; + } + } + } + } +} + +.pi-wrapper__form { + .mat-mdc-radio-group { + .pi-list-item { + .mat-mdc-radio-button { + label { + white-space: normal; + } + } + } + } + // Mat Radio Group in Flex Direction Column Wrapper + .pi-wrapper__flex-direction--column { + .mat-mdc-radio-group { + flex-direction: column; + } + } +} + +.pi-wrapper__grid--two { + .mat-mdc-radio-label { + min-width: 9rem; + } +} + +.mat-mdc-radio-label-content { + white-space: pre-wrap; + word-break: break-word; +} + +html { + // Mobile + &.is-mobile { + .pi-layout { + .mat-mdc-radio-group { + flex-direction: column; + gap: var(--pi-spacing-small); + & > * { + display: flex; + flex-direction: column; + align-items: flex-start; + } + } + } + + .pi-wrapper__form { + .mat-mdc-radio-group { + margin: var(--pi-spacing-xx-large) auto; + gap: var(--pi-spacing-xx-large); + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + } + } + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + .pi-layout { + .mat-mdc-radio-group { + gap: var(--pi-spacing-medium); + [class*='pi-panel__width--'] { + margin: auto; + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .mat-mdc-radio-group { + &.pi-wrapper__grid--two { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: var(--pi-spacing-x-large); + + & > *:nth-child(n + 1) { + & > *:nth-child(n + 1) { + max-width: 70%; + &:not(.mat-mdc-radio-label) { + display: block; + margin-top: var(--pi-spacing-xxx-small); + } + } + } + } + } + } + // Tablet L & Desktop S & M + @media screen and (min-width: 62rem) and (max-width: 120rem) { + .pi-layout { + .mat-mdc-radio-group { + gap: var(--pi-spacing-large); + } + } + } + } + // Desktop + &.is-desktop { + // Common + // Desktop S + @media screen and (max-width: 90rem) { + } + // Desktop M + @media screen and (min-width: 90rem) and (max-width: 120rem) { + } + // Desktop L + @media screen and (min-width: 120rem) { + .pi-layout { + .mat-mdc-radio-group { + gap: var(--pi-spacing-x-large); + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-slider.scss b/libs/ui/src/lib/styles/common/vendors/_mat-slider.scss new file mode 100644 index 0000000..cf0deaa --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-slider.scss @@ -0,0 +1,149 @@ +@mixin sliderChevron($bottom: 1.975rem) { + .pi-slider { + // Flex direction + &.pi-slider__flex-direction--column-reverse { + .mdc-slider { + .mdc-slider__value-indicator-container { + bottom: -1.875rem; + .mdc-slider__value-indicator { + &::before { + bottom: $bottom; + } + } + } + } + } + } +} + +.pi-slider { + .mat-mdc-slider { + display: flex; + width: -webkit-fill-availabe; + + .mdc-slider__thumb { + .mdc-slider__value-indicator-container { + border-radius: 50% 50% 0; + .mdc-slider__value-indicator { + border-radius: var(--pi-border-radius-small); + transform: scale(1); + background-color: var(--pi-color-primary); + opacity: 1; + &::before { + border-top-color: var(--pi-color-primary); + } + .mdc-slider__value-indicator-text { + white-space: nowrap; + } + } + } + } + } + // Type + // Default + &:not(.pi-slider__range) { + .mat-mdc-slider { + .mat-mdc-slider { + margin-left: var(--pi-spacing-medium); + margin-right: var(--pi-spacing-medium); + } + } + } + + // Range + &.pi-slider__range { + .mat-mdc-slider { + margin-left: var(--pi-spacing-xx-large); + margin-right: var(--pi-spacing-xx-large); + } + } + + // Flex direction + &.pi-slider__flex-direction--column-reverse { + .mdc-slider { + .mdc-slider__value-indicator-container { + .mdc-slider__value-indicator { + &::before { + border-top: unset; + border-bottom: 0.375rem solid var(--pi-color-primary); + } + } + } + } + } + // Disabled + &.is-disabled { + .mat-mdc-slider { + &.mdc-slider--disabled { + .mdc-slider__track--active_fill { + border-color: var(--pi-color-primary-200); + } + .mdc-slider__thumb-knob { + background-color: var(--pi-color-primary-200); + border-color: var(--pi-color-primary-200); + } + } + .mdc-slider__input { + pointer-events: none; + } + } + .mdc-slider { + .mdc-slider__value-indicator-container { + display: none; + } + } + } +} + +.pi-card { + .pi-slider { + // Range + &.pi-slider__range { + .mat-mdc-slider { + margin-bottom: unset; + } + } + } +} + +// Slider in dynamic menu +.pi-dynamic-menu { + .pi-slider { + .mat-mdc-slider { + margin-top: var(--pi-spacing-xx-large); + margin-left: var(--pi-spacing-x-large); + margin-right: var(--pi-spacing-xx-large); + } + } +} + +html { + // Mobile + &.is-mobile { + @include sliderChevron; + .pi-slider { + // Default + &:not(.pi-slider__range) { + .mat-mdc-slider { + width: calc( + 100vw - var(--pi-spacing-xxxx-large) - var(--pi-spacing-xx-large) - + var(--pi-spacing-xx-large) + ); + } + } + } + .pi-list-item { + &.is-stroked { + .pi-slider { + .mat-mdc-slider { + width: calc( + 100vw - var(--pi-spacing-xxxx-large) - var(--pi-spacing-xx-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-large) - var(--pi-spacing-medium) - + var(--pi-spacing-medium) + ); + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-snackbar.scss b/libs/ui/src/lib/styles/common/vendors/_mat-snackbar.scss new file mode 100644 index 0000000..c7363d1 --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-snackbar.scss @@ -0,0 +1,101 @@ +@mixin snackbarWidth($width: 80vw) { + .cdk-overlay-container { + .mat-mdc-snack-bar-container { + &.pi-snackbar { + width: $width; + max-width: $width; + } + } + } + .pi-layout { + .pi-snackbar { + width: $width; + max-width: $width; + } + } +} + +.cdk-overlay-container { + .mat-mdc-snack-bar-container { + background: unset; + margin: var(--pi-spacing-x-large); + &.pi-snackbar { + padding: var(--pi-spacing-large) var(--pi-spacing-x-large); + border-radius: var(--pi-border-radius-large); + box-shadow: var(--pi-box-shadow); + border: 1px solid; + &.pi-snackbar__info { + color: var(--pi-color-primary); + background-color: var(--pi-color-primary-25); + border-color: var(--pi-color-primary-50); + } + &.pi-snackbar__error { + color: var(--pi-color-warn); + background-color: var(--pi-color-warn-25); + border-color: var(--pi-color-warn-50); + } + &.pi-snackbar__success { + color: var(--pi-color-success); + background-color: var(--pi-color-success-25); + border-color: var(--pi-color-success-50); + } + } + .mdc-snackbar__surface { + background-color: transparent; + box-shadow: unset; + } + .mdc-snackbar__label { + padding-left: unset; + padding-right: unset; + padding-top: unset; + padding-bottom: unset; + span { + text-align: left; + line-height: 130%; + } + } + } +} + +.pi-layout { + .pi-snackbar { + padding: var(--pi-spacing-medium) var(--pi-spacing-x-large); + border-radius: var(--pi-border-radius-large); + border: 1px solid; + } +} + +// Mobile +@media screen and (max-width: 40rem) { + @include snackbarWidth; + .cdk-overlay-container { + .mat-mdc-snack-bar-handset { + .mat-mdc-snack-bar-container { + &.pi-snackbar { + margin: var(--pi-spacing-x-small) auto; + } + } + } + } +} + +// Tablet +@media screen and (min-width: 40rem) and (max-width: 77.5rem) { + @include snackbarWidth(66.66vw); +} + +// Desktop +// Desktop S +@media screen and (min-width: 77.5rem) and (max-width: 90rem) { + @include snackbarWidth(33.33vw); +} + +// Desktop M +@media screen and (min-width: 90rem) and (max-width: 120rem) { + @include snackbarWidth(33.33vw); +} + +// Desktop L +@media screen and (min-width: 120rem) { + @include snackbarWidth(25vw); +} diff --git a/libs/ui/src/lib/styles/common/vendors/_mat-tooltip.scss b/libs/ui/src/lib/styles/common/vendors/_mat-tooltip.scss new file mode 100644 index 0000000..4c2a27f --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_mat-tooltip.scss @@ -0,0 +1,81 @@ +.cdk-overlay-container { + .mat-mdc-tooltip { + margin: var(--pi-spacing-xxx-small); + padding: var(--pi-spacing-xx-small) var(--pi-spacing-small); + box-shadow: var(--pi-box-shadow-panel-double); + border-radius: var(--pi-border-radius-medium); + .mdc-tooltip__surface { + text-align: center; + } + } +} + +.pi-tooltip { + &::after { + content: ''; + position: absolute; + left: calc(50% - 0.5625rem); + top: -0.5rem; + width: 0; + height: 0; + border-left: 0.5625rem solid transparent; + border-right: 0.5625rem solid transparent; + transform: rotate(180deg); + clear: both; + } + + font: { + family: 'Soehne', Helvetica, Arial, sans-serif; + size: var(--pi-font-size-x-small); + weight: 300; + } + // Color + &.pi-tooltip__white { + background-color: var(--pi-color-background-400); + + &::after { + border-top: 0.5rem solid var(--pi-color-background-400); + } + + .mdc-tooltip__surface { + color: var(--pi-color-highlighted); + background-color: var(--pi-color-background-400); + } + } + &.pi-tooltip__grey { + background-color: var(--pi-color-grey-200); + + &::after { + border-top: 0.5rem solid var(--pi-color-grey-200); + } + + .mdc-tooltip__surface { + color: var(--pi-color-background-400); + background-color: var(--pi-color-grey-200); + } + } + &.pi-tooltip__dark { + background-color: var(--pi-color-grey); + + &::after { + border-top: 0.5rem solid var(--pi-color-grey); + } + .mdc-tooltip__surface { + color: var(--pi-color-background-400); + background-color: var(--pi-color-grey); + } + } + // Type + &.pi-tooltip__type--error { + background-color: var(--pi-color-background-400); + + &::after { + border-top: 0.5rem solid var(--pi-color-background-400); + } + + .mdc-tooltip__surface { + color: var(--pi-color-warn); + background-color: var(--pi-color-background-400); + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_ngx-slider.scss b/libs/ui/src/lib/styles/common/vendors/_ngx-slider.scss new file mode 100644 index 0000000..2a0ee8c --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_ngx-slider.scss @@ -0,0 +1,56 @@ +.pi-shell__deale-saas, +.pi-shell__deale-website { + .ngx-slider { + & > .ngx-slider-pointer { + background-color: var(--pi-color-primary); + width: 13px; + height: 13px; + top: -5px; + &::after { + display: none; + } + } + + & > .ngx-slider-span { + & > .ngx-slider-selection { + background: var(--pi-color-primary); + } + &.ngx-slider-bubble { + &.ngx-slider-model-high, + &.ngx-slider-model-value { + background: var(--pi-color-primary); + border-radius: var(--pi-border-radius-x-small); + padding: var(--pi-spacing-xxx-small) var(--pi-spacing-xx-small); + bottom: 14px; + color: white; + } + } + } + + &[disabled] { + & > .ngx-slider-span { + & > .ngx-slider-selection { + background: #d8e0f3; + } + &.ngx-slider-pointer { + background: #d8e0f3; + } + } + } + + &.animate { + .ngx-slider-bubble { + &.ngx-slider-limit { + visibility: hidden; + } + } + } + + // .ngx-slider.animate .ngx-slider-bubble.ngx-slider-limit { + // display: none; + // } + .ngx-slider .ngx-slider-selection { + background-color: var(--pi-color-primary); + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/_typeform.scss b/libs/ui/src/lib/styles/common/vendors/_typeform.scss new file mode 100644 index 0000000..a3639fa --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/_typeform.scss @@ -0,0 +1,8 @@ +@media screen and (max-width: 77.5rem) { + .tf-v1-popup { + .tf-v1-close { + transform: translateX(2rem); + transform: translateY(-2rem); + } + } +} diff --git a/libs/ui/src/lib/styles/common/vendors/index.scss b/libs/ui/src/lib/styles/common/vendors/index.scss new file mode 100644 index 0000000..79c9dca --- /dev/null +++ b/libs/ui/src/lib/styles/common/vendors/index.scss @@ -0,0 +1,15 @@ +@forward 'ch2-button'; +@forward 'mat-checkbox'; +@forward 'mat-chip'; +@forward 'mat-dialog'; +@forward 'mat-expansion-panel'; +@forward 'mat-form-field'; +@forward 'mat-menu'; +@forward 'mat-progress-bar'; +@forward 'mat-progress-spinner'; +@forward 'mat-radio-button'; +@forward 'mat-slider'; +@forward 'mat-snackbar'; +@forward 'mat-tooltip'; +@forward 'ngx-slider'; +@forward 'typeform'; diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-action-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-action-wrapper.scss new file mode 100644 index 0000000..5a18a28 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-action-wrapper.scss @@ -0,0 +1,396 @@ +@mixin fixedActionWrapperMain( + $left: var(--pi-spacing-medium), + $width: calc(100% - var(--pi-spacing-medium) - var(--pi-spacing-medium)) +) { + .pi-layout { + &.pi-layout__main { + .pi-wrapper { + &.pi-wrapper__action { + &.pi-wrapper__action--position-fixed { + z-index: 999; + left: $left; + width: $width; + transition: var(--pi-animation); + & > button { + &:not(.pi-button__appearance--ghost) { + box-shadow: var(--pi-box-shadow); + } + } + } + } + } + } + } +} + +@mixin fixedMobileActionWrapperHorizontalBehavior($side-spacing: medium) { + .pi-wrapper__action { + &.pi-wrapper__action--position-fixed-mobile { + left: var(--pi-spacing-#{$side-spacing}); + width: calc(100% - var(--pi-spacing-#{$side-spacing}) - var(--pi-spacing-#{$side-spacing})); + } + } +} + +@mixin fixedActionWrapperMainBottom( + $position: fixed, + $bottom: calc(var(--pi-height-footer) + env(safe-area-inset-bottom) - var(--pi-spacing-x-large)) +) { + .pi-layout { + &.pi-layout__main { + .pi-wrapper { + &.pi-wrapper__action { + &.pi-wrapper__action--position-fixed { + position: $position; + bottom: $bottom; + } + } + } + } + } +} + +@mixin fixedActionWrapperFullscreen( + $bottom: env(safe-area-inset-bottom), + $padding: var(--pi-spacing-x-large) +) { + .pi-layout { + &:not(.pi-layout__main) { + .pi-wrapper { + &.pi-wrapper__action { + &.pi-wrapper__action--position-fixed { + position: fixed; + z-index: 99; + left: 0; + bottom: $bottom; + width: 100%; + padding: $padding; + background-color: rgba($color: white, $alpha: 0.96); + box-shadow: var(--pi-box-shadow); + } + } + } + } + // Action wrapper in dealbox + .pi-layout__06-edge-to-edge { + .pi-wrapper { + &.pi-wrapper__action { + &.pi-wrapper__action--position-fixed { + background-color: unset; + box-shadow: unset; + } + } + } + } + } +} + +.pi-wrapper { + // Action Wrapper + &.pi-wrapper__action { + display: flex; + } + &.pi-wrapper__justify--center { + justify-content: center; + } +} + +html { + // Mobile + &.is-mobile { + @include fixedMobileActionWrapperHorizontalBehavior; + } + // Mobile & Tablet + &:not(.is-desktop) { + @include fixedActionWrapperFullscreen; + // Small Device + @media screen and (max-width: 62rem) { + // Navbar layouts + &.pi-layout__02-full-width, + &.pi-layout__07-list { + // Action wrapper // Only fixed in small devices + .pi-wrapper__action { + &.pi-wrapper__action--position-fixed-mobile { + bottom: calc( + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + var(--pi-height-footer) + + var(--pi-spacing-x-large) + env(safe-area-inset-bottom) + ); + } + } + } + // Fixed Action Wrapper Main Bottom + .pi-shell { + & > div { + // Has Not Sticky Note + &:not(.has-bottom-sticky-note) { + @include fixedActionWrapperMainBottom; + } + // Sticky Note + &.has-bottom-sticky-note { + @include fixedActionWrapperMainBottom( + unset, + calc( + var(--pi-height-footer) + env(safe-area-inset-bottom) - var(--pi-spacing-x-large) + ) + ); + } + } + } + .pi-layout { + .pi-wrapper__action { + button { + &:not(.pi-close-button):not(.pi-hamburger).pi-button:not(.pi-button__icon):not( + .pi-button__x-small + ):not(.pi-button__small):not(.pi-button__large):not(.pi-button__trimed):not( + .pi-button__xx-small + ), + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single):not( + .pi-button__xx-small + ) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-small); + padding-left: var(--pi-spacing-small); + padding-right: var(--pi-spacing-small); + } + } + // Fixed mobile + &.pi-wrapper__action--position-fixed-mobile { + position: fixed; + z-index: 99; + button { + box-shadow: var(--pi-box-shadow); + } + } + } + + // Full screen layouts + &:not(.pi-layout__07-list):not(.pi-layout__02-full-width) { + // Action wrapper + .pi-wrapper__action { + // Only fixed in small devices + &.pi-wrapper__action--position-fixed-mobile { + left: 0; + bottom: calc(var(--pi-spacing-x-large) + env(safe-area-inset-bottom)); + width: 100%; + } + } + } + // Navbar layouts + &.pi-layout__02-full-width, + &.pi-layout__07-list { + // Action wrapper + .pi-wrapper__action { + // Only fixed in small devices + &.pi-wrapper__action--position-fixed-mobile { + bottom: calc( + var(--pi-height-footer) - var(--pi-spacing-large) + env(safe-area-inset-bottom) + ); + } + } + } + } + } + } + // Tablet + &.is-tablet { + &.is-tablet__small { + @include fixedMobileActionWrapperHorizontalBehavior(x-large); + @include fixedActionWrapperMain( + var(--pi-spacing-x-large), + calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)) + ); + .pi-wrapper__action { + &.pi-wrapper__action--position-fixed-mobile { + left: var(--pi-spacing-x-large); + } + } + } + &.is-tablet__large { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc( + 100% - var(--pi-width-navbar-collapsed) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-x-large) + ) + ); + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // Fixed Action Wrapper Main Bottom + .pi-shell { + & > div { + // Has Not Sticky Note + &:not(.has-bottom-sticky-note) { + @include fixedActionWrapperMainBottom(fixed, var(--pi-spacing-x-large)); + } + // Sticky Note + &.has-bottom-sticky-note { + @include fixedActionWrapperMainBottom(unset, unset); + } + } + } + } + } + // Desktop + &.is-desktop { + &.is-desktop__small { + // Navbar collapsed + .navbar-is-collapsed { + &.aside-is-open { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(100% - var(--pi-width-navbar-collapsed) - var(--pi-width-aside) - 7.75rem) + ); + } + &.aside-is-closed { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc( + 90% - var(--pi-width-navbar-collapsed) - var(--pi-spacing-x-large) - + var(--pi-spacing-xxx-small) + ) + ); + } + } + // Navbar expanded + .navbar-is-expanded { + // Aside is open + &.aside-is-open { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(100% - var(--pi-width-navbar-expanded) - var(--pi-width-aside) - 7.75rem) + ); + } + // Aside is closed + &.aside-is-closed { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc( + 90% - var(--pi-width-navbar-expanded) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-medium) - 0.0625rem + ) + ); + } + } + @include fixedActionWrapperFullscreen(0, var(--pi-spacing-xx-large)); + } + + &.is-desktop__medium { + // Navbar collapsed + .navbar-is-collapsed { + // Aside is open + &.aside-is-open { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(100% - var(--pi-width-navbar-collapsed) - var(--pi-width-aside) - 7.75rem) + ); + } + // Aside is closed + &.aside-is-closed { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + var(--pi-width-container) + ); + } + } + // Navbar expanded + .navbar-is-expanded { + // Aside is open + &.aside-is-open { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(100% - var(--pi-width-navbar-expanded) - var(--pi-width-aside) - 7.75rem) + ); + } + // Aside is closed + &.aside-is-closed { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + var(--pi-width-container) + ); + } + } + + @include fixedActionWrapperFullscreen( + 0, + var(--pi-spacing-xx-large) var(--pi-spacing-xxx-large) + ); + } + &.is-desktop__large { + // Desktop L + @media screen and (max-width: 140rem) { + // Navbar collapsed + .navbar-is-collapsed { + @include fixedActionWrapperMain( + calc(var(--pi-width-navbar-collapsed) - 4.65rem), + var(--pi-width-container-l) + ); + } + // Navbar expanded + .navbar-is-expanded { + @include fixedActionWrapperMain( + calc(var(--pi-width-navbar-expanded) - 12rem), + var(--pi-width-container-l) + ); + } + } + + // Desktop XL + @media screen and (min-width: 140rem) { + // Navbar collapsed + .navbar-is-collapsed { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(66.66% - var(--pi-width-navbar-collapsed) - 1.6rem) + ); + } + // Navbar expanded + .navbar-is-expanded { + @include fixedActionWrapperMain( + calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + ), + calc(66.66% - var(--pi-width-navbar-expanded) + 1.2rem) + ); + } + } + + @include fixedActionWrapperFullscreen( + 0, + var(--pi-spacing-xx-large) var(--pi-spacing-xxxx-large) + ); + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-form-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-form-wrapper.scss new file mode 100644 index 0000000..9e868e6 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-form-wrapper.scss @@ -0,0 +1,128 @@ +@mixin formWrapperSpacings($spacing: large) { + .pi-wrapper { + &.pi-wrapper__form { + // Vertically behavior + & > form { + & > *:nth-child(n + 2) { + &:not(.pi-text-viewer-link) { + margin-top: var(--pi-spacing-#{$spacing}); + } + } + } + } + } +} + +.pi-wrapper { + &.pi-wrapper__form { + display: block; + margin-top: var(--pi-spacing-xxxx-small); + + & > form { + & > .pi-field { + display: block; + } + & > fieldset { + & > div { + &.pi-wrapper__flex { + & > .pi-form-field__text { + flex: 1; + } + } + } + } + } + h4 { + color: var(--pi-color-primary); + margin-bottom: var(--pi-spacing-x-large); + font: { + size: var(--pi-font-size-small); + } + } + &.pi-wrapper__grid--two { + & > form { + display: grid; + grid-gap: var(--pi-spacing-x-large); + } + } + + .pi-wrapper__grid { + fieldset + fieldset { + margin-top: unset; + } + } + + .pi-list-item + .pi-list-item { + margin-top: var(--pi-spacing-xxx-small); + } + } +} + +@mixin fillFormFieldMinWidth($min-width: calc(100vw - 10rem)) { + .pi-wrapper { + &.pi-wrapper__form { + & > form { + & > .pi-field { + .mat-mdc-form-field-flex { + min-width: $min-width; + } + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include fillFormFieldMinWidth; + @include formWrapperSpacings; + .pi-wrapper { + &.pi-wrapper__form { + // Horizontally behavior + &.pi-wrapper__grid--half { + & > form { + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr; + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + @include formWrapperSpacings(x-large); + + // Large device + @media screen and (min-width: 62rem) { + .pi-wrapper { + &.pi-wrapper_form { + // Horizontally behavior + &.pi-wrapper__grid--half { + & > form { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: var(--pi-spacing-x-large); + } + } + } + } + } + } + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include fillFormFieldMinWidth(calc(100vw - 11rem)); + } + // Tablet L + &.is-tablet__large { + @include fillFormFieldMinWidth(16rem); + } + } + // Desktop + &.is-desktop { + @include fillFormFieldMinWidth(18.5rem); + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-grid-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-grid-wrapper.scss new file mode 100644 index 0000000..80e7239 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-grid-wrapper.scss @@ -0,0 +1,516 @@ +// 2 cols +@mixin gridWrapper2Columns($columns: 1) { + .pi-wrapper { + &.pi-wrapper__grid { + // 3 + &.pi-wrapper__grid--columns-2 { + grid-template-columns: repeat(#{$columns}, 1fr); + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(2, 1fr); + } + } + } + } +} + +@mixin gridWrapperColmnsWidths { + .pi-wrapper { + &.pi-wrapper__grid { + &.pi-wrapper__grid--columns-2 { + // 2 / 3 + &.pi-wrapper__grid--two-thirds { + grid-template-columns: 2fr 1fr; + } + // 1 / 2 + &.pi-wrapper__grid--half { + grid-template-columns: repeat(1, 1fr); + } + // 1 / 3 + &.pi-wrapper__grid--third { + grid-template-columns: 1fr 2fr; + } + // 1 / 4 + &.pi-wrapper__grid--quarter { + grid-template-columns: 1fr 3fr; + } + // 1 / 5 + &.pi-wrapper__grid--fifth { + grid-template-columns: 1fr 4fr; + } + // 1 / 6 + &.pi-wrapper__grid--sixth { + grid-template-columns: 1fr 5fr; + } + } + } + } +} + +.pi-wrapper { + // Grid directive + &.pi-wrapper__grid { + display: grid; + // Align items + &.pi-wrapper__align-items--start { + align-items: start; + } + &.pi-wrapper__align-items--center { + align-items: center; + } + &.pi-wrapper__align-items--end { + align-items: end; + } + // Align self + &.pi-wrapper__align-self--start { + align-self: start; + } + &.pi-wrapper__align-self--center { + align-self: center; + } + &.pi-wrapper__align-self--end { + align-self: end; + } + // Grid Half + &.pi-wrapper__grid--half { + grid-gap: var(--pi-spacing-x-large); + // Fill + &.pi-wrapper__fill--fit-content { + width: fit-content; + } + &.pi-wrapper__fill--fill-available { + width: -webkit-fill-available; + } + + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: 1fr; + grid-template-rows: repeat(2, 1fr); + } + // Tablet & Dektop + @media screen and (min-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + } + // Grid Four + &.pi-wrapper__grid--four { + justify-items: center; + &.pi-wrapper__fill--fill-available { + width: -webkit-fill-available; + & > *:nth-child(n + 1) { + width: -webkit-fill-available; + } + } + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: 1fr; + grid-template-rows: repeat(2, 1fr); + } + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + } + + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-columns: repeat(2, 1fr); + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-columns: repeat(4, 1fr); + } + } + // Gap + &.pi-wrapper__gap-l { + grid-gap: var(--pi-spacing-large); + } + &.pi-wrapper__gap-x-l { + grid-gap: var(--pi-spacing-x-large); + } + &.pi-wrapper__gap-xx-l { + grid-gap: var(--pi-spacing-xx-large); + } + &.pi-wrapper__gap-xxx-l { + grid-gap: var(--pi-spacing-xxx-large); + } + &.pi-wrapper__gap-xxxx-l { + grid-gap: var(--pi-spacing-xxxx-large); + } + + & > *:nth-child(1) { + grid-area: item1; + } + & > *:nth-child(2) { + grid-area: item2; + } + & > *:nth-child(3) { + grid-area: item3; + } + & > *:nth-child(4) { + grid-area: item4; + } + + // 1fr 1fr + // 1fr 1fr + &.pi-wrapper__grid--four-1-1-1-1 { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-areas: + 'item1' + 'item2' + 'item3' + 'item4'; + } + + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-areas: + 'item1 item2' + 'item3 item4'; + } + } + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-areas: + 'item1 item2' + 'item3 item4'; + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-areas: 'item1 item2 item3 item4'; + } + } + } + // . 1fr + // 1fr 1fr + &.pi-wrapper__grid--four-0-1-1-1 { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-areas: + '.' + 'item1' + 'item2' + 'item3'; + } + + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-areas: + '. item1' + 'item2 item3'; + } + } + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-areas: + '. item1' + 'item2 item3'; + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-areas: '. item1 item2 item3'; + } + } + } + // 1fr . + // 1fr 1fr + &.pi-wrapper__grid--four-1-0-1-1 { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-areas: + 'item1' + '.' + 'item2' + 'item3'; + } + + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-areas: + 'item1 .' + 'item2 item3'; + } + } + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-areas: + 'item1 .' + 'item2 item3'; + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-areas: 'item1 . item2 item3'; + } + } + } + // 1fr 1fr + // . 1fr + &.pi-wrapper__grid--four-1-1-0-1 { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-areas: + 'item1' + 'item2' + '.' + 'item3'; + } + + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-areas: + 'item1 item2' + '. item3'; + } + } + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-areas: + 'item1 item2' + '. item3'; + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-areas: 'item1 item2 . item3'; + } + } + } + // 1fr 1fr + // 1fr . + &.pi-wrapper__grid--four-1-1-1-0 { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-areas: + 'item1' + 'item2' + 'item3' + '.'; + } + + &:not(.is-responsive) { + // Tablet & Desktop + @media screen and (min-width: 40rem) { + grid-template-areas: + 'item1 item2' + 'item3 .'; + } + } + &.is-responsive { + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-areas: + 'item1 item2' + 'item3 .'; + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-areas: 'item1 item2 item3 .'; + } + } + } + } + &.pi-wrapper__max-width--full-width { + // Desktop S + @media screen and (min-width: 77.5rem) and (max-width: 90rem) { + max-width: var(--pi-website-width-container-s); + } + // Desktop M + @media screen and (min-width: 90rem) and (max-width: 120rem) { + max-width: var(--pi-website-width-container); + } + // Desktop L + @media screen and (min-width: 120rem) { + max-width: var(--pi-website-width-container-l); + } + } + + &.pi-wrapper__fill--fill-available { + width: -webkit-fill-available; + } + + // 3 + &.pi-wrapper__grid--columns-3 { + // Is Responsive + &.is-responsive { + // Mobile S + @media screen and (max-width: 62rem) { + grid-template-columns: repeat(1, 1fr); + } + // Tablet & Desktop S + @media screen and (min-width: 62rem) and (max-width: 77.5rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet & Desktop + @media screen and (min-width: 77.5rem) { + grid-template-columns: repeat(3, 1fr); + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(3, 1fr); + } + } + // 4 + &.pi-wrapper__grid--columns-4 { + // Is Responsive + &.is-responsive { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: repeat(1, 1fr); + } + // Tablet S + @media screen and (min-width: 40rem) and (max-width: 62rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet L + @media screen and (min-width: 62rem) and (min-width: 77.5rem) { + grid-template-columns: repeat(4, 1fr); + } + // Desktop + @media screen and (min-width: 77.5rem) { + width: 25%; + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(4, 1fr); + } + } + // 5 + &.pi-wrapper__grid--columns-5 { + // Is Responsive + &.is-responsive { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet S + @media screen and (min-width: 40rem) and (max-width: 62rem) { + grid-template-columns: repeat(3, 1fr); + } + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + grid-template-columns: repeat(5, 1fr); + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(5, 1fr); + } + } + // 6 + &.pi-wrapper__grid--columns-6 { + // Is Responsive + &.is-responsive { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + grid-template-columns: repeat(3, 1fr); + } + // Desktop + @media screen and (min-width: 77.5rem) { + grid-template-columns: repeat(6, 1fr); + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(6, 1fr); + } + } + // 7 + &.pi-wrapper__grid--columns-7 { + // Is Responsive + &.is-responsive { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet S + @media screen and (min-width: 40rem) and (max-width: 62rem) { + grid-template-columns: repeat(3, 1fr); + } + // Tablet L & Desktop + @media screen and (min-width: 62rem) and (max-width: 77.5rem) { + grid-template-columns: repeat(4, 1fr); + } + // Tablet L & Desktop + @media screen and (min-width: 77.5rem) { + grid-template-columns: repeat(7, 1fr); + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(7, 1fr); + } + } + // 8 + &.pi-wrapper__grid--columns-8 { + // Is Responsive + &.is-responsive { + // Mobile + @media screen and (max-width: 40rem) { + grid-template-columns: repeat(2, 1fr); + } + // Tablet S + @media screen and (min-width: 40rem) and (max-width: 62rem) { + grid-template-columns: repeat(4, 1fr); + } + // Tablet L & Desktop + @media screen and (min-width: 62rem) and (max-width: 77.5rem) { + grid-template-columns: repeat(6, 1fr); + } + // Tablet L & Desktop + @media screen and (min-width: 77.5rem) { + grid-template-columns: repeat(8, 1fr); + } + } + // NOT Is Responsive + &:not(.is-responsive) { + grid-template-columns: repeat(8, 1fr); + } + } + // Justify + // Center + &.pi-wrapper__justify-items--center { + justify-items: center; + } + // End + &.pi-wrapper__justify-items--end { + justify-items: end; + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + // 2 cols + @include gridWrapper2Columns; + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // 2 cols + @include gridWrapper2Columns(2); + @include gridWrapperColmnsWidths; + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-header-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-header-wrapper.scss new file mode 100644 index 0000000..2f4ec42 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-header-wrapper.scss @@ -0,0 +1,288 @@ +@mixin headerWrapperHeight($height: 2.75rem) { + .pi-wrapper { + &.pi-wrapper__header { + &:not(.pi-wrapper__full-grid) { + & > header, + & > .pi-header-wrapper-content__right { + height: $height; + } + } + } + } +} + +.pi-wrapper { + // Header Wrapper + &.pi-wrapper__header { + display: flex; + justify-content: space-between; + + & > header { + & > .pi-text-viewer { + &:nth-of-type(1) { + &:hover { + cursor: pointer; + & > * { + cursor: pointer; + & > * { + cursor: pointer; + } + } + } + .mat-icon { + transition: all 0.2s ease; + } + &:hover { + .mat-icon { + transform: translateX(-0.25rem); + } + } + } + } + + & > span { + color: var(--pi-color-primary); + font: { + size: var(--pi-font-size-medium); + weight: 300; + style: italic; + } + } + + & > a { + color: var(--pi-color-grey); + display: flex; + align-items: center; + column-gap: var(--pi-spacing-xx-small); + } + } + // Justify + &.pi-wrapper__justify--center { + margin: auto; + } + // Align items + // End + &.pi-wrapper__align-items--flex-end { + & > header { + display: flex; + align-items: flex-end; + } + .pi-header-wrapper-content { + display: flex; + align-items: flex-end; + height: 100%; + } + } + // Center + &.pi-wrapper__align-items--center { + .pi-header-wrapper-content { + display: flex; + height: 100%; + } + } + + // Flex direction + &.pi-wrapper__flex-direction--column { + justify-content: flex-end; + + .pi-header-wrapper-content { + flex-direction: column; + gap: var(--pi-spacing-xxx-small); + &.pi-header-wrapper-content__left { + align-items: flex-start; + justify-content: center; + } + } + } + // Full grid + &.pi-wrapper__full-grid { + & > header { + margin-top: var(--pi-spacing-x-small); + width: 100%; + } + } + .pi-form-field__search, + .pi-button__toggle { + &:not(:hover) { + box-shadow: var(--pi-box-shadow); + } + } + } +} + +html { + &.is-mobile { + .pi-shell { + &.pi-shell__deale-saas { + @include headerWrapperHeight; + } + } + .pi-layout__main { + .pi-wrapper { + &.pi-wrapper__header { + // Flex direction + &.pi-wrapper__flex-direction--column { + flex-direction: column; + align-items: flex-start; + gap: var(--pi-spacing-medium); + .pi-header-wrapper-content { + &.pi-header-wrapper-content__right { + align-items: flex-start; + } + } + } + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-header-wrapper-content { + &.pi-header-wrapper-content__center { + width: 100%; + } + + &.pi-header-wrapper-content__right { + & > aside { + .pi-form-field__search { + // Is expanded + &.is-expanded { + position: absolute; + left: var(--pi-spacing-medium); + width: calc(100% - var(--pi-spacing-xx-large)); + margin-top: -1.5rem; + } + } + .pi-toggle-button { + span { + display: none; + } + } + } + } + } + } + } + // Tablet + &.is-tablet { + .pi-shell { + &.pi-shell__deale-saas { + @include headerWrapperHeight(var(--pi-height-layout-header)); + } + } + + // Tablet S + &.is-tablet__small { + } + // Tablet L + &.is-tablet__large { + } + } + + &:not(.is-mobile) { + .pi-layout__main { + .pi-wrapper { + &.pi-wrapper__header { + // Flex direction + &.pi-wrapper__flex-direction--column { + flex-direction: row; + justify-content: space-between; + .pi-header-wrapper-content { + &.pi-header-wrapper-content__center { + width: 66.66%; + } + &.pi-header-wrapper-content__right { + align-items: flex-end; + } + } + } + } + } + } + } + // Desktop + &.is-desktop { + .pi-header-wrapper-content { + &:not(.pi-header-wrapper-content__center) { + width: 50%; + } + } + + .pi-shell { + &.pi-shell__deale-saas { + @include headerWrapperHeight(var(--pi-height-layout-header)); + } + } + + .pi-wrapper { + &.pi-wrapper__header { + // 1/4 - 3/4 + &.pi-wrapper__grid--quarter { + & > header { + width: 25%; + } + .pi-header-wrapper-content__right { + width: 75%; + } + } + // 1/3 - 2/3 + &.pi-wrapper__grid--third { + & > header { + width: 33.33%; + } + .pi-header-wrapper-content__right { + width: 66.66%; + } + } + // 1/2 - 1/2 + &.pi-wrapper__grid--half { + & > header { + width: 50%; + } + .pi-header-wrapper-content__right { + width: 50%; + } + } + // 2/5 - 3/5 + &.pi-wrapper__grid--fifth { + & > header { + width: 40%; + } + .pi-header-wrapper-content__right { + width: 60%; + } + } + + // Reverse + &.pi-wrapper__grid--reverse-quarter { + & > header { + width: 75%; + } + .pi-header-wrapper-content__right { + width: 25%; + } + } + // 1/3 - 2/3 + &.pi-wrapper__grid--reverse-third { + & > header { + width: 66.66%; + } + .pi-header-wrapper-content__right { + width: 33.33%; + } + } + // 3/5 - 2/5 + &.pi-wrapper__grid--reverse-fifth { + & > header { + width: 60%; + } + .pi-header-wrapper-content__right { + width: 40%; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-list-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-list-wrapper.scss new file mode 100644 index 0000000..b086505 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-list-wrapper.scss @@ -0,0 +1,154 @@ +@mixin listWrapperHeight($device: mobile, $extra-height: 5.2) { + .pi-layout__07-list { + .pi-wrapper { + &.pi-wrapper__list { + // Has Scroll + &.pi-wrapper__list--has-scroll { + & > section { + overflow-y: scroll; + height: calc( + 100vh - var(--pi-height-header-#{$device}) - var(--pi-height-layout-header) - #{$extra-height}rem + ); + &::-webkit-scrollbar { + display: none; + } + } + } + } + } + } +} + +@mixin listWrapperWidth($width: calc(100% - var(--pi-spacing-xxx-small))) { + .pi-wrapper { + // List Wrapper + &.pi-wrapper__list { + & > section { + max-width: $width; + } + } + } +} + +@mixin listWrapperNoResultsTemplate($min-height, $padding) { + .pi-wrapper { + // List Wrapper + &.pi-wrapper__list { + & > section { + .pi-wrapper { + &.pi-wrapper__flex-direction--column { + &.pi-wrapper__justify--center { + min-height: $min-height; + padding-top: $padding; + padding-bottom: $padding; + } + } + } + } + } + } +} + +// Common +.pi-wrapper { + // List Wrapper + &.pi-wrapper__list { + header { + &.pi-wrapper__justify--space-between { + & > *:nth-child(n + 1) { + width: -webkit-fill-available; + } + } + } + // Has Scroll + &.pi-wrapper__list--has-scroll { + & > section { + border: 1px solid transparent; + overflow-y: auto; + overflow-x: hidden; + + .pi-data-grid__mode--grid { + overflow-y: auto; + overflow-x: hidden; + &::-webkit-scrollbar { + display: none; + } + } + } + } + } +} + +// Mode Grid +.is-mode-grid { + @include listWrapperNoResultsTemplate(57vh, var(--pi-spacing-x-large)); + .pi-wrapper { + &.pi-wrapper__list { + & > section { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-xxx-small); + .pi-data-grid__mode--grid { + display: flex; + flex-wrap: wrap; + flex-grow: 0; + flex-shrink: 0; + gap: var(--pi-spacing-x-large); + padding-bottom: var(--pi-spacing-medium); + } + } + } + } +} + +.is-mode-list { + @include listWrapperNoResultsTemplate(57vh, var(--pi-spacing-x-large)); +} + +html { + // Mobile + &.is-mobile { + @include listWrapperHeight; + @include listWrapperWidth; + } + + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include listWrapperHeight(tablet, 5.7); + @include listWrapperWidth; + } + // Tablet L + &.is-tablet__large { + @include listWrapperHeight(tablet, 3); + @include listWrapperWidth(100%); + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // Mode List + + .is-mode-list { + .pi-wrapper { + &.pi-wrapper__list { + border-radius: var(--pi-border-radius-large); + background-color: var(--pi-color-white); + box-shadow: var(--pi-box-shadow-panel); + } + } + } + } + } + + // Desktop + &.is-desktop { + @include listWrapperWidth(100%); + .pi-layout__07-list { + @include listWrapperHeight(desktop, 7.8); + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-main-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-main-wrapper.scss new file mode 100644 index 0000000..aaaf257 --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-main-wrapper.scss @@ -0,0 +1,164 @@ +@mixin mainWrapperCentered($height: calc(100% - var(--pi-height-header-mobile))) { + .pi-shell__deale-saas { + & > div { + // NOT Is fullscreen + &:not(.is-fullscreen) { + .pi-layout { + // Main layouts + &.pi-layout__main { + // Center + .pi-wrapper { + &.pi-wrapper__main { + // Center + &.is-centered { + height: $height; + & > section { + height: $height; + } + } + } + } + } + } + } + // Is fullscreen + &.is-fullscreen { + .pi-wrapper { + &.pi-wrapper__main { + // Center + &.is-centered { + height: 100%; + & > section { + height: 100%; + } + } + } + } + } + } + } +} + +.pi-wrapper { + &.pi-wrapper__main { + // Main wrapper + & > section { + display: flex; + width: 100%; + } + // Flex direction + &.pi-wrapper__flex-direction--column { + & > section { + flex-direction: column; + } + } + // Type document + &.pi-wrapper__main--type-document { + padding-bottom: calc(var(--pi-height-action-bar) + var(--pi-spacing-x-large)); + h1 { + text-align: center; + } + } + // Justify content + &.pi-wrapper__justify--center { + & > section { + justify-content: center; + } + } + // Align items + // Center + &.pi-wrapper__align-items--center { + & > section { + align-items: center; + } + } + // Flex end + &.pi-wrapper__align-items--flex-end { + & > section { + align-items: flex-end; + } + } + // Gap + // 0.75rem - 12px + &.pi-wrapper__gap--s { + & > section { + gap: var(--pi-spacing-small); + } + } + // 1rem - 16px + &.pi-wrapper__gap--m { + & > section { + gap: var(--pi-spacing-medium); + } + } + // 1.25rem - 20px + &.pi-wrapper__gap--l { + & > section { + gap: var(--pi-spacing-large); + } + } + // 1.5rem - 24px + &.pi-wrapper__gap--xl { + & > section { + gap: var(--pi-spacing-x-large); + } + } + // 2rem - 32px + &.pi-wrapper__gap--xxl { + & > section { + gap: var(--pi-spacing-xx-large); + } + } + // 2.5rem - 40px + &.pi-wrapper__gap--xxxl { + gap: var(--pi-spacing-xxx-large); + } + } +} +.pi-main-wrapper-content { + flex: 1 1 auto; + display: block; + & > * + * { + display: block; + margin-top: var(--pi-spacing-x-large); + } + .pi-list-item { + display: flex; + } + &.is-sticky { + height: fit-content; + position: sticky; + top: 0; + z-index: 9; + padding-top: var(--pi-spacing-small); + background-color: var(--pi-color-white); + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + .pi-wrapper { + &.pi-wrapper__main { + & > section { + flex-direction: column; + } + } + } + } + + // Mobile & Tablet S + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + @include mainWrapperCentered; + } + } + // Tablet L & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + @include mainWrapperCentered(calc(100% - var(--pi-height-header))); + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/_pi-wrapper.scss b/libs/ui/src/lib/styles/common/wrappers/_pi-wrapper.scss new file mode 100644 index 0000000..baf367e --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/_pi-wrapper.scss @@ -0,0 +1,297 @@ +.pi-wrapper { + // Align items + &.pi-wrapper__align-items--center { + align-items: center; + } + &.pi-wrapper__align-items--flex-end { + align-items: flex-end; + } + &.pi-wrapper__align-items--stretch { + align-items: stretch; + } + // Flex direction + &.pi-wrapper__flex-direction--column { + flex-direction: column; + } + &.pi-wrapper__flex-direction--column-reverse { + flex-direction: column-reverse; + } + &.pi-wrapper__flex-direction--row-reverse { + flex-direction: row-reverse; + } + &.pi-wrapper__full-width-auto { + margin: auto; + } + &.pi-wrapper__fit-content { + max-width: fit-content; + } + // Justify + &.pi-wrapper__justify--center { + justify-content: center; + } + &.pi-wrapper__justify--flex-end { + justify-content: flex-end; + } + &.pi-wrapper__justify--space-between { + justify-content: space-between; + } + &.pi-wrapper__justify--space-evenly { + justify-content: space-evenly; + } + // Gap + &.pi-wrapper__gap--xxxs { + gap: var(--pi-spacing-xxx-small); + } + // 0.5rem - 8px + &.pi-wrapper__gap--xxs { + gap: var(--pi-spacing-xx-small); + } + // 0.625rem - 10px + &.pi-wrapper__gap--xs { + gap: var(--pi-spacing-x-small); + } + // 0.75rem - 12px + &.pi-wrapper__gap--s { + gap: var(--pi-spacing-small); + } + // 1rem - 16px + &.pi-wrapper__gap--m { + gap: var(--pi-spacing-medium); + } + // 1.25rem - 20px + &.pi-wrapper__gap--l { + gap: var(--pi-spacing-large); + } + // 1.5rem - 24px + &.pi-wrapper__gap--xl { + gap: var(--pi-spacing-x-large); + } + // 2rem - 32px + &.pi-wrapper__gap--xxl { + gap: var(--pi-spacing-xx-large); + } + // 2.5rem - 40px + &.pi-wrapper__gap--xxxl { + gap: var(--pi-spacing-xxx-large); + } + // 3.375rem - 54px + &.pi-wrapper__gap--xxxxl { + gap: var(--pi-spacing-xxxx-large); + } + + // Flex directive + &.pi-wrapper__flex { + display: flex; + width: 100%; + // Wrap + &.pi-wrapper__flex-direction--wrap { + flex-wrap: wrap; + gap: var(--pi-spacing-xx-small); + } + // Flex horizontally + // Space between + &.pi-wrapper__justify--space-between { + justify-content: space-between; + } + // Space evenly + &.pi-wrapper__justify--space-around { + justify-content: space-around; + } + // Center + &.pi-wrapper__justify--center { + justify-content: center; + } + // Alignment + // Flex start + &.pi-wrapper__align-items--flex-start { + align-items: flex-start; + } + // Items width + // 25% + &.pi-wrapper__items-width--quarter { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + flex-direction: column; + } + & > *:nth-child(n + 1) { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + width: 100%; + } + // Tablet L + @media screen and (min-width: 62rem) and (min-width: 77.5rem) { + width: 33.33%; + } + // Desktop + @media screen and (min-width: 77.5rem) { + width: 25%; + } + } + } + // 33.33% + &.pi-wrapper__items-width--third { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + flex-direction: column; + } + + & > *:nth-child(n + 1) { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + width: 100%; + } + // Tablet L + @media screen and (min-width: 62rem) and (min-width: 77.5rem) { + width: 50%; + } + // Desktop + @media screen and (min-width: 77.5rem) { + width: 33.33%; + } + } + } + // 50% + &.pi-wrapper__items-width--half { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + flex-direction: column; + } + & > *:nth-child(n + 1) { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + width: 100%; + } + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + width: 50%; + } + } + } + // 100% + &.pi-wrapper__items-width--full { + & > *:nth-child(n + 1) { + width: 100%; + } + } + } + // Padding + &.pi-wrapper__padding--xs { + padding: var(--pi-spacing-x-small); + } + + // Border + // Top + &.pi-wrapper__border--top { + border-top: 1px solid var(--pi-color-grey-25); + } + // Bottom + &.pi-wrapper__border--bottom { + border-bottom: 1px solid var(--pi-color-grey-25); + } +} + +// Responsive +html { + // Mobile + &.is-mobile { + .pi-wrapper { + // Padding + &.pi-wrapper__has-padding { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + + // Grid + &.pi-wrapper__grid { + // Grid half + &.pi-wrapper__grid--half { + grid-template-columns: unset; + } + } + } + } + // Tablet + &.is-tablet { + .pi-wrapper { + // Padding + &.pi-wrapper__has-padding { + padding-left: var(--pi-spacing-xxx-large); + padding-right: var(--pi-spacing-xxx-large); + } + } + } + // Desktop + &.is-desktop { + .pi-wrapper { + // Grid + // 1/4 - 3/4 + &.pi-wrapper__grid--quarter { + .pi-main-wrapper-content__left { + width: 25%; + } + .pi-main-wrapper-content__right { + width: 75%; + } + } + // 1/3 - 2/3 + &.pi-wrapper__grid--third { + .pi-main-wrapper-content__left { + width: 33.33%; + } + .pi-main-wrapper-content__right { + width: 66.66%; + } + } + // 1/2 - 1/2 + &.pi-wrapper__grid--half { + .pi-main-wrapper-content__left { + width: 50%; + } + .pi-main-wrapper-content__right { + width: 50%; + } + } + // 2/5 - 3/5 + &.pi-wrapper__grid--fifth { + .pi-main-wrapper-content__left { + width: 40%; + } + .pi-main-wrapper-content__right { + width: 60%; + } + } + + // Reverse + &.pi-wrapper__grid--reverse-quarter { + .pi-main-wrapper-content__left { + width: 75%; + } + .pi-main-wrapper-content__right { + width: 25%; + } + } + // 1/3 - 2/3 + &.pi-wrapper__grid--reverse-third { + .pi-main-wrapper-content__left { + width: 66.66%; + } + .pi-main-wrapper-content__right { + width: 33.33%; + } + } + // 3/5 - 2/5 + &.pi-wrapper__grid--reverse-fifth { + .pi-main-wrapper-content__left { + width: 60%; + } + .pi-main-wrapper-content__right { + width: 40%; + } + } + &.pi-wrapper__has-padding { + padding-left: var(--pi-spacing-xxxx-large); + padding-right: var(--pi-spacing-xxxx-large); + } + } + } +} diff --git a/libs/ui/src/lib/styles/common/wrappers/index.scss b/libs/ui/src/lib/styles/common/wrappers/index.scss new file mode 100644 index 0000000..57d083c --- /dev/null +++ b/libs/ui/src/lib/styles/common/wrappers/index.scss @@ -0,0 +1,7 @@ +@forward 'pi-action-wrapper'; +@forward 'pi-form-wrapper'; +@forward 'pi-grid-wrapper'; +@forward 'pi-header-wrapper'; +@forward 'pi-list-wrapper'; +@forward 'pi-main-wrapper'; +@forward 'pi-wrapper'; diff --git a/libs/ui/src/lib/styles/configs.ts b/libs/ui/src/lib/styles/configs.ts new file mode 100644 index 0000000..3d9e9a3 --- /dev/null +++ b/libs/ui/src/lib/styles/configs.ts @@ -0,0 +1,181 @@ +// Shell level +export const shellTheming = (ShellClass: string) => `pi-shell pi-shell__${ShellClass}`; + +export const ShellClass = { + // Deale Landings (Public) + DealeLandings: shellTheming('deale-landings'), + // Deale SaaS (Private) + DealeSaaS: shellTheming('deale-saas'), + // Deale Website (Public) + DealeWebsite: shellTheming('deale-website'), +}; + +// View component level +// Container Layout ~ Landings +export const ContainerLayout = 'pi-container-layout'; + +// Layouts +// Heights +export const enum Height { + Small = 'small', // Mobile: 50, Tablet: 50, Desktop: 25 + Medium = 'medium', // Mobile: 100, Tablet: 75, Desktop: 50 + Large = 'large', // Mobile: 150, Tablet: 100, Desktop: 75 + Full = 'full', // Mobile: 200, Tablet: 150, Desktop: 100 +} + +export const height = (height: Height): string => `pi-layout__${height}`; + +// Responsive +export const heights = ( + mobileHeight: Height, + tabletHeight: Height, + desktopHeight: Height +): string => + `pi-layout__${mobileHeight}--mobile pi-layout__${tabletHeight}--tablet pi-layout__${desktopHeight}--desktop`; + +// Not Responsive +export const LayoutHeight = { + Small: height(Height.Small), // 25 + Medium: height(Height.Medium), // 50 + Large: height(Height.Large), // 75 + Full: height(Height.Full), // 100 +}; + +// HostBinding layout class with or without background image ~ Landings +export const layoutBackground = (imageName: string) => + `url(https://d2m4265u67f2xu.cloudfront.net/assets/images/${imageName})`; + +export const backgroundWaveClass = (waveRef: string) => + `pi-background-wave pi-background-wave__${waveRef}`; + +// Not Responsive +export const LayoutWave = { + // Grey + Grey1: backgroundWaveClass('01-grey'), + Grey1RGBA: backgroundWaveClass('01-grey-rgba'), + Grey2: backgroundWaveClass('02-grey'), + Grey2RGBA: backgroundWaveClass('02-grey-rgba'), + // Highlighted + Highlighted1: backgroundWaveClass('01-highlighted'), + Highlighted1RGBA: backgroundWaveClass('01-highlighted-rgba'), + Highlighted2: backgroundWaveClass('02-highlighted'), + Highlighted2RGBA: backgroundWaveClass('02-highlighted-rgba'), + // Primary + Primary1: backgroundWaveClass('01-primary'), + Primary1RGBA: backgroundWaveClass('01-primary-rgba'), + Primary2: backgroundWaveClass('02-primary'), + Primary2RGBA: backgroundWaveClass('02-primary-rgba'), + // PrimaryWhite + PrimaryWhite1: backgroundWaveClass('01-primary-white'), + PrimaryWhite1RGBA: backgroundWaveClass('01-primary-white-rgba'), + PrimaryWhite2: backgroundWaveClass('02-primary-white'), + PrimaryWhite2RGBA: backgroundWaveClass('02-primary-white-rgba'), + // Primary50 + PrimaryPrimary501: backgroundWaveClass('01-primary-primary-50'), + Primary501: backgroundWaveClass('01-primary-50'), + PrimaryPrimary501RGBA: backgroundWaveClass('01-primary-primary-50-rgba'), + PrimaryPrimary502: backgroundWaveClass('02-primary-primary-50'), + Primary502: backgroundWaveClass('02-primary-50'), + PrimaryPrimary502RGBA: backgroundWaveClass('02-primary-primary-50-rgba'), + // White + White1: backgroundWaveClass('01-white'), + White1RGBA: backgroundWaveClass('01-white-rgba'), + White2: backgroundWaveClass('02-white'), + White2RGBA: backgroundWaveClass('02-white-rgba'), +}; + +// Layouts ~ SaaS & Landings +export const LayoutNoPadding = 'pi-layout__no-padding'; + +export const LayoutCollapsable = 'pi-layout__collapsable'; + +export const userType = (userType: string) => `is-${userType}`; + +// User type class +export const UserTypeClass = { + // Company + Company: userType('company'), + // Investor + Investor: userType('investor'), + // Advisor + Advisor: userType('advisor'), +}; + +// Layouts (Add to any view) +export const LayoutTypeClass = { + // Specifics + Authentication: 'pi-layout pi-layout__00-authentication', + Dashboard: 'pi-layout pi-layout__01-dashboard', + // TODO: Delete old dashboard layout when ready other users + DashboardOld: 'pi-layout pi-layout__main pi-layout__01-dashboard-old', + // Main Horizontally + // Full Width + FullWidth: 'pi-layout pi-layout__main pi-layout__02-full-width', + // Full Width Centered + FullWidthAuto: + 'pi-layout pi-layout__main pi-layout__02-full-width pi-layout__02a-full-width--auto', + // Main Vertically Centered + VerticallyCentered: 'pi-layout pi-layout__03-vertically-centered', + // Main Space Between + SpaceBetween: 'pi-layout pi-layout__04-space-between', + // Splitted + Splitted: 'pi-layout pi-layout__05-splitted', + // SplittedPaywall + SplittedPaywall: 'pi-layout pi-layout__05-splitted pi-05a-splitted-paywall-layout', + // Edge to Edge + EdgeToEdge: 'pi-layout pi-layout__06-edge-to-edge', + // List Left + List: 'pi-layout pi-layout__07-list', + // Infinite Scroll + ListAuto: 'pi-layout pi-layout__07-list pi-layout__07a-list--auto', + // List Left + PaginatedList: 'pi-layout pi-layout__07-list pi-layout__07b-list--paginated', + // Form Layout + Form: 'pi-layout pi-layout__08-form', + // Pricing Layout + Pricing: 'pi-layout pi-layout__09-pricing', +}; + +export const WebsiteScreenTypeClass = { + ThankYou: 'pi-website-screen pi-website-screen__00-type-thank-you', +}; + +// Wrappers +export const wrapperType = (wrapperClass: string) => `pi-wrapper pi-wrapper__${wrapperClass}`; + +export const WrapperTypeClass = { + Action: wrapperType('action'), + Form: wrapperType('form'), + Header: wrapperType('header'), + List: wrapperType('list'), + Main: wrapperType('main'), + Steps: wrapperType('steps'), +}; + +// Components +export const componentFamily = (componentClass: string) => `pi-component pi-${componentClass}`; + +export const ComponentFamilyClass = { + Button: componentFamily('button'), + Card: componentFamily('card'), + Carousel: componentFamily('carousel'), + DomainComponent: componentFamily('domain-component'), + Chip: componentFamily('chip'), + DynamicMenu: componentFamily('dynamic-menu'), + FileUpload: componentFamily('file-upload'), + Field: componentFamily('field'), + Icon: componentFamily('icon'), + Image: componentFamily('image'), + List: componentFamily('list'), + CustomModal: componentFamily('modal pi-modal__custom-template'), + Panel: componentFamily('panel'), + Table: componentFamily('table'), + Text: componentFamily('text'), +}; + +// Transition durations +export const enum DURATION { + Slow = 300, + Medium = 200, + Fast = 100, +} diff --git a/libs/ui/src/lib/styles/deale-saas/index.scss b/libs/ui/src/lib/styles/deale-saas/index.scss new file mode 100644 index 0000000..11065c5 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/index.scss @@ -0,0 +1,4 @@ +@use 'layouts' as *; +@use 'theming' as *; +@use 'shell' as *; +@use 'wrappers' as *; diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00-authentication-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00-authentication-layout.scss new file mode 100644 index 0000000..bf3a8d2 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00-authentication-layout.scss @@ -0,0 +1,330 @@ +@use 'pi-00a-authentication-layout-auth'; +@use 'pi-00b-authentication-layout-recover-password'; +@use 'pi-00c-authentication-layout-user-profile'; +@use 'pi-00d-authentication-layout-default-space'; +@use 'pi-00e-authentication-layout-impersonation'; + +@mixin authenticationLayoutSizes( + $width: calc(100% - var(--pi-spacing-xx-large)), + $max-width: calc(100% - var(--pi-spacing-xx-large)), + $height: 90vh +) { + & > .pi-component { + width: $width; + max-width: $max-width; + & > .pi-panel { + width: -webkit-fill-available; + & > section { + height: $height; + } + } + } +} + +@mixin authenticationLayoutsButtonSize() { +} + +@mixin authenticationLayoutSpacings($spacing, $gap) { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth):not(.pi-layout__00-authentication--default-space) { + .pi-panel { + .pi-panel-content__top, + .pi-panel-content__bottom { + gap: var(--pi-spacing-#{$gap}); + } + &.pi-panel__inner-padding--xxxxl { + &.pi-panel__inner-padding { + &.pi-panel__flex-direction--column { + & > section { + & > * { + padding-left: $spacing; + padding-right: $spacing; + } + } + } + } + } + } + } + } +} + +@mixin authenticationLayoutMobileSmall { + // Mobile S + @media screen and (max-height: 41.6875rem) { + .pi-wrapper { + .pi-wrapper__gap--l { + gap: var(--pi-spacing-small); + } + } + .pi-panel { + &.pi-panel__inner-padding--xxxl { + &.pi-panel__inner-padding { + &.pi-panel__flex-direction--column { + & > section { + & > * { + padding-top: var(--pi-spacing-x-large); + padding-bottom: var(--pi-spacing-x-large); + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + } + } + } + } + } + } +} +.pi-layout__00-authentication { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + width: 100%; + + & > .pi-component { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + & > .pi-panel { + background: linear-gradient(rgba(255, 255, 255, 0.95), white) padding-box, + linear-gradient(to right, var(--pi-color-primary-50), var(--pi-color-primary-25)) border-box; + background-color: rgba(255, 255, 255, 0.66); + animation: gradient 0.5s linear infinite; + animation-direction: alternate-reverse; + background-size: 150% 300%; + border: 0.0625rem solid transparent; + box-shadow: 0px 0px 30px 0px rgba(69, 97, 199, 0.125); + .pi-panel-content__right { + & > * { + max-width: 21.25rem; + height: fit-content; + } + } + .pi-list-item { + .pi-text-viewer { + article { + label, + span { + line-height: 105%; + } + } + } + } + } + .pi-wrapper__form + .pi-wrapper__action { + margin-top: var(--pi-spacing-xxxx-large); + } + .pi-wrapper__action { + .pi-text-centered { + margin-top: var(--pi-spacing-x-large); + } + } + } + &:not(.pi-layout__00-authentication--auth) { + // Panel in other authentication layouts + .pi-panel { + .pi-panel-content__bottom { + position: sticky; + bottom: 0; + } + + .pi-panel-content__top, + .pi-panel-content__bottom { + display: flex; + gap: var(--pi-spacing-x-small); + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include authenticationLayoutSpacings(var(--pi-spacing-xx-large), x-small); + .pi-layout__00-authentication { + @include authenticationLayoutSizes; + // Mobile S + @include authenticationLayoutMobileSmall; + } + } + // Tablet + &.is-tablet { + &.is-tablet__small { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth) { + @include authenticationLayoutSizes(80%, 80%, 80vh); + } + &.pi-layout__00-authentication--auth { + @include authenticationLayoutSizes(80%, 80%, 85vh); + } + } + } + &.is-tablet__large { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth) { + @include authenticationLayoutSizes(calc(100% - 4rem), calc(100% - 4rem), 70vh); + } + &.pi-layout__00-authentication--auth { + @include authenticationLayoutSizes( + calc(100% - 4rem), + calc(100% - 4rem), + calc(100vh - 4rem) + ); + } + } + } + } + + // Tablet & Mobile + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-layout__00-authentication { + & > .pi-component { + // Panel in authentication layout + & > .pi-panel { + .pi-panel-content__bottom { + // Action wrapper in panel bottom content + .pi-wrapper { + &.pi-wrapper__action { + width: -webkit-fill-available; + // Hide 'Press enter' text viewer on mobile and tablet + & > *:last-child { + display: none; + } + } + } + } + } + } + // Not Auth Layout + &:not(.pi-layout__00-authentication--auth) { + // Panel in other authentication layouts + & > .pi-component { + .pi-panel { + & > section { + overflow: hidden; + + .pi-panel-content__center { + flex-grow: 1; + overflow-y: auto; + // Generic Scroll + &::-webkit-scrollbar { + width: var(--pi-spacing-xxx-small); + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-primary-50); + border-radius: var(--pi-border-radius-small); + + &:hover { + transition: var(--pi-animation); + background-color: var(--pi-color-primary-100); + } + } + } + } + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth) { + .pi-panel { + position: relative; + // Add logo to bottom right corner of panel + &::after { + content: ''; + position: absolute; + height: 100%; + width: 4.375rem; + right: var(--pi-spacing-xxx-large); + bottom: var(--pi-spacing-xxx-large); + background-image: url('https://d2m4265u67f2xu.cloudfront.net/assets/images/deale-logo.svg'); + background-repeat: no-repeat; + background-position: bottom right; + } + } + } + } + } + .pi-shell { + .is-public { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth):not( + .pi-layout__00-authentication--recover-password + ):not(.pi-layout__00-authentication--impersonation) { + .pi-panel { + & > section { + .pi-panel-content__top, + .pi-panel-content__bottom { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not( + .pi-button__toggle + ):not(.pi-button__type--menu-item):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle):not(.ch2-btn) { + min-width: 9.5rem; + } + } + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not( + .pi-button__toggle + ):not(.pi-button__type--menu-item).pi-button:not(.pi-button__icon):not( + .pi-button__x-small + ):not(.pi-button__small):not(.pi-button__large):not(.pi-button__large):not( + .pi-button__trimed + ):not(.pi-button__xx-small):not(.pi-button__trimmed) { + min-width: 11.5rem; + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-large); + } + } + } + } + } + } + } + } + } + } + + &.is-desktop { + @include authenticationLayoutSpacings(4rem, x-large); + // Desktop S + &.is-desktop__small { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth) { + @include authenticationLayoutSizes(75%, var(--pi-public-width-container), 70vh); + } + &.pi-layout__00-authentication--auth { + @include authenticationLayoutSizes(72%, var(--pi-public-width-container), 76vh); + } + } + } + // Desktop M & L + &.is-desktop__medium, + &.is-desktop__large { + .pi-layout__00-authentication { + &:not(.pi-layout__00-authentication--auth) { + @include authenticationLayoutSizes(72%, var(--pi-public-width-container), 78vh); + } + &.pi-layout__00-authentication--auth { + @include authenticationLayoutSizes(66.66%, var(--pi-public-width-container), 78vh); + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00a-authentication-layout-auth.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00a-authentication-layout-auth.scss new file mode 100644 index 0000000..03d6bb0 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00a-authentication-layout-auth.scss @@ -0,0 +1,83 @@ +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small Device + @media screen and (max-width: 62rem) { + .pi-layout { + &.pi-layout__00-authentication { + &.pi-layout__00-authentication--auth { + // Panel in authentication layout + .pi-panel { + .pi-panel-content__left { + display: none; + } + .pi-panel-content__right { + padding-top: var(--pi-spacing-xxx-large); + @media screen and (max-height: 41.6875rem) { + transform: scale(0.95); + &.pi-wrapper { + &.pi-wrapper__gap--xl { + gap: unset; + } + } + } + } + } + // Headline in authentication layout + .pi-headline { + &.pi-headline__type--logo { + & > figure { + max-width: 5.75rem; + } + } + } + } + + // Panel in Root auth form + &.pi-layout__00-authentication--root-org { + // Panel in authentication layout + .pi-panel { + .pi-panel-content__center { + .pi-wrapper { + &:first-child { + margin-top: var(--pi-spacing-medium); + } + } + } + } + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large Device + @media screen and (min-width: 62rem) { + .pi-layout { + &.pi-layout__00-authentication { + &.pi-layout__00-authentication--auth { + // Panel in authentication layout + .pi-panel { + .pi-panel-content__left { + display: grid; + grid-template-rows: repeat(2, 1fr); + & > *:nth-child(n + 1) { + align-self: end; + } + } + } + // Headline in authentication layout + .pi-headline { + &.pi-headline__type--logo { + & > figure { + max-width: 4.6875rem; + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00b-authentication-layout-recover-password.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00b-authentication-layout-recover-password.scss new file mode 100644 index 0000000..d6e4289 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00b-authentication-layout-recover-password.scss @@ -0,0 +1,28 @@ +html { + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--recover-password { + .pi-panel { + & > section { + .pi-panel-content__top, + .pi-panel-content__bottom { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not( + .pi-button__toggle + ):not(.pi-button__type--menu-item):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle):not(.ch2-btn) { + min-width: 13.5rem; + } + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00c-authentication-layout-user-profile.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00c-authentication-layout-user-profile.scss new file mode 100644 index 0000000..05ceb02 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00c-authentication-layout-user-profile.scss @@ -0,0 +1,109 @@ +@mixin authenticationLayoutSelectMenuTypeCustom($columnsNum: 1, $card-height: unset) { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--user-profile { + .pi-select { + .pi-select-options-menu { + padding: unset; + background-color: transparent; + } + &.pi-select__menu-type--custom-grid { + .pi-select-options-menu { + & > div { + display: grid; + grid-template-columns: repeat(#{$columnsNum}, 1fr); + grid-gap: var(--pi-spacing-large); + .pi-card { + min-height: $card-height; + } + } + } + } + } + // Grid wrapper 3 cols in Authentiocation User Type layout + .pi-wrapper { + &.pi-wrapper__grid { + &.pi-wrapper__grid--columns-3 { + &.is-responsive { + grid-template-columns: repeat(#{$columnsNum}, 1fr); + } + } + } + } + } + } +} + +@mixin authenticationLayoutMobile { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--user-profile { + .pi-select-option__large { + width: -webkit-fill-available; + } + .pi-card { + .pi-card-content__bottom { + display: none; + } + } + .pi-panel__gap--xxxl { + & > section { + gap: var(--pi-spacing-x-small); + } + } + } + } +} + +.pi-layout__00-authentication { + &.pi-layout__00-authentication--user-profile { + .pi-card { + &.is-active { + .pi-card-content__bottom { + & > span { + font: { + weight: 400; + } + } + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include authenticationLayoutSelectMenuTypeCustom; + @include authenticationLayoutMobile; + } + &:not(.is-desktop) { + @media screen and (max-width: 62rem) { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--user-profile { + .pi-panel-content__center { + padding-top: var(--pi-spacing-medium); + } + } + } + } + } + // Tablet + &.is-tablet { + // Tablet Small + &.is-tablet__small { + @include authenticationLayoutSelectMenuTypeCustom; + } + // Tablet L + &.is-tablet__large { + @include authenticationLayoutSelectMenuTypeCustom(3, 9rem); + } + } + // Desktop + &.is-desktop { + &.is-desktop__small { + @include authenticationLayoutSelectMenuTypeCustom(3, 9.5rem); + } + &:not(.is-desktop__small) { + @include authenticationLayoutSelectMenuTypeCustom(3); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00d-authentication-layout-default-space.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00d-authentication-layout-default-space.scss new file mode 100644 index 0000000..bd18b06 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00d-authentication-layout-default-space.scss @@ -0,0 +1,41 @@ +@mixin authenticationDefaultSpaceLayoutSpacings($spacing, $gap) { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--default-space { + .pi-panel { + .pi-panel-content__top, + .pi-panel-content__bottom { + gap: var(--pi-spacing-#{$gap}); + } + &.pi-panel__inner-padding--xxxxl { + &.pi-panel__inner-padding { + &.pi-panel__flex-direction--column { + & > section { + & > * { + padding-left: $spacing; + padding-right: $spacing; + } + } + } + } + } + } + } + } +} + +html { + &.is-mobile { + @include authenticationDefaultSpaceLayoutSpacings(var(--pi-spacing-xxx-large), x-small); + } + &.is-tablet { + &.is-tablet__small { + @include authenticationDefaultSpaceLayoutSpacings(var(--pi-spacing-xxxx-large), small); + } + &.is-tablet__large { + @include authenticationDefaultSpaceLayoutSpacings(8rem, x-large); + } + } + &.is-desktop { + @include authenticationDefaultSpaceLayoutSpacings(11rem, x-large); + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00e-authentication-layout-impersonation.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00e-authentication-layout-impersonation.scss new file mode 100644 index 0000000..013b453 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-00e-authentication-layout-impersonation.scss @@ -0,0 +1,28 @@ +html { + &:not(.is-mobile) { + .pi-shell { + .is-public { + .pi-layout__00-authentication { + &.pi-layout__00-authentication--impersonation { + .pi-panel { + & > section { + .pi-panel-content__top, + .pi-panel-content__bottom { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not( + .pi-button__toggle + ):not(.pi-button__type--menu-item):not(.pi-button__icon--single):not( + .pi-button__xx-small + ):not(.pi-button__trimmed):not(.pi-button__toggle):not(.ch2-btn) { + min-width: 13.5rem; + } + } + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout-old.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout-old.scss new file mode 100644 index 0000000..460424d --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout-old.scss @@ -0,0 +1,239 @@ +.pi-layout { + &.pi-layout__01-dashboard-old { + .pi-panel__primary { + border: none; + background-color: var(--pi-color-primary); + } + } +} + +.is-advisor { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } +} + +.pi-layout { + &.pi-layout__01-dashboard-old { + &.is-company { + .pi-wrapper { + &.pi-wrapper__main { + & > section { + align-items: stretch; + .pi-panel { + height: 100%; + & > section { + height: 100%; + } + } + } + } + } + } + } +} + +.pi-layout { + &.pi-layout__01-dashboard-old { + &.is-investor { + & > div { + & > div { + & > * { + display: block; + .pi-panel { + height: 100%; + } + } + } + } + .pi-panel { + & > section { + height: 100%; + // Press component + & > div { + a { + & > .pi-headline { + & > figure { + width: unset; + img { + height: 1.75rem; + width: 1.75rem; + } + } + } + } + } + } + } + } + } +} +html { + // Mobile + &.is-mobile { + .is-advisor { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + .is-company { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + + .pi-layout { + &.pi-layout__01-dashboard-old { + &.is-investor { + .pi-panel { + & > section { + // Deal manager component + & > .pi-headline { + flex-direction: row; + } + } + } + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + } + // Tablet + &.is-tablet { + .is-advisor { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + .is-company { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + .is-investor { + .pi-layout { + &.pi-layout__01-dashboard-old { + } + } + } + // Tablet S + &.is-tablet__small { + .is-advisor { + } + .is-company { + } + .is-investor { + } + } + // Tablet L + &.is-tablet__large { + .is-advisor { + } + .is-company { + } + .is-investor { + } + } + } + // Large device + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .is-mode-list { + .pi-layout { + &.pi-layout__01-dashboard-old { + .pi-data-grid { + &.pi-data-grid__data-direction--row { + height: calc(100% - 2.6563rem); + .pi-data-grid__content { + display: flex; + flex-direction: column; + justify-content: space-between; + height: calc(100% - 4.6563rem); + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-close-button):not( + .pi-hamburger + ):not(.pi-button__xx-small) { + height: 2.4rem; + min-height: unset; + padding-left: var(--pi-spacing-medium); + padding-right: var(--pi-spacing-medium); + } + } + .pi-chip-label { + max-width: 15ch; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + } + } + } + } + } + } + // Desktop + &.is-desktop { + .is-advisor { + } + .is-company { + } + .is-investor { + } + // Desktop S + &.is-desktop__small { + .pi-layout__01-dashboard-old { + width: 90%; + } + .is-advisor { + } + .is-company { + } + .is-investor { + } + } + + // Desktop M + &.is-desktop__medium { + .pi-layout__01-dashboard-old { + width: 85%; + } + .is-advisor { + } + .is-company { + } + .is-investor { + } + } + + // Desktop L + &.is-desktop__large { + .pi-layout__01-dashboard-old { + width: 75%; + } + .is-advisor { + } + .is-company { + } + .is-investor { + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout.scss new file mode 100644 index 0000000..e333683 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01-dashboard-layout.scss @@ -0,0 +1,47 @@ +@mixin dashboardLayoutWidth( + $width: calc(100% - var(--pi-spacing-x-large)), + $max-width: calc(100% - var(--pi-spacing-x-large)), + $margin: var(--pi-height-header-mobile) auto 0 +) { + .pi-layout { + &.pi-layout__01-dashboard { + width: $width; + max-width: $max-width; + margin: $margin; + } + } +} + +.pi-layout { + &.pi-layout__01-dashboard { + display: grid; + align-items: center; + } +} + +html { + // Maobile & Tablet + &:not(.is-desktop) { + // Small devices + @include dashboardLayoutWidth; + } + // Large device + &:not(.is-mobile) { + // Tablet L & Desktop S, M & L > 2400px + @media screen and (min-width: 62rem) and (max-width: 140rem) { + @include dashboardLayoutWidth( + calc(100% - var(--pi-spacing-x-large)), + calc(100% - var(--pi-spacing-x-large)), + unset + ); + } + // X Large device + @media screen and (min-width: 140rem) { + @include dashboardLayoutWidth( + calc(100% - var(--pi-spacing-x-large) - var(--pi-width-aside) - var(--pi-spacing-x-large)), + calc(100% - var(--pi-spacing-x-large) - var(--pi-width-aside) - var(--pi-spacing-x-large)), + unset + ); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01a-dashboard-layout-company.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01a-dashboard-layout-company.scss new file mode 100644 index 0000000..e1f9f66 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01a-dashboard-layout-company.scss @@ -0,0 +1,323 @@ +$pi-seller-dashboard-overviews-height: 7.5rem; // 120px + +:root { + // Company Dashboard Spacings + --pi-seller-dashboard-overviews-height: #{$pi-seller-dashboard-overviews-height}; // 3.125rem; // 50px +} + +@mixin companyDashboardGridMobileAndTablet { + .pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-medium); + & > *:nth-child(n + 1) { + display: block; + width: -webkit-fill-available; + } + } + } + } +} + +@mixin companyDashboardInsightsPanelEmptyState($width: 66.66%) { + .pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + & > *:nth-child(2) { + &.is-empty { + & > .pi-panel { + .pi-panel-content__center { + display: flex; + justify-content: center; + align-items: center; + min-height: calc( + 100vh - var(--pi-spacing-xxxx-large) - var(--pi-height-header) - 23.65rem + ); + .pi-note { + width: $width; + } + } + } + } + } + } + } + } +} + +@mixin companyDashboardGridDesktopSmall { + .pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + grid-template-columns: repeat(2, 1fr); + grid-template-areas: + 'item1 item1' + 'item2 item3' + 'item4 item5'; + & > *:nth-child(2), + & > *:nth-child(3) { + & > * { + max-width: 100%; + } + } + } + } + } +} + +@mixin companyDashboardGridDesktop { + .pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + grid-template-columns: repeat(6, 1fr); + grid-template-areas: + 'item1 item1 item1 item1 item3 item3' + 'item2 item2 item2 item2 item3 item3' + 'item4 item4 item5 item5 item3 item3'; + + & > *:nth-child(3) { + & > * { + min-height: calc(100vh - var(--pi-spacing-xxxx-large) - var(--pi-height-header)); + } + .pi-panel-content { + // Center + &.pi-panel-content__center { + padding: unset; + + &:nth-child(2) { + & > *:nth-child(n + 1) { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + & > article { + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-x-small); + } + } + } + } + } + } + } + } + } +} + +// Accordion horizontal in Insights panel +@mixin companyDashboardHorizontalAccordionDesktop { + .pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + .pi-accordion { + &.pi-accordion__direction--horizontal { + & > section { + &:nth-child(1) { + header { + .pi-list-item { + flex-direction: column; + gap: unset; + .pi-list-item-icon { + &.pi-list-item-icon__left { + max-width: 100%; + width: 100%; + // Insights chip + .pi-chip__large { + &:not(.pi-chip__single-icon) { + height: calc( + var(--pi-spacing-xxxx-large) - var(--pi-spacing-xxx-small) + ); + max-width: 100%; + width: 100%; + } + } + } + } + .pi-list-item-content { + &.pi-list-item-content__left { + display: flex; + align-items: flex-end; + text-align: center; + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } + } + } + } + } + } +} + +.pi-layout { + &.pi-layout__01-dashboard { + &.is-company { + @for $i from 1 through 5 { + & > *:nth-child(#{$i}) { + grid-area: item#{$i}; + align-self: flex-start; + } + } + grid-gap: var(--pi-spacing-medium); + // Overviews + & > *:nth-child(1) { + .pi-card { + min-height: var(--pi-seller-dashboard-overviews-height); + + .pi-card-content { + &.pi-card-content__top { + padding-left: var(--pi-spacing-large); + padding-top: var(--pi-spacing-medium); + padding-right: var(--pi-spacing-large); + } + &.pi-card-content__bottom { + flex-direction: row; + align-items: center; + min-height: var(--pi-spacing-xxxx-large); + } + } + &:hover { + border-color: var(--pi-color-primary-100); + } + } + } + // TO·DO List + & > *:nth-child(3) { + .pi-panel { + .pi-panel-content { + & > .pi-divider { + padding-top: var(--pi-spacing-xxx-small); + & + * { + padding-top: var(--pi-spacing-medium); + } + } + + & > .pi-text-viewer { + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-x-small); + } + + // Top + &.pi-panel-content__top { + padding-left: var(--pi-spacing-xx-large); + padding-right: var(--pi-spacing-xx-large); + & > span { + line-height: 105%; + } + } + } + } + &:not(.is-empty) { + & > *:nth-child(3) { + .pi-panel { + .pi-panel-content { + // Center + &.pi-panel-content__center { + padding: unset; + + &:nth-child(2) { + .pi-icon__appearance--fill { + &.pi-color__primary { + background-color: var(--pi-color-primary-50); + } + } + & > *:nth-child(n + 1) { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + & > article { + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-x-small); + } + } + .pi-wrapper__flex-direction--column { + // Only scroll for desktop M & L + @media screen and (min-width: 90rem) { + max-height: calc( + 100vh - var(--pi-spacing-xxxx-large) - var(--pi-height-header) - 24rem + ); + max-width: calc(100% - var(--pi-spacing-xxx-small)); + overflow-y: auto; + // Generic Scroll + &::-webkit-scrollbar { + width: var(--pi-spacing-xxxx-small); + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-primary-100); + border-radius: var(--pi-border-radius-small); + + &:hover { + transition: var(--pi-animation); + background-color: var(--pi-color-primary-200); + } + } + } + } + } + } + } + } + } + } + } + // Deal manager CTA + & > *:nth-child(5) { + .pi-headline { + height: var(--pi-spacing-xxx-large); + width: var(--pi-spacing-xxx-large); + &::after { + content: ''; + position: relative; + z-index: 99; + height: var(--pi-spacing-xx-small); + width: var(--pi-spacing-xx-small); + margin-top: -4rem; + margin-left: 2.25rem; + border-radius: 50%; + background-color: var(--pi-color-success); + } + } + .pi-list-item-icon { + &.pi-list-item-icon__right { + display: flex; + align-items: center; + gap: var(--pi-spacing-xxx-small); + } + } + } + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + @include companyDashboardGridMobileAndTablet; + } + + &.is-desktop { + // Desktop S + &.is-desktop__small { + @include companyDashboardGridDesktopSmall; + @include companyDashboardInsightsPanelEmptyState; + } + // Desktop S & M + &:not(.is-desktop__large) { + @include companyDashboardHorizontalAccordionDesktop; + } + // Desktop M & L + &:not(.is-desktop__small) { + @include companyDashboardGridDesktop; + @include companyDashboardInsightsPanelEmptyState(50%); + } + } +} diff --git a/apps/api/src/assets/.gitkeep b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01b-dashboard-layout-investor.scss similarity index 100% rename from apps/api/src/assets/.gitkeep rename to libs/ui/src/lib/styles/deale-saas/layouts/_pi-01b-dashboard-layout-investor.scss diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01c-dashboard-layout-advisor.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-01c-dashboard-layout-advisor.scss new file mode 100644 index 0000000..e69de29 diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02-full-width-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02-full-width-layout.scss new file mode 100644 index 0000000..d30d80e --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02-full-width-layout.scss @@ -0,0 +1,139 @@ +@mixin fullWidthLayoutWidthLargeDevice( + $width: calc(100vw - var(--pi-width-aside) - var(--pi-width-navbar-collapsed) - 6.5rem), + $max-width-closed: var(--pi-width-container-s), + $max-width-open: + calc(100% - var(--pi-width-aside) - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)) +) { + .pi-shell__deale-saas { + & > div { + // Aside is open + &.aside-is-open { + // Layout full width + .pi-layout { + &.pi-layout__02-full-width { + &.pi-layout__collapsable { + transition: var(--pi-animation); + max-width: $max-width-open; + } + &:not(.pi-layout__collapsable) { + transition: var(--pi-animation); + max-width: $max-width-closed; + & > *:nth-child(n + 1) { + width: $width; + max-width: $max-width-closed; + } + } + } + } + } + // Aside is closed + &.aside-is-closed { + // Layout full width + .pi-layout { + &.pi-layout__02-full-width { + transition: var(--pi-animation); + & > *:nth-child(n + 1) { + width: $width; + max-width: $max-width-closed; + } + } + } + } + } + } +} +@mixin fullWidthLayoutGridWrapper3Columns($columns: 1) { + .pi-layout { + &.pi-layout__02-full-width { + .pi-wrapper { + &.pi-wrapper__grid { + // 3 + &.pi-wrapper__grid--columns-3 { + grid-template-columns: repeat(#{$columns}, 1fr); + } + } + } + } + } +} + +html { + &.is-mobile { + @include fullWidthLayoutGridWrapper3Columns; + } + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__02-full-width { + max-width: calc(100% - var(--pi-spacing-x-large)); + } + } + } + + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include fullWidthLayoutGridWrapper3Columns; + } + // Tablet L + &.is-tablet__large { + @include fullWidthLayoutGridWrapper3Columns(2); + } + .pi-layout { + &.pi-layout__02-full-width { + margin: auto; + } + } + } + // Desktop + &.is-desktop { + .pi-layout { + &.pi-layout__02-full-width { + padding: 0 0 calc(var(--pi-spacing-xxxx-large) + var(--pi-height-footer)); + } + } + + // Desktop S + &.is-desktop__small { + @include fullWidthLayoutWidthLargeDevice; + @include fullWidthLayoutGridWrapper3Columns(2); + } + &:not(.is-desktop__small) { + @include fullWidthLayoutGridWrapper3Columns(3); + } + // Desktop M + &.is-desktop__medium { + @include fullWidthLayoutWidthLargeDevice( + calc(100vw - var(--pi-width-aside) - var(--pi-width-navbar-collapsed) - 6.5rem), + var(--pi-width-container), + calc(100% - var(--pi-width-aside) - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)) + ); + } + // Desktop L + &.is-desktop__large { + // Desktop L + @media screen and (max-width: 140rem) { + .pi-layout { + &.pi-layout__02-full-width { + & > *:nth-child(n + 1) { + width: var(--pi-width-container-l); + max-width: var(--pi-width-container-l); + } + } + } + } + // Desktop XL + @media screen and (min-width: 140rem) { + .pi-layout { + &.pi-layout__02-full-width { + & > *:nth-child(n + 1) { + width: 66.66%; + max-width: 66.66%; + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02a-full-width-auto-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02a-full-width-auto-layout.scss new file mode 100644 index 0000000..e2416ea --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-02a-full-width-auto-layout.scss @@ -0,0 +1,20 @@ +.pi-layout { + &.pi-layout__02-full-width { + &.pi-layout__02-full-width--auto { + margin: auto; + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__02-full-width { + &.pi-layout__02-full-width--auto { + padding: var(--pi-spacing-x-large); + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-03-vertically-centered-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-03-vertically-centered-layout.scss new file mode 100644 index 0000000..57da7b1 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-03-vertically-centered-layout.scss @@ -0,0 +1,102 @@ +@mixin layoutVerticallyCenteredSpacings($padding-top: var(--pi-public-height-header-mobile)) { + .pi-container-layout { + & > .pi-layout__06-edge-to-edge { + & > .pi-wrapper__header { + .pi-header-wrapper-content__center { + padding-top: $padding-top; + min-width: 75%; + } + } + } + } +} + +.pi-layout { + &.pi-layout__03-vertically-centered { + display: flex; + flex-direction: column; + justify-content: center; + + padding: var(--pi-spacing-xxxx-large); + background-position: center; + background-size: cover; + background-repeat: no-repeat; + &:not(.pi-layout__small):not(.pi-layout__medium):not(.pi-layout__large) { + min-height: 100vh; + } + + // Main wrapper align center and justify center in VerticallyCentered layout + .pi-wrapper__main { + &.pi-wrapper__align-items--center { + &.pi-wrapper__justify--center { + display: flex; + align-items: center; + } + } + } + } +} + +.pi-shell { + .is-fullscreen { + .pi-layout { + &.pi-layout__03-vertically-centered { + // Main wrapper align center and justify center in VerticallyCentered layout + & > .pi-wrapper__main { + &.pi-wrapper__align-items--center { + &.pi-wrapper__justify--center { + height: 100vh; + & > section { + gap: var(--pi-spacing-xxxx-large); + } + + .pi-headline { + gap: var(--pi-spacing-xxxx-large); + .pi-headline-content { + & > *:nth-child(n + 1) { + padding-left: var(--pi-spacing-xx-large); + padding-right: var(--pi-spacing-xx-large); + margin-top: var(--pi-spacing-x-small); + } + } + & > figure { + width: 7.1875rem; + } + } + .pi-close-button { + position: absolute; + @media screen and (max-width: 40rem) { + top: var(--pi-spacing-large); + right: var(--pi-spacing-large); + } + @media screen and (min-width: 40rem) and (max-width: 77.5rem) { + top: var(--pi-spacing-xx-large); + right: var(--pi-spacing-xx-large); + } + @media screen and (min-width: 77.5rem) { + top: var(--pi-spacing-xxxx-large); + right: var(--pi-spacing-xxxx-large); + } + } + } + } + } + } + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + @media screen and (max-width: 62rem) { + @include layoutVerticallyCenteredSpacings; + } + } + // Tablet & Desktop + &:not(.is-mobile) { + @media screen and (min-width: 62rem) { + @include layoutVerticallyCenteredSpacings(var(--pi-public-height-header-desktop)); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-04-space-between-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-04-space-between-layout.scss new file mode 100644 index 0000000..c7c1a0f --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-04-space-between-layout.scss @@ -0,0 +1,8 @@ +.pi-layout { + &.pi-layout__04-space-between { + display: flex; + flex-direction: column; + justify-content: space-between; + } +} + diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05-splitted-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05-splitted-layout.scss new file mode 100644 index 0000000..6bc88b8 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05-splitted-layout.scss @@ -0,0 +1,71 @@ +.pi-layout { + &.pi-layout__05-splitted { + display: flex; + flex-direction: column; + justify-content: space-between; + + .pi-panel { + &.pi-panel__is-clickable { + // Is active + &.is-active { + background-color: var(--pi-color-primary-50); + } + &:hover { + cursor: pointer; + } + &.is-active, + &:hover { + box-sizing: border-box; + outline-offset: -1px; + } + } + } + &:not(.pi-05a-splitted-paywall-layout) { + .pi-wrapper__action { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon) { + min-width: 10rem; + } + } + } + } + } +} + +// Desktop +html { + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__05-splitted { + overflow-y: scroll; + } + } + } + &.is-desktop { + .pi-layout { + &.pi-layout__05-splitted { + .pi-main-wrapper-content__right { + .mat-mdc-radio-group { + &.pi-wrapper { + &.pi-wrapper__flex { + max-width: 50%; + } + } + } + } + + @media screen and (min-width: 90rem) { + overflow-y: hidden; + } + &:not(.pi-05a-splitted-paywall-layout) { + height: 100vh; + padding: 0; + .pi-main-wrapper-content { + min-height: 100vh; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05a-splitted-paywall-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05a-splitted-paywall-layout.scss new file mode 100644 index 0000000..90c5504 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-05a-splitted-paywall-layout.scss @@ -0,0 +1,413 @@ +.pi-layout { + &.pi-05a-splitted-paywall-layout { + width: 100vw; + background-size: contain; + background-repeat: no-repeat; + background-position-x: right; + + .pi-main-wrapper-content { + min-height: unset; + // Left + &.pi-main-wrapper-content__left { + display: flex; + flex-direction: column; + header { + h1 + * { + margin-top: var(--pi-spacing-x-small); + } + } + } + // Right + &.pi-main-wrapper-content__right { + display: flex; + flex-direction: column; + } + } + + * + .mat-mdc-radio-group { + padding-top: var(--pi-spacing-x-large); + } + .mat-mdc-radio-group { + display: flex; + flex-direction: column; + label { + margin-bottom: unset; + } + } + .mat-mdc-radio-label-content { + padding-left: var(--pi-spacing-medium); + } + + .pi-panel { + box-shadow: var(--pi-box-shadow); + transition: var(--pi-animation); + width: 100%; + max-width: 30rem; + &.is-active { + border: 1px solid var(--pi-color-primary-100); + background-color: var(--pi-color-primary-50); + } + + &:hover { + cursor: pointer; + box-shadow: var(--pi-box-shadow-hover); + &:not(.is-active) { + border: 1px solid var(--pi-color-primary-200); + } + } + // Padding + &.has-padding { + width: 100%; + padding: var(--pi-spacing-large); + } + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + padding: var(--pi-spacing-x-large) var(--pi-spacing-x-large) 6.75rem; + @media screen and (max-height: 46.25rem) { + min-height: calc(198vh + var(--pi-height-footer)); + } + .pi-main-wrapper-content { + // Right + &.pi-main-wrapper-content__right { + height: calc( + 100vh - var(--pi-spacing-x-large) - var(--pi-spacing-x-large) - + var(--pi-height-footer) - 8rem + ); + } + // Left + &.pi-main-wrapper-content__left { + // Header + & > header { + transform: scale(0.92); + .pi-color__black-primary { + & > article { + display: flex; + gap: var(--pi-spacing-xxx-small); + } + } + } + } + } + + // Radio panels + .mat-mdc-radio-group { + gap: var(--pi-spacing-small); + width: 100%; + transform: scale(0.92); + } + + .pi-button { + &:not(.pi-button__icon--single):not(.pi-button__x-small):not(.pi-button__small):not( + .pi-button__large + ) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-large); + } + } + } + } + } + } + &.is-tablet { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + padding: var(--pi-spacing-xx-large) var(--pi-spacing-xx-large) 6.75rem; + .pi-wrapper__main { + .pi-main-wrapper-content { + &.pi-main-wrapper-content__left { + & > header { + max-width: 66.66%; + margin: 0 auto; + } + } + &.pi-main-wrapper-content__right { + min-height: calc( + 100vh - var(--pi-spacing-xx-large) - var(--pi-spacing-xx-large) - + var(--pi-height-footer) - 20rem + ); + } + } + } + .pi-button { + &:not(.pi-button__icon--single):not(.pi-button__x-small):not(.pi-button__small):not( + .pi-button__large + ) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-large); + min-width: 11rem; + } + } + } + } + } + // Tablet S + &.is-tablet__small { + .pi-wrapper__main { + .pi-main-wrapper-content { + & > * + *:not(:last-child) { + width: 66.66%; + margin: var(--pi-spacing-x-large) auto; + } + } + } + } + // Tablet L + &.is-tablet__large { + .pi-wrapper__main { + .pi-main-wrapper-content { + & > * + *:not(:last-child) { + width: 50%; + margin: var(--pi-spacing-x-large) auto; + } + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + // Main + .pi-wrapper { + &.pi-wrapper__main { + & > section { + padding-bottom: var(--pi-height-header); + } + .pi-main-wrapper-content { + width: 100%; + // Left + &.pi-main-wrapper-content__left { + & > header { + justify-content: center; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + h3 { + display: block; + } + } + form { + & > h4 { + text-align: center; + } + } + // Action wrapper + .pi-wrapper__action { + position: fixed; + bottom: 0; + left: 0; + z-index: 999; + height: var(--pi-height-footer); + width: 100%; + margin-top: unset; + background-color: var(--pi-color-white-rgba-06); + box-shadow: var(--pi-box--shadow-top); + } + } + // Right + &.pi-main-wrapper-content__right { + gap: var(--pi-spacing-xxxx-large); + padding-top: 6rem; + background-image: unset; + + & > form { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + width: 100%; + } + } + } + } + } + .mat-mdc-radio-group { + align-items: center; + } + .mat-mdc-radio-label-content { + h3 { + font: { + size: var(--pi-font-size-xx-large); + } + } + } + // Mobile and Tablet S + @media screen and (max-width: 62rem) { + .pi-main-wrapper-content { + &.pi-main-wrapper-content__left { + // Header + & > header { + h1 { + font: { + size: var(--pi-font-size-x-large); + } + } + } + } + } + } + // Action wrapper + .pi-wrapper__action { + justify-content: space-around; + } + } + } + } + // Small device + @media screen and (max-width: 62rem) { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + .mat-mdc-radio-group > * { + width: -webkit-fill-available; + } + } + } + } + } + } // Tablet & Desktop + &:not(.is-mobile) { + // Tablet L and Desktop + @media screen and (min-width: 62rem) { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + .pi-main-wrapper-content { + &.pi-main-wrapper-content__left { + // Header + & > header { + h1 { + font: { + size: var(--pi-font-size-xx-large); + } + } + } + } + } + + .mat-mdc-radio-label-content { + h3 { + font: { + size: 2.375rem; + } + } + } + } + } + } + } + } + // Desktop + &.is-desktop { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + height: 100vh; + overflow-y: hidden; + .pi-wrapper__main { + .pi-main-wrapper-content { + // Left + &.pi-main-wrapper-content__left { + justify-content: space-between; + max-width: 50%; + .pi-panel { + &.has-padding { + max-width: 32rem; + } + } + .pi-wrapper { + &.pi-wrapper__action { + gap: var(--pi-spacing-x-large); + } + } + } + // Right + &.pi-main-wrapper-content__right { + justify-content: center; + } + } + } + } + } + } + &.is-desktop__small { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + padding: var(--pi-spacing-xxx-large); + gap: var(--pi-spacing-xx-large); + .pi-main-wrapper-content { + height: calc(100vh - var(--pi-spacing-xxx-large) - var(--pi-spacing-xxx-large)); + // Left + &.pi-main-wrapper-content__left { + .mat-mdc-radio-group { + max-width: 70%; + } + } + + // Right + &.pi-main-wrapper-content__right { + gap: var(--pi-spacing-medium); + } + } + } + } + } + } + &.is-desktop__medium { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + padding: var(--pi-spacing-xxx-large); + gap: var(--pi-spacing-xxx-large); + .pi-main-wrapper-content { + // Left + &.pi-main-wrapper-content__left { + height: calc(100vh - var(--pi-spacing-xxx-large) - var(--pi-spacing-xxx-large)); + .mat-mdc-radio-group { + max-width: 66.66%; + } + } + // Right + &.pi-main-wrapper-content__right { + gap: var(--pi-spacing-x-large); + } + } + } + } + } + } + } + &.is-desktop__large { + .pi-layout { + &.pi-layout__05-splitted { + &.pi-05a-splitted-paywall-layout { + padding: var(--pi-spacing-xxxx-large); + gap: var(--pi-spacing-xxxx-large); + + .pi-main-wrapper-content { + height: calc(100vh - var(--pi-spacing-xxxx-large) - var(--pi-spacing-xxxx-large)); + // Left + &.pi-main-wrapper-content__left { + .mat-mdc-radio-group { + max-width: 50%; + } + .pi-wrapper__action { + margin-top: 5rem; + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-06-edge-to-edge-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-06-edge-to-edge-layout.scss new file mode 100644 index 0000000..336ca0e --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-06-edge-to-edge-layout.scss @@ -0,0 +1,52 @@ +@mixin saasContainerSpacings($padding-top: var(--pi-public-height-header-mobile)) { + .pi-container-layout { + padding-bottom: var(--pi-spacing-xxxx-large); + & > .pi-layout__06-edge-to-edge { + & > .pi-wrapper__header { + .pi-header-wrapper-content__center { + padding-top: $padding-top; + } + } + } + } +} + +.pi-layout { + &.pi-layout__06-edge-to-edge { + width: 100%; + & > *:nth-child(n + 2) { + &:not(.pi-wrapper__list) { + display: block; + margin-top: var(--pi-spacing-x-large); + } + } + } +} +html { + // Mobile + &.is-mobile { + .pi-layout { + &.pi-layout__06-edge-to-edge { + padding: var(--pi-spacing-x-large); + } + } + } + + // Tablet + &.is-tablet { + .pi-layout { + &.pi-layout__06-edge-to-edge { + padding: var(--pi-spacing-xxx-large); + } + } + } + + // Desktop + &.is-desktop { + .pi-layout { + &.pi-layout__06-edge-to-edge { + padding: var(--pi-spacing-xxxx-large); + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07-list-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07-list-layout.scss new file mode 100644 index 0000000..0d48638 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07-list-layout.scss @@ -0,0 +1,221 @@ +@mixin wrappersWidth( + $width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)), + $justify-content: center, + $margin: 0 auto, + $side-spacing: x-large +) { + .pi-layout { + &.pi-layout__07-list { + .pi-wrapper { + // Wrapper + &:not(.pi-wrapper__list):not(.pi-wrapper__flex) { + max-width: $width; + width: -webkit-fill-available; + } + // List Wrapper + &.pi-wrapper__list { + .pi-data-grid { + justify-content: $justify-content; + } + } + } + } + } + + .pi-layout { + &.pi-layout__07-list { + display: flex; + flex-direction: column; + justify-content: space-between; + // Mode Grid + .pi-wrapper { + // List Wrapper + &.pi-wrapper__list { + max-width: $width; + margin-top: var(--pi-spacing-xx-large); + } + } + // List Layout and edge to edge + &.pi-layout__05-edge-to-edge { + // Mode Grid + .pi-wrapper { + // List Wrapper + &.pi-wrapper__list { + .pi-data-grid { + .pi-grid-item { + // < Desktop L + @media screen and (max-width: 120rem) { + max-width: calc(100% - var(--pi-spacing-x-large)); + } + // >= Desktop L + @media screen and (min-width: 120rem) { + max-width: $width; + } + } + } + } + } + } + } + } +} + +@mixin listLayoutHeight($height: calc(100vh - var(--pi-height-header-mobile))) { + .pi-shell__deale-saas { + & > div { + .pi-layout { + &.pi-layout__07-list { + height: $height; + } + } + } + } +} + +@mixin paginatorWidth($width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large))) { + .pi-shell__deale-saas { + & > div { + .pi-layout { + &.pi-layout__07-list { + .pi-paginator { + max-width: $width; + } + } + } + } + } +} + +.pi-shell__deale-saas { + & > div { + // Grid mode (Infinite scroll) + &.is-mode-grid { + .pi-layout { + &.pi-layout__07-list { + width: 100%; + overflow-y: hidden; + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include wrappersWidth; + @include listLayoutHeight; + @include paginatorWidth; + } + // Mobile & Tablet + &:not(.is-desktop) { + .pi-shell__deale-saas { + .pi-icon-button { + &.is-sticky { + position: absolute; + right: var(--pi-spacing-x-large); + bottom: calc( + var(--pi-height-footer) + env(safe-area-inset-bottom) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-small) + ); + z-index: 99; + } + } + } + // Small device + @media screen and (max-width: 62rem) { + .pi-shell__deale-saas { + & > div { + .pi-layout { + &.pi-layout__07-list { + .pi-wrapper__header { + .pi-switch-button { + display: none; + } + } + } + } + // List mode + &.is-mode-list { + .pi-layout { + &.pi-layout__07-list { + .pi-wrapper__header { + padding-top: var(--pi-height-header-tablet); + } + } + } + } + } + // Grid mode + &.is-mode-grid { + .pi-layout { + &.pi-layout__07-list { + // Wrapper + .pi-wrapper { + // Header wrapper + &.pi-wrapper__header { + min-height: var(--pi-height-layout-header); + margin: var(--pi-height-header-mobile) auto 0; + } + } + } + } + } + } + } + } + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include listLayoutHeight(calc(100vh - var(--pi-height-header-tablet))); + @include wrappersWidth; + @include paginatorWidth; + } + // Tablet L + &.is-tablet__large { + @include listLayoutHeight( + calc( + 100vh - var(--pi-height-header-tablet) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-xx-large) + ) + ); + @include wrappersWidth( + calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)), + left, + unset, + unset + ); + @include paginatorWidth(calc(100%)); + } + } + + &.is-desktop { + @include listLayoutHeight( + calc( + 100vh - var(--pi-height-header-desktop) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-medium) + ) + ); + // Desktop S + &.is-desktop__small { + @include wrappersWidth( + calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)), + left, + unset, + unset + ); + @include paginatorWidth(calc(100%)); + } + // Desktop M + &.is-desktop__medium { + @include wrappersWidth(calc(100% - var(--pi-spacing-x-large)), left, unset, unset); + @include paginatorWidth(calc(100% - var(--pi-spacing-x-large))); + } + // Desktop L + &.is-desktop__large { + @include wrappersWidth(var(--pi-width-container-l), left, unset, unset); + @include paginatorWidth(calc(var(--pi-width-container-l))); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07a-list-auto-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07a-list-auto-layout.scss new file mode 100644 index 0000000..2434860 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-07a-list-auto-layout.scss @@ -0,0 +1,18 @@ +.pi-layout { + &.pi-layout__07-list { + &.pi-layout__07a-list--auto { + // Mode Grid + .pi-wrapper { + margin: 0 auto var(--pi-spacing-x-large); + // List Wrapper + &.pi-wrapper__list { + & > section { + & > div { + margin: auto; + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-08-form-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-08-form-layout.scss new file mode 100644 index 0000000..70eeb09 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-08-form-layout.scss @@ -0,0 +1,19 @@ +.pi-layout { + &.pi-layout__05-splitted { + .pi-layout { + &.pi-layout__08-form { + padding-top: var(--pi-spacing-medium); + &::-webkit-scrollbar { + display: block; + width: var(--pi-spacing-xxx-small); + } + } + } + } + &.pi-layout__08-form { + .pi-filter-range { + display: block; + width: 100%; + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-container-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-container-layout.scss new file mode 100644 index 0000000..3a65530 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-container-layout.scss @@ -0,0 +1,3 @@ +.pi-container-layout { + padding-bottom: var(--pi-spacing-xxxx-large); +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/_pi-layout.scss b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-layout.scss new file mode 100644 index 0000000..fe0b79f --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/_pi-layout.scss @@ -0,0 +1,203 @@ +@mixin layoutHeight( + $height: + calc( + 100vh - var(--pi-height-header-mobile) - var(--pi-height-footer) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) + ) +) { + .pi-layout { + &.pi-layout__main, + &.pi-layout__01-dashboard { + height: $height; + } + } +} + +@mixin layoutWidth( + $max-width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)), + $margin: var(--pi-height-header-mobile) auto 0 +) { + .pi-layout { + // Default scroll + &.pi-layout__main { + max-width: $max-width; + margin: $margin; + } + } +} + +.pi-layout { + &:not(.pi-layout__00-authentication):not(.pi-layout__01-dashboard) { + display: block; + } + + // Default scroll + &:not(.pi-layout__07-list):not(.pi-layout__00-authentication):not(.pi-layout__05-splitted):not(.pi-layout__08-form + ) { + overflow-y: scroll; + overflow-x: hidden; + } + &.pi-layout__main { + & > *:nth-child(1) { + margin-top: var(--pi-spacing-xx-large); + } + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-x-large); + } + & > .pi-component { + display: block; + & > .pi-panel { + box-shadow: var(--pi-box-shadow); + } + } + } + + &.pi-layout__no-padding { + padding: 0; + } +} + +html { + // Mobile + &.is-mobile { + @include layoutHeight; + @include layoutWidth; + .has-bottom-sticky-note { + .pi-layout { + &.pi-layout__01-dashboard, + &.pi-layout__main { + padding-bottom: 11.5rem; + } + &.pi-layout__07-list { + & > .pi-wrapper__list { + & > section { + padding-bottom: 5rem; + } + } + } + } + } + .pi-layout { + // Default scroll + &:not(.pi-layout__07-list):not(.pi-layout__00-authentication):not( + .pi-layout__05-splitted + ):not(.pi-layout__08-form) { + max-width: calc(100vw - var(--pi-spacing-medium) - var(--pi-spacing-medium)); + } + &.pi-layout__main { + padding: 0 0 calc(var(--pi-spacing-medium) + var(--pi-height-footer)); + & > *:nth-child(-n + 3) { + margin-top: var(--pi-spacing-medium); + } + } + } + } + + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + .has-bottom-sticky-note { + @include layoutHeight( + calc(100vh - var(--pi-height-header-tablet) - var(--pi-height-footer) - 2rem) + ); + .pi-layout { + &.pi-layout__main, + &.pi-layout__01-dashboard { + padding-bottom: 5rem; + } + &.pi-layout__07-list { + & > .pi-wrapper__list { + & > section { + padding-bottom: 5rem; + } + } + } + } + } + + @include layoutHeight(calc(100vh - var(--pi-height-header-tablet) - var(--pi-height-footer))); + @include layoutWidth( + calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)), + var(--pi-height-header-tablet) auto 0 + ); + .pi-layout { + // Default scroll + &:not(.pi-layout__07-list):not(.pi-layout__00-authentication):not( + .pi-layout__05-splitted + ):not(.pi-layout__08-form) { + max-width: calc(100vw - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + } + &.pi-layout__main { + padding: 0 0 calc(var(--pi-spacing-x-large) + var(--pi-height-footer)); + & > *:nth-child(-n + 3) { + margin-top: var(--pi-spacing-x-large); + } + } + } + } + // Tablet L + &.is-tablet__large { + .has-bottom-sticky-note { + .pi-layout { + &.pi-layout__main, + &.pi-layout__01-dashboard { + padding-bottom: 5rem; + } + &.pi-layout__07-list { + & > .pi-wrapper__list { + & > section { + padding-bottom: 5rem; + } + } + } + } + } + @include layoutHeight(calc(100vh - var(--pi-height-header-tablet) - 3.25rem)); + @include layoutWidth(calc(100% - var(--pi-spacing-x-large)), unset); + } + } + + // Tablet & Desktop + &:not(.is-desktop) { + // Large device + @media screen and (min-width: 62rem) { + .pi-layout { + &.pi-layout__main { + .pi-layout { + &.pi-layout__main { + & > *:nth-child(-n + 3) { + margin-top: var(--pi-spacing-xx-large); + } + } + } + } + } + } + } + + // Desktop + &.is-desktop { + .has-bottom-sticky-note { + .pi-layout { + &.pi-layout__main, + &.pi-layout__01-dashboard { + padding-bottom: 5rem; + } + &.pi-layout__07-list { + & > .pi-wrapper__list { + & > section { + padding-bottom: 5rem; + } + } + } + } + } + @include layoutHeight( + calc( + 100vh - var(--pi-height-header-tablet) - var(--pi-spacing-x-large) - var(--pi-spacing-large) + ) + ); + @include layoutWidth(calc(100% - var(--pi-spacing-x-large)), unset); + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/layouts/index.scss b/libs/ui/src/lib/styles/deale-saas/layouts/index.scss new file mode 100644 index 0000000..fcaae36 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/layouts/index.scss @@ -0,0 +1,18 @@ +@forward 'pi-00-authentication-layout'; +@forward 'pi-01-dashboard-layout'; +@forward 'pi-01a-dashboard-layout-company'; +@forward 'pi-01b-dashboard-layout-investor'; +@forward 'pi-01c-dashboard-layout-advisor'; +@forward 'pi-01-dashboard-layout-old'; +@forward 'pi-02-full-width-layout'; +@forward 'pi-02a-full-width-auto-layout'; +@forward 'pi-03-vertically-centered-layout'; +@forward 'pi-04-space-between-layout'; +@forward 'pi-05-splitted-layout'; +@forward 'pi-05a-splitted-paywall-layout'; +@forward 'pi-06-edge-to-edge-layout'; +@forward 'pi-07-list-layout'; +@forward 'pi-07a-list-auto-layout'; +@forward 'pi-08-form-layout'; +@forward 'pi-container-layout'; +@forward 'pi-layout'; diff --git a/libs/ui/src/lib/styles/deale-saas/shell/_pi-aside.scss b/libs/ui/src/lib/styles/deale-saas/shell/_pi-aside.scss new file mode 100644 index 0000000..3db453e --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/_pi-aside.scss @@ -0,0 +1,361 @@ +@mixin asideActionWrapper() { + gap: var(--pi-spacing-medium); + & > *:nth-child(n + 1) { + display: flex; + justify-content: center; + align-items: center; + min-width: 50%; + width: -webkit-fill-available; + } + + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__icon--single + ):not(.pi-button__xx-small):not(.pi-button__trimmed):not(.pi-button__toggle) { + min-width: unset; + padding-left: unset; + padding-right: unset; + } + } + .pi-button__w100 { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__icon--single + ):not(.pi-button__xx-small):not(.pi-button__trimmed):not(.pi-button__toggle) { + min-width: 100%; + } + } + } +} +.pi-shell__deale-saas { + & > div { + .pi-saas-aside { + display: block; + position: fixed; + z-index: 99; + padding: calc(var(--pi-spacing-xx-large) + env(safe-area-inset-top)) 0 + calc(var(--pi-spacing-x-large) + env(safe-area-inset-bottom)); + overflow: hidden; + transition: var(--pi-animation); + & > .pi-close-button { + position: absolute; + margin-top: -1rem; + } + & > .pi-icon { + position: absolute; + height: var(--pi-spacing-xxx-large); + width: var(--pi-spacing-xxx-large); + margin-left: var(--pi-spacing-x-small); + margin-top: -0.475rem; + transition: var(--pi-animation); + } + &.pi-saas-aside__white { + background-color: var(--pi-color-white); + border: 1px solid var(--pi-color-white-900); + .pi-buyer-companies-list-filters { + .pi-accordion + .pi-wrapper { + padding: 0 var(--pi-spacing-xx-small) var(--pi-spacing-x-large); + } + } + } + .pi-accordion-item-content__header { + & > .pi-text-viewer { + &.pi-text-viewer__font-size--m-xs { + label { + font: { + size: var(--pi-font-size-small); + } + } + } + } + } + } + + // Aside is open + &.aside-is-open { + .pi-saas-aside { + opacity: 1; + } + } + + // Aside is closed + &.aside-is-closed { + .pi-saas-aside { + transform: translateX(calc(100vw + var(--pi-width-aside) + var(--pi-spacing-x-large))); + } + } + // Aside is closable + &.aside-is-closable { + .pi-dynamic-menu { + & > *:nth-child(2) { + padding: var(--pi-spacing-x-large) var(--pi-spacing-x-large) 0; + + .pi-wrapper__action { + flex-direction: column; + @include asideActionWrapper; + } + } + + & > *:nth-child(3) { + @include asideActionWrapper; + } + } + } + + // Dynamic menu + .pi-dynamic-menu { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + overflow-y: scroll; + + .pi-panel { + width: 100%; + } + + & > *:nth-child(1) { + flex-shrink: 0; + height: calc(var(--pi-height-header-aside) + env(safe-area-inset-top)); + .pi-headline { + max-width: 50%; + margin: auto; + } + } + & > *:nth-child(2) { + &.pi-wrapper__flex { + overflow-y: auto; + height: 100%; + // Generic Scroll + &::-webkit-scrollbar { + width: 0.1875rem; + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + border-radius: var(--pi-border-radius-small); + + &:hover { + transition: var(--pi-animation); + } + } + } + + & > .pi-list-item { + border-radius: var(--pi-border-radius-medium); + padding: var(--pi-spacing-medium); + } + } + & > *:nth-child(3) { + flex-shrink: 0; + width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + margin: 0 auto; + .pi-icon-button { + margin-bottom: var(--pi-spacing-x-large); + width: -webkit-fill-available; + } + } + &.pi-saas-admin-user-profile { + .pi-icon__x-large { + &.is-stroked { + &.pi-icon__appearance--fill { + height: 7rem; + width: 7rem; + border-radius: 50%; + .mat-icon { + height: 4rem; + width: 4rem; + } + } + } + } + } + } + } + + // Aside is NOT closable + &:not(.aside-is-closable) { + .pi-dynamic-menu { + & > *:nth-child(1) { + padding: 0 var(--pi-spacing-x-large) var(--pi-spacing-x-large) + calc(var(--pi-spacing-x-large) + var(--pi-spacing-large)); + } + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-shell__deale-saas { + & > div { + .pi-saas-aside { + & > .pi-close-button { + margin-left: calc(100% - 0.9rem - var(--pi-spacing-large)); + } + } + // Aside is open + &.aside-is-open { + .pi-saas-aside { + transform: translateX(var(--pi-spacing-x-large)); + } + } + } + } + } + + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-shell__deale-saas { + & > div { + // Not fullscreen + &:not(.is-fullscreen) { + .pi-saas-aside { + top: 0; + height: calc(100vh + env(safe-area-inset-top) + env(safe-area-inset-bottom)); + width: 100%; + & > .pi-close-button { + margin-left: calc(100% - 0.9rem - var(--pi-spacing-large)); + } + + & > *:nth-child(2) { + height: calc( + 100% - env(safe-area-inset-top) - env(safe-area-inset-bottom) - + var(--pi-height-header-aside) - var(--pi-height-footer-aside) + ); + } + } + // Aside is open + &.aside-is-open { + overflow: hidden; + .pi-saas-aside { + transform: translateX(0); + transition-duration: 0.4s; + } + + & > section, + & > .pi-saas-navbar { + position: fixed; + transform: translateX(calc(-100vw)); + transition-duration: 0.4s; + } + } + // Aside is closed + &.aside-is-closed { + overflow: unset; + .pi-saas-aside { + transition-duration: 0.4s; + } + & > section, + & > .pi-saas-navbar { + transform: translateX(calc(0)); + transition-duration: 0.5s; + } + } + + .pi-dynamic-menu { + & > *:nth-child(3) { + height: calc(var(--pi-height-footer-aside) + env(safe-area-inset-bottom)); + margin: 0 auto var(--pi-spacing-xxx-large); + } + & > *:nth-child(n + 2) { + padding: 0 var(--pi-spacing-x-large); + } + } + } + // Aside is closable + &.aside-is-closable { + .pi-dynamic-menu { + & > *:nth-child(1) { + padding: 0 var(--pi-spacing-x-large) var(--pi-spacing-x-large) + var(--pi-spacing-xxxx-large); + } + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .pi-shell__deale-saas { + & > div { + // Not fullscreen + &:not(.is-fullscreen) { + .pi-saas-aside { + top: var(--pi-spacing-x-large); + height: calc(100vh - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + width: var(--pi-width-aside); + border-radius: var(--pi-border-radius-large); + box-shadow: var(--pi-box-shadow-panel-double); + & > *:nth-child(2) { + flex-grow: 1; + height: 100%; + } + + & > .pi-close-button { + margin-left: calc(var(--pi-width-aside) - 1.125rem - var(--pi-spacing-large)); + } + } + // Aside is open + &.aside-is-open { + .pi-saas-aside { + transform: translateX( + calc(100vw - var(--pi-width-aside) - var(--pi-spacing-x-large)) + ); + } + } + // Aside is closed + &.aside-is-closed { + .pi-saas-aside { + opacity: 0; + transition-duration: 0.6s; + } + } + .pi-dynamic-menu { + & > *:nth-child(2) { + padding: var(--pi-spacing-x-large); + } + & > *:nth-child(3) { + height: calc(var(--pi-height-footer-aside) + env(safe-area-inset-bottom)); + margin: 0 auto; + padding: var(--pi-spacing-x-large) 0 0; + } + } + + // Back link is hidden + &.backlink-is-hidden { + .pi-dynamic-menu { + & > *:nth-child(1) { + padding: 0 var(--pi-spacing-x-large) var(--pi-spacing-x-large); + } + } + .pi-saas-aside { + & > .pi-icon { + display: none; + } + } + } + } + // Aside is closable + &.aside-is-closable { + .pi-dynamic-menu { + & > *:nth-child(1) { + padding: 0 var(--pi-spacing-x-large) var(--pi-spacing-x-large) + var(--pi-spacing-x-large); + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/shell/_pi-header.scss b/libs/ui/src/lib/styles/deale-saas/shell/_pi-header.scss new file mode 100644 index 0000000..5afe878 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/_pi-header.scss @@ -0,0 +1,369 @@ +.pi-shell__deale-saas { + & > div { + .pi-saas-header { + // Private + display: flex; + border-radius: var(--pi-border-radius-large); + + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--pi-spacing-xxxx-small); + } + // Right + &.pi-saas-header-content__right { + display: flex; + justify-content: flex-end; + align-items: center; + + & > *:nth-child(n + 1) { + display: flex; + align-items: center; + transition: var(--pi-animation); + &:hover { + cursor: pointer; + } + } + + .pi-icon-button { + &.is-active { + position: absolute; + transform: scale(0); + opacity: 0; + } + } + .pi-icon { + // Common + &:not(.is-stroked) { + &:hover { + img, + .mat-icon { + transition-duration: 0.15s; + transform: scale(1.15); + } + } + &:not(:hover):not(.is-active) { + img, + .mat-icon { + transition-duration: 0.15s; + transform: scale(1); + } + } + } + // Type stroke + &.is-stroked { + &:hover { + transition-duration: 0.15s; + transform: scale(1.1); + } + &:not(:hover):not(.is-active) { + transition-duration: 0.15s; + transform: scale(1); + } + } + // Active + &.is-active { + position: absolute; + transform: scale(0); + opacity: 0; + } + } + } + } + } + // Aside is open + &.aside-is-open { + .pi-saas-header-content { + // Right + &.pi-saas-header-content__right { + .pi-icon { + &.is-active { + background-color: var(--pi-color-primary-50); + border-radius: 50%; + } + } + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-saas-header { + left: var(--pi-spacing-medium); + width: calc(100% - var(--pi-spacing-medium) - var(--pi-spacing-medium)); + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + h2 { + max-width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + // Right + &.pi-saas-header-content__right { + gap: var(--pi-spacing-medium); + } + } + } + } + + // Mobile & Tablet + &:not(.is-desktop) { + // Small Devices + @media screen and (max-width: 62rem) { + .pi-saas-header { + position: fixed; + + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + & > span { + display: flex; + gap: var(--pi-spacing-xx-small); + & > h1 { + font: { + size: var(--pi-font-size-medium); + } + } + } + } + // Mobile + @media screen and (max-width: 40rem) { + height: var(--pi-height-header-mobile); + + // Right + &.pi-saas-header-content__right { + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not( + .pi-button__toggle + ):not(.pi-button__icon--single):not(.pi-button__xx-small):not( + .pi-button__trimmed + ):not(.pi-button__toggle) { + min-width: unset; + padding-left: var(--pi-spacing-medium); + padding-right: var(--pi-spacing-medium); + } + } + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__icon--single) { + min-width: var(--pi-spacing-xx-large); + } + &:not(.pi-close-button):not(.pi-hamburger).pi-button__medium { + padding: var(--pi-spacing-xxxx-small) unset; + } + &:not(.pi-close-button):not(.pi-hamburger).pi-button__appearance--outline { + background-color: var(--pi-color-accent-75); + transition: all 0.16s ease-in; + border: unset; + } + &:not(.pi-close-button):not(.pi-hamburger):not(.pi-button__x-large) { + border-radius: var(--pi-border-radius-medium); + -webkit-border-radius: var(--pi-border-radius-medium); + -moz-border-radius: var(--pi-border-radius-medium); + } + } + } + } + // Tablet S + @media screen and (min-width: 40rem) { + height: var(--pi-height-header-tablet); + // Right + &.pi-saas-header-content__right { + button { + &:not(.pi-close-button):not(.pi-hamburger).pi-button__medium { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-small); + } + } + } + } + // Left + &.pi-saas-header-content__left { + width: 60%; + & > span { + & > h1 { + font: { + size: var(--pi-font-size-medium); + } + } + } + } + + // Right + &.pi-saas-header-content__right { + width: 40%; + display: flex; + align-items: center; + + .pi-avatar { + padding: var(--pi-spacing-xxx-small); + + &:hover, + &.is-active { + transform: scale(1.05); + } + } + } + } + } + } + } + + // Tablet + &.is-tablet { + .pi-avatar { + picture { + width: 3rem; + height: 3rem; + } + } + &.is-tablet__small { + .pi-saas-header { + left: var(--pi-spacing-x-large); + width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + } + } + // Tablet L + &.is-tablet__large { + .pi-saas-header { + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + height: var(--pi-height-header-tablet); + padding-top: var(--pi-spacing-large); + } + // Right + &.pi-saas-header-content__right { + height: var(--pi-height-header-tablet); + padding-top: var(--pi-spacing-xx-small); + } + } + } + } + } + + // Tablet L & Desktop + &:not(.is-mobile) { + .pi-saas-header { + .pi-saas-header-content { + // Right + &.pi-saas-header-content__right { + gap: var(--pi-spacing-medium); + + .pi-icon-button, + .pi-icon:nth-of-type(1) { + &.is-active { + position: absolute; + transform: scale(0); + opacity: 0; + } + &:not(.is-active) { + &::after { + content: '|'; + color: var(--pi-color-primary-200); + margin-left: var(--pi-spacing-medium); + font: { + size: var(--pi-font-size-small); + } + } + } + } + } + } + } + + // Large Devices + @media screen and (min-width: 62rem) { + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + & span { + & > h1 { + font: { + size: var(--pi-font-size-large); + } + } + } + } + // Right + &.pi-saas-header-content__right { + button { + &:not(.pi-close-button):not(.pi-hamburger).pi-button__medium { + &.pi-button__appearance--ghost { + padding: var(--pi-spacing-xxxx-small) 0; + } + &:not(.pi-button__appearance--ghost) { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-large); + } + } + } + } + } + } + @media screen and (min-width: 62rem) { + .pi-shell__deale-saas { + & > div { + .pi-saas-header { + transition: var(--pi-animation); + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + width: 45%; + } + // Right + &.pi-saas-header-content__right { + justify-content: flex-end; + width: 55%; + .pi-avatar { + padding: 0.375rem; + + &:hover, + &.is-active { + transform: scale(1.075); + } + } + } + } + } + // Aside is open + &.aside-is-open { + .pi-saas-header { + width: calc(100% - var(--pi-width-aside) - var(--pi-spacing-x-large)); + } + } + // Aside is closed + &.aside-is-closed { + .pi-saas-header { + width: 100%; + } + } + } + } + } + } + // Desktop + &.is-desktop { + .pi-saas-header { + .pi-saas-header-content { + // Left + &.pi-saas-header-content__left { + height: var(--pi-height-header); + padding-top: var(--pi-spacing-small); + } + // Right + &.pi-saas-header-content__right { + height: var(--pi-height-header); + padding-top: var(--pi-spacing-medium); + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/shell/_pi-main.scss b/libs/ui/src/lib/styles/deale-saas/shell/_pi-main.scss new file mode 100644 index 0000000..0d00cd4 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/_pi-main.scss @@ -0,0 +1,155 @@ +.pi-shell__deale-saas { + & > div { + width: 100%; + + // NOT Is fullscreen + &:not(.is-fullscreen) { + & > section { + position: fixed; + } + } + // Is fullscreen + &.is-fullscreen { + & > section { + left: 0; + width: 100%; + } + .pi-saas-header, + .pi-saas-navbar { + display: none; + } + } + } +} + +html { + // Mobile & Tablet S + &:not(.is-desktop) { + // Tablet L + @media screen and (max-width: 62rem) { + .pi-shell__deale-saas { + & > div { + & > section { + width: 100%; + padding: 0; + } + // Footer logo + & > .pi-headline { + display: none; + } + } + } + } + } + + // Tablet + &.is-tablet { + // Tablet L + @media screen and (min-width: 62rem) { + .pi-shell__deale-saas { + & > div { + & > section { + height: calc(100vh - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + } + // Not is fullscreen + &:not(.is-fullscreen) { + & > section { + left: calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + ); + width: calc( + 100% - var(--pi-width-navbar-collapsed) - var(--pi-spacing-x-large) - + var(--pi-spacing-x-large) - var(--pi-spacing-xxx-small) + ); + } + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + .pi-shell__deale-saas { + & > div { + height: 100vh; + width: 100%; + overflow-x: hidden; + overflow-y: auto; + // Not is fullscreen + &:not(.is-fullscreen) { + // Layout container + & > section { + top: var(--pi-spacing-x-large); + & > *:nth-child(1) { + padding-right: var(--pi-spacing-x-large); + } + } + } + + // Aside is open + &.aside-is-open { + & > .pi-headline { + transform: var(--pi-animation); + transform: translateX(0); + } + } + // Aside is closed + &.aside-is-closed { + & > .pi-headline { + transform: var(--pi-animation); + transform: translateX(calc(var(--pi-width-aside) + var(--pi-spacing-x-large))); + } + } + } + } + } + } + // Desktop + &.is-desktop { + .pi-shell__deale-saas { + & > div { + // Not is fullscreen + &:not(.is-fullscreen) { + // Layout container + & > section { + height: calc(100vh - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + transition: var(--pi-animation); + & > *:nth-child(1) { + padding-right: var(--pi-spacing-x-large); + } + } + // Navbar is expanded + &.navbar-is-expanded { + & > section { + left: calc( + var(--pi-width-navbar-expanded) + var(--pi-spacing-xx-large) + + var(--pi-spacing-x-large) + ); + + width: calc( + 100% - var(--pi-width-navbar-expanded) - var(--pi-spacing-x-large) - + var(--pi-spacing-xx-large) - var(--pi-spacing-xxxx-small) + ); + } + } + // Navbar is collapsed + &.navbar-is-collapsed { + & > section { + left: calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-xx-large) + + var(--pi-spacing-x-large) + ); + width: calc( + 100% - var(--pi-width-navbar-collapsed) - var(--pi-spacing-x-large) - + var(--pi-spacing-xx-large) - var(--pi-spacing-xxxx-small) + ); + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/shell/_pi-menu.scss b/libs/ui/src/lib/styles/deale-saas/shell/_pi-menu.scss new file mode 100644 index 0000000..c5014bf --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/_pi-menu.scss @@ -0,0 +1,615 @@ +@mixin parentMenuEntryAppearance($justify-content, $height) { + .pi-saas-navbar { + .pi-menu-event-item { + display: flex; + align-items: center; + justify-content: $justify-content; + gap: var(--pi-spacing-medium); + width: -webkit-fill-available; + height: $height; + } + } +} + +@mixin parentMenuItemExpanded($height: var(--pi-size-menu-item-expanded)) { + .pi-shell__deale-saas { + & > div { + &.navbar-is-expanded { + // Navbar + .pi-saas-navbar { + .pi-menu { + & > nav { + & > .pi-menu-event-item { + .pi-menu-entry { + &.pi-menu-entry__context--parent-menu { + height: var(--pi-size-menu-item-expanded); + width: calc(100% - var(--pi-spacing-large) - var(--pi-spacing-large)); + margin-left: var(--pi-spacing-large); + padding: 0 var(--pi-spacing-large); + border-radius: var(--pi-border-radius-medium); + transition: var(--pi-animation); + article { + transition: var(--pi-animation); + transform: scale(1); + transition-delay: 0.1s; + visibility: visible; + opacity: 1; + } + label { + line-height: 1.25; + } + } + } + } + } + } + } + } + } + } +} + +@mixin parentMenuItemCollapsed($size: var(--pi-spacing-xxxx-large), $radius: medium, $scale: 1) { + .pi-saas-navbar { + .pi-menu { + .pi-menu-entry { + &.pi-menu-entry__context--parent-menu { + justify-content: center; + height: $size; + width: $size; + border-radius: var(--pi-border-radius-#{$radius}); + transition: var(--pi-animation); + + article { + position: absolute; + transform: scale(0.5); + visibility: hidden; + opacity: 0; + } + } + // Not disabled hover and active effects + &:not(.is-disabled) { + &.pi-menu-entry__context--parent-menu { + &:hover { + &:not(.is-active) { + .mat-icon { + transform: scale(#{$scale}); + } + } + } + &.is-active { + background-color: var(--pi-color-primary-50); + color: var(--pi-color-primary-900); + .mat-icon { + transform: scale(#{$scale}); + } + } + } + } + } + } + } +} + +@mixin parentMenuEntryColor { + .pi-menu-event-item { + &.is-selected { + .pi-menu-entry { + &:not(.is-disabled) { + &:not([class*='pi-menu-entry__context--aside-menu']):not([class*='children']) { + background-color: var(--pi-color-primary-25); + color: var(--pi-color-primary); + .mat-icon { + color: var(--pi-color-primary); + } + } + } + } + } + &.is-disabled { + opacity: 0.3; + } + &:not(.is-disabled) { + .pi-menu-entry { + &.pi-menu-entry__context--parent-menu, + &.pi-menu-entry__context--children-submenu { + &.pi-text-viewer { + &.pi-text-viewer__icon-color--black { + &:not(.pi-menu-entry__context--children-menu):not( + .pi-menu-entry__context--aside-menu + ) { + &:not(.is-active) { + color: var(--pi-color-grey); + .mat-icon { + color: var(--pi-color-grey); + } + } + &:hover { + &:not(.is-active):not(.is-disabled) { + background-color: var(--pi-color-primary-50); + + span, + label, + .mat-icon { + color: var(--pi-color-primary); + } + } + } + &:not(.is-disabled) { + &.is-active { + max-width: -webkit-fill-available; + background-color: var(--pi-color-primary-50); + color: var(--pi-color-primary-900); + .mat-icon { + color: var(--pi-color-primary-900); + } + } + } + + &:hover { + &:not(.is-active):not(.is-disabled) { + background-color: var(--pi-color-primary-50); + label, + .mat-icon { + color: var(--pi-color-primary); + } + } + } + } + } + } + &.is-disabled { + opacity: 0.3; + } + } + } + } + } +} + +@mixin childrenMenuEntryAppearance { + .pi-menu-entry { + &.pi-menu-entry__context--children-menu { + max-width: -webkit-fill-available; + padding-left: var(--pi-spacing-xx-small); + font: { + size: var(--pi-font-size-x-small); + } + } + &.pi-menu-entry__context--children-submenu { + height: var(--pi-size-menu-item-expanded); + padding: var(--pi-spacing-medium); + border-radius: var(--pi-border-radius-medium); + transition: var(--pi-animation); + article { + transition: var(--pi-animation); + transform: scale(1); + transition-delay: 0.1s; + visibility: visible; + opacity: 1; + } + label { + line-height: 1.25; + } + } + } +} + +@mixin childrenMenuCollapsed { + position: fixed; + display: none; + visibility: hidden; + opacity: 0; + transition: var(--pi-animation-slow); +} + +@mixin childrenMenuPosition($left) { + // Menu item + transform: translate($left, var(--pi-spacing-xxx-large)); +} + +@mixin childrenMenuAppearance($width: -webkit-fill-available) { + .pi-saas-navbar { + .pi-menu__children { + position: absolute; + flex-direction: column; + gap: var(--pi-spacing-x-small); + width: $width; + padding-top: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-xxxx-small); + padding-bottom: var(--pi-spacing-x-large); + border-radius: var(--pi-border-radius-large); + background-color: var(--pi-color-white); + border: 0.5px solid var(--pi-color-grey-50); + box-shadow: var(--pi-box-shadow-double); + + // Submenu entries + & > *:nth-child(n + 1) { + & > *:nth-child(n + 1) { + &:not(.pi-menu-entry__context--children-menu) { + padding-left: var(--pi-spacing-x-large); + padding-right: var(--pi-spacing-x-large); + } + } + } + + .pi-menu-event-children { + &.pi-menu-children__entries { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-xxx-small); + + & > section { + max-height: 25vh; + + overflow-y: auto; + &::-webkit-scrollbar { + width: 0.1875rem; + } + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-grey-50); + + &:hover { + background-color: var(--pi-color-grey-75); + } + } + } + } + } + .pi-submenu-children__aside-entries { + margin-top: var(--pi-spacing-x-small); + } + .pi-menu-entry { + width: -webkit-fill-available; + display: flex; + align-items: center; + justify-content: space-between; + transition: var(--pi-animation); + &:hover { + cursor: pointer; + } + &.pi-menu-entry__context--aside-menu { + button { + width: -webkit-fill-available; + } + } + &.pi-menu-entry__context--children-menu { + padding-bottom: var(--pi-spacing-xxx-small); + border-bottom: 1px solid; + margin-left: calc(var(--pi-spacing-x-large) + var(--pi-spacing-xxxx-small)); + margin-right: calc(var(--pi-spacing-x-large) + var(--pi-spacing-xxxx-small)); + + &:nth-of-type(1) { + border-color: var(--pi-color-grey-100); + } + &:nth-of-type(2) { + border-color: var(--pi-color-primary-75); + } + &:nth-of-type(3) { + border-color: var(--pi-color-accent-75); + } + &:nth-of-type(n + 2) { + margin-top: var(--pi-spacing-x-large); + } + } + } + } + } +} + +@mixin childrenMenuVisibility { + & > .pi-menu-event-item { + &.has-children { + &.is-selected { + // Child menu visible + .pi-menu__children { + &.is-visible { + opacity: 1; + visibility: visible; + } + + &:not(.is-visible) { + display: none; + } + } + } + &:not(.is-selected) { + // Child menu hidden + .pi-menu__children { + &:not(.is-visible) { + display: none; + opacity: 0; + visibility: hidden; + } + } + } + } + } +} + +@mixin smallDeviceChildrenMenuPosition { + .pi-saas-navbar { + .pi-menu { + &.pi-menu__children { + left: 0; + bottom: calc( + env(safe-area-inset-bottom) + var(--pi-spacing-medium) + var(--pi-height-footer) + ); + z-index: 99; + } + & > nav { + @include childrenMenuVisibility; + } + } + } +} + +@mixin largeDeviceChildrenMenuPosition { + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + .pi-menu { + &.pi-menu__parent { + width: -webkit-fill-available; + } + & > nav { + display: flex; + flex-direction: column; + + // Hide - show child menu + @include childrenMenuVisibility; + } + } + } + } + } +} + +// Menu in navbar +.pi-saas-navbar { + @include parentMenuEntryColor; + @include childrenMenuAppearance; + @include childrenMenuEntryAppearance; +} + +html { + &.is-mobile { + @include smallDeviceChildrenMenuPosition; + .pi-shell__deale-saas { + & > div { + @include parentMenuItemCollapsed; + } + } + } + + &.is-tablet { + &.is-tablet__small { + @include smallDeviceChildrenMenuPosition; + .pi-shell__deale-saas { + & > div { + @include parentMenuItemCollapsed(3.375rem, medium, 1.1); + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small Device + @media screen and (max-width: 62rem) { + @include childrenMenuAppearance; + .pi-shell__deale-saas { + & > div { + @include parentMenuEntryAppearance(center, 3.25rem); + .pi-saas-navbar { + .pi-menu-entry { + &.pi-menu-entry__context--parent-menu { + gap: 0; + transition: var(--pi-animation-slow); + + &:not(.is-active) { + .mat-icon { + color: var(--pi-color-black); + } + } + article { + display: none; + } + } + } + + .pi-menu { + & > nav { + display: flex; + justify-content: space-between; + align-items: center; + height: var(--pi-height-footer); + padding-left: var(--pi-spacing-xx-small); + padding-right: var(--pi-spacing-xx-small); + } + } + .pi-chevron-button { + display: none; + } + } + } + } + // Menu in layout + } + } + // Tablet + &.is-tablet { + &.is-tablet__large { + .pi-shell__deale-saas { + & > div { + @include parentMenuEntryAppearance(center, var(--pi-size-menu-item-collapsed)); + @include parentMenuItemCollapsed(var(--pi-size-menu-item-collapsed), large, 1.2); + .pi-saas-navbar { + // Tablet L + .pi-menu { + & > nav { + gap: var(--pi-spacing-xxx-small); + & > .pi-menu-event-item { + .pi-menu-entry { + & > span { + @include childrenMenuCollapsed; + } + } + // Child menu + &.has-children { + .pi-menu__children { + @include childrenMenuPosition( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-x-large) + + var(--pi-spacing-x-large) + var(--pi-spacing-xxx-small) + ) + ); + } + // Is selected and children is visible + &.is-selected { + &:not(.is-disabled) { + .pi-menu-entry { + &:not(.is-active) { + .mat-icon { + color: var(--pi-color-primary); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + @include childrenMenuAppearance(var(--pi-width-children-menu-expanded)); + @include largeDeviceChildrenMenuPosition; + // Menu in layout + .pi-layout { + .pi-menu { + &.pi-menu__parent { + & > nav { + width: calc( + var(--pi-width-navbar-expanded) - var(--pi-spacing-xxxx-large) - + var(--pi-spacing-xxxx-small) + ); + } + .pi-menu-event-item { + & > *:nth-child(1) { + width: 100%; + } + &.has-children { + padding-left: var(--pi-spacing-large); + padding-right: var(--pi-spacing-large); + .pi-menu__children { + transform: translate(-1.25rem, -1rem); + } + } + } + } + } + } + } + } + + // Desktop + &.is-desktop { + @include parentMenuItemExpanded; + .pi-shell__deale-saas { + & > div { + // Cursor styles + .pi-menu-event-item { + &:not(.is-active) { + &:hover { + .pi-menu-entry { + &:not(.is-disabled) { + cursor: pointer; + label { + &:hover { + &:not(.is-disabled) { + cursor: pointer; + } + } + } + } + } + } + } + + .pi-menu-entry { + &.pi-menu-entry__context--children-submenu { + &:hover { + &:not(.is-active):not(.is-disabled) { + cursor: pointer; + } + } + article, + span, + label { + &:hover { + cursor: pointer; + } + } + } + } + } + // Expanded + &.navbar-is-expanded { + @include parentMenuEntryAppearance(flex-start, var(--pi-size-menu-item-expanded)); + // Navbar + .pi-saas-navbar { + .pi-menu { + & > nav { + gap: var(--pi-spacing-x-small); + & > .pi-menu-event-item { + // Child menu + &.has-children { + .pi-menu__children { + @include childrenMenuPosition( + calc(var(--pi-width-navbar-expanded) + var(--pi-spacing-xx-large)) + ); + } + } + } + } + } + } + } + // Collapsed + &.navbar-is-collapsed { + @include parentMenuEntryAppearance(center, var(--pi-size-menu-item-collapsed)); + @include parentMenuItemCollapsed(var(--pi-size-menu-item-collapsed), large, 1.2); + .pi-saas-navbar { + .pi-menu { + & > nav { + gap: var(--pi-spacing-xxx-small); + & > .pi-menu-event-item { + // Child menu + &.has-children { + .pi-menu__children { + @include childrenMenuPosition( + calc( + var(--pi-width-navbar-collapsed) + var(--pi-spacing-xx-large) + + var(--pi-spacing-xx-large) + var(--pi-spacing-large) + + var(--pi-spacing-x-large) + ) + ); + } + } + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/shell/_pi-navbar.scss b/libs/ui/src/lib/styles/deale-saas/shell/_pi-navbar.scss new file mode 100644 index 0000000..e41716f --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/_pi-navbar.scss @@ -0,0 +1,280 @@ +.pi-saas-navbar { + position: fixed; + z-index: 99; + background-color: var(--pi-color-white); + border: 1px solid var(--pi-color-white-900); + border-radius: var(--pi-border-radius-large); + .pi-headline { + margin: auto; + } + .pi-saas-navbar-content { + &.pi-saas-navbar-content__top { + .pi-headline { + width: calc(100% - var(--pi-spacing-x-large)); + padding: var(--pi-spacing-xx-large) 0 0 var(--pi-spacing-x-large); + } + .pi-chevron-button { + width: var(--pi-spacing-x-large); + margin-bottom: 1.125rem; + padding: 0.1375rem; + } + } + } + .pi-chevron-button { + position: relative; + left: var(--pi-spacing-small); + border-radius: 50%; + box-shadow: var(--pi-box-shadow); + } +} + +@mixin multispaceNavbarGridLargeDevice { + .pi-saas-navbar-content { + &.pi-saas-navbar-content__center { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--pi-spacing-xxx-small); + + .pi-divider { + width: -webkit-fill-available; + max-width: calc(100% - var(--pi-spacing-xxxx-large)); + } + } + } +} + +@mixin multispaceNavbarGridSmallDevice { + // Advisor Profile Multispace + .is-advisor { + .pi-saas-navbar-content { + &.pi-saas-navbar-content__center { + display: flex; + flex-direction: row-reverse; + align-items: center; + .pi-menu { + &.pi-menu__parent { + &:first-of-type { + width: 20%; + & > nav { + padding-left: unset; + .pi-menu-event-item { + &.has-children { + border-left: 1px solid var(--pi-color-grey-75); + } + } + } + } + &:last-of-type { + width: 80%; + & > nav { + padding-right: unset; + } + } + } + } + } + } + } +} + +html { + // Mobile & Tablet + &.is-mobile { + @include multispaceNavbarGridSmallDevice; + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + left: var(--pi-spacing-medium); + bottom: var(--pi-spacing-medium); + width: calc(100% - var(--pi-spacing-medium) - var(--pi-spacing-medium)); + } + } + } + } + + &:not(.is-desktop) { + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + // Mobile & Tablet S + @media screen and (max-width: 62rem) { + position: fixed; + height: var(--pi-height-footer); + box-shadow: var(--pi-box-shadow-panel); + .pi-saas-navbar-content { + &.pi-saas-navbar-content__top, + &.pi-saas-navbar-content__bottom { + display: none; + } + } + } + } + } + } + } + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include multispaceNavbarGridSmallDevice; + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + left: var(--pi-spacing-x-large); + bottom: var(--pi-spacing-x-large); + width: calc(100% - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + } + } + } + } + // Tablet L + &.is-tablet__large { + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + width: var(--pi-width-navbar-collapsed); + box-shadow: var(--pi-box-shadow-panel); + .pi-headline { + height: 5.25rem; + max-width: 3.35rem; + padding: 1.6rem var(--pi-spacing-small); + } + .pi-chevron-button { + display: none; + } + .pi-saas-navbar-content { + &.pi-saas-navbar-content__top { + & > .pi-chip { + transform: scale(0.8); + img { + transform: scale(0.8); + } + } + } + + &.pi-saas-navbar-content__bottom { + padding: var(--pi-spacing-x-large) 0; + margin: 0 auto; + } + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Tablet L & Desktop + @media screen and (min-width: 62rem) { + @include multispaceNavbarGridLargeDevice; + .pi-shell__deale-saas { + & > div { + &:not(.is-fullscreen) { + .pi-saas-navbar { + top: var(--pi-spacing-x-large); + left: var(--pi-spacing-x-large); + display: flex; + flex-direction: column; + justify-content: space-between; + height: calc(100vh - var(--pi-spacing-x-large) - var(--pi-spacing-x-large)); + .pi-chip { + margin: auto; + } + } + } + } + } + } + + // Desktop + &.is-desktop { + .pi-shell__deale-saas { + & > div { + .pi-saas-navbar { + width: var(--pi-width-navbar-expanded); + box-shadow: var(--pi-box-shadow-panel); + transition: var(--pi-animation); + .pi-saas-navbar-content { + &.pi-saas-navbar-content__top { + & > header { + display: flex; + align-items: flex-end; + } + & > .pi-chip { + margin: auto; + transform: scale(0.9); + img { + transform: scale(0.9); + } + } + } + &.pi-saas-navbar-content__bottom { + padding: var(--pi-spacing-x-large) 0; + text-align: center; + } + } + + .pi-headline { + transition: var(--pi-animation); + } + } + // Expanded + &.navbar-is-expanded { + .pi-saas-navbar { + width: var(--pi-width-navbar-expanded); + + .pi-headline { + max-width: 8rem; + height: 5rem; + opacity: 0; + &.pi-logo { + transition: var(--pi-animation); + transition-duration: 0.35s; + transition-delay: 0.15s; + opacity: 1; + } + & > .pi-chip { + transform: scale(0.9); + } + } + .pi-saas-navbar-content { + &.pi-saas-navbar-content__center { + padding: var(--pi-spacing-x-large) 0; + } + } + } + } + // Collapsed + &.navbar-is-collapsed { + .pi-saas-navbar { + width: var(--pi-width-navbar-collapsed); + .pi-headline { + max-width: 3.35rem; + height: 5.25rem; + padding: 1.25rem 0 0 var(--pi-spacing-x-large); + opacity: 0; + transition: var(--pi-animation); + + &.pi-logo-simple { + transition: var(--pi-animation); + transition-duration: 0.35s; + transition-delay: 0.15s; + opacity: 1; + } + } + .pi-saas-navbar-content { + &.pi-saas-navbar-content__top { + & > .pi-chip { + transform: scale(0.8); + } + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/shell/index.scss b/libs/ui/src/lib/styles/deale-saas/shell/index.scss new file mode 100644 index 0000000..dea116e --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/shell/index.scss @@ -0,0 +1,5 @@ +@forward 'pi-aside'; +@forward 'pi-header'; +@forward 'pi-main'; +@forward 'pi-menu'; +@forward 'pi-navbar'; diff --git a/libs/ui/src/lib/styles/deale-saas/theming/_pi-user-type-theming.scss b/libs/ui/src/lib/styles/deale-saas/theming/_pi-user-type-theming.scss new file mode 100644 index 0000000..e25c02b --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/theming/_pi-user-type-theming.scss @@ -0,0 +1,293 @@ +@mixin asideColor($color) { + .pi-shell__deale-saas { + & > div { + .pi-saas-aside { + // Aside theme + &.pi-saas-aside__theme { + // Not custom + &:not(.is-custom) { + background-color: var(--pi-color-#{$color}-50); + .pi-close-button { + &:hover { + &::before, + &::after { + background-color: var(--pi-color-#{$color}); + } + } + } + // Dynamic menu + .pi-dynamic-menu { + & > *:nth-child(2) { + &.pi-wrapper__flex { + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-#{$color}-100); + + &:hover { + background-color: var(--pi-color-#{$color}-200); + } + } + } + } + & > *:nth-child(3) { + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--pi-color-#{$color}-100); + } + .pi-list-item { + transition: var(--pi-animation); + &.is-clickable { + &:hover { + &.pi-color__primary { + &:not(.is-disabled):not(.is-active) { + background-color: var(--pi-color-#{$color}-75); + } + } + } + } + &.pi-color__primary { + &.is-active { + background-color: var(--pi-color-#{$color}-75); + } + } + .pi-chip { + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-#{$color}); + color: var(--pi-color-#{$color}); + + label { + color: var(--pi-color-#{$color}); + } + } + } + } + } + } + // User profile + &.pi-saas-admin-user-profile { + .pi-chip { + // Profile + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--fill { + &:not(.pi-chip__single-icon) { + background-color: var(--pi-color-#{$color}); + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-#{$color}); + } + } + } + } + // Subscription + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-#{$color}); + color: var(--pi-color-#{$color}); + } + } + } + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + label { + color: var(--pi-color-#{$color}); + } + } + } + } + } + .pi-icon { + &.is-stroked { + &.pi-icon__appearance--fill { + &.pi-icon__x-large { + border-color: var(--pi-color-#{$color}-200); + background-color: var(--pi-color-#{$color}-75); + .mat-icon { + color: var(--pi-color-#{$color}); + } + } + } + } + } + } + } + } + // Custom + &.is-custom { + background-color: var(--pi-color-primary-50); + .pi-close-button { + &:hover { + &::before, + &::after { + background-color: var(--pi-color-primary); + } + } + } + // Dynamic menu + .pi-dynamic-menu { + & > *:nth-child(2) { + &.pi-wrapper__flex { + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-primary-100); + + &:hover { + background-color: var(--pi-color-primary-200); + } + } + } + } + & > *:nth-child(3) { + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--pi-color-primary-100); + } + .pi-list-item { + transition: var(--pi-animation); + &.is-clickable { + &:hover { + &.pi-color__primary { + &:not(.is-disabled):not(.is-active) { + background-color: var(--pi-color-primary-100); + } + } + } + } + &.pi-color__primary { + &.is-active { + background-color: var(--pi-color-primary-100); + } + } + .pi-chip { + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-primary); + color: var(--pi-color-primary); + + label { + color: var(--pi-color-primary); + } + } + } + } + } + } + // User profile + &.pi-saas-admin-user-profile { + .pi-chip { + // Profile + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--fill { + &:not(.pi-chip__single-icon) { + background-color: var(--pi-color-primary); + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-primary); + } + } + } + } + // Subscription + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + border-width: 1px; + border-style: solid; + border-color: var(--pi-color-primary); + color: var(--pi-color-primary); + } + } + } + &:not(.pi-status) { + &.pi-color__primary { + &.pi-chip__appearance--outline { + label { + color: var(--pi-color-primary); + } + } + } + } + } + .pi-icon { + &.is-stroked { + &.pi-icon__appearance--fill { + &.pi-icon__x-large { + border-color: var(--pi-color-primary-200); + background-color: var(--pi-color-primary-200-rgba); + .mat-icon { + color: var(--pi-color-primary); + } + } + } + } + } + } + } + } + } + // Aside white + &.pi-saas-aside__white { + // Dynamic menu + .pi-dynamic-menu { + & > *:nth-child(2) { + &.pi-wrapper__flex { + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-grey-25); + + &:hover { + background-color: var(--pi-color-grey-75); + } + } + } + } + & > *:nth-child(3) { + border-top-width: 1px; + border-top-style: solid; + border-top-color: var(--pi-color-white-900); + } + } + } + } + + &:not(.is-fullscreen) { + // Large device + @media screen and (min-width: 62rem) { + .pi-saas-aside { + &.pi-saas-aside__theme { + border-width: 1px; + border-style: solid; + &:not(.is-custom) { + border-color: var(--pi-color-#{$color}-100); + } + &.is-custom { + border-color: var(--pi-color-primary-100); + } + } + } + } + } + } + } +} + +.is-company { + @include asideColor(primary); +} + +.is-investor { + @include asideColor(accent); +} + +.is-advisor { + @include asideColor(highlighted); +} diff --git a/libs/ui/src/lib/styles/deale-saas/theming/index.scss b/libs/ui/src/lib/styles/deale-saas/theming/index.scss new file mode 100644 index 0000000..80197b6 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/theming/index.scss @@ -0,0 +1 @@ +@forward 'pi-user-type-theming'; diff --git a/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-header-wrapper.scss b/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-header-wrapper.scss new file mode 100644 index 0000000..55ea273 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-header-wrapper.scss @@ -0,0 +1,47 @@ +.pi-wrapper { + // Header Wrapper + &.pi-wrapper__header { + // Center + &.pi-wrapper__align-items--center { + .pi-header-wrapper-content { + align-items: center; + } + } + } +} + +.pi-layout__07-list { + .pi-header-wrapper-content { + &.pi-header-wrapper-content__left { + width: 100%; + } + &.pi-header-wrapper-content__right { + justify-content: flex-end; + display: flex; + & > aside { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--pi-spacing-xx-small); + + .pi-select { + &.pi-select__picked-option-type--button { + .pi-select-picked-option { + background-color: var(--pi-color-white); + } + &:not(:hover) { + .pi-select-picked-option { + box-shadow: var(--pi-box-shadow); + } + } + } + } + .pi-toggle-button { + & > button { + border-radius: var(--pi-border-radius-large); + } + } + } + } + } +} \ No newline at end of file diff --git a/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-wrapper.scss b/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-wrapper.scss new file mode 100644 index 0000000..d5ba1ca --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/wrappers/_pi-wrapper.scss @@ -0,0 +1,42 @@ +@mixin dealeSaaSWrapperMaxWidth($max-width: 75%) { + .pi-shell__deale-saas { + .pi-wrapper { + // Max width + &.pi-wrapper__full-width, + &.pi-wrapper__full-width-centered { + max-width: $max-width; + } + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-wrapper { + div { + // Fixed wrapper + &.is-fixed { + position: fixed; + z-index: 99; + left: 0; + bottom: calc(var(--pi-height-footer) + env(safe-area-inset-bottom)); + display: flex; + justify-content: flex-end; + align-items: center; + width: 100%; + padding: var(--pi-spacing-xx-small) var(--pi-spacing-x-large); + right: 0; + button { + min-width: 50%; + box-shadow: var(--pi-box-shadow); + } + } + } + } + } + // Desktop + &.is-desktop { + @include dealeSaaSWrapperMaxWidth; + } +} diff --git a/libs/ui/src/lib/styles/deale-saas/wrappers/index.scss b/libs/ui/src/lib/styles/deale-saas/wrappers/index.scss new file mode 100644 index 0000000..cf35872 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-saas/wrappers/index.scss @@ -0,0 +1,2 @@ +@forward 'pi-header-wrapper'; +@forward 'pi-wrapper'; diff --git a/libs/ui/src/lib/styles/deale-website/components/_pi-text.scss b/libs/ui/src/lib/styles/deale-website/components/_pi-text.scss new file mode 100644 index 0000000..31fed15 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/components/_pi-text.scss @@ -0,0 +1,39 @@ +h1 { + line-height: 110%; +} + +html { + // Mobile + &.is-mobile { + .pi-website-text { + // xxxxl - 2.875rem - 46px + &.pi-website-text__size--xxxxl { + font: { + size: 2.875rem; + } + } + } + } + // Tablet + &.is-tablet { + .pi-website-text { + // xxxxl - 62px + &.pi-website-text__size--xxxxl { + font: { + size: 3.875rem; + } + } + } + } + // Desktop + &.is-desktop { + .pi-website-text { + // xxxxl - 4.5rem - 72px + &.pi-website-text__size--xxxxl { + font: { + size: var(--pi-public-font-size-xxx-large); + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/components/index.scss b/libs/ui/src/lib/styles/deale-website/components/index.scss new file mode 100644 index 0000000..6bbdfe2 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/components/index.scss @@ -0,0 +1 @@ +@forward 'pi-text'; diff --git a/libs/ui/src/lib/styles/deale-website/index.scss b/libs/ui/src/lib/styles/deale-website/index.scss new file mode 100644 index 0000000..0055ca7 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/index.scss @@ -0,0 +1,5 @@ +@use 'components' as *; +@use 'layouts' as *; +@use 'screens' as *; +@use 'shell' as *; +@use 'wrappers' as *; diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-02-full-width-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-02-full-width-layout.scss new file mode 100644 index 0000000..be56546 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-02-full-width-layout.scss @@ -0,0 +1,97 @@ +.pi-layout { + &.pi-layout__02-full-width { + &:not(.pi-layout__02a-full-width--auto) { + & > *:nth-child(1) { + & > *:nth-child(n + 1):not(:is(h1, h2, h3, h4, h5, p, i, figcaption, small)) { + margin: 0 auto; + } + & > *:nth-child(n + 1):is(h1, h2, h3, h4, h5, p, i, figcaption, small) { + margin: var(--pi-spacing-medium) auto; + } + } + } + } +} +@mixin layoutBasicPages( + $padding-top-1: calc(var(--pi-public-height-header-mobile) + var(--pi-spacing-xxx-large)), + $padding-top-2: var(--pi-spacing-xxx-large) +) { + .pi-shell__deale-website { + .pi-container-layout { + &.pi-layout { + &.pi-layout__02-full-width { + & > *:nth-child(1) { + padding-top: $padding-top-1; + } + & > *:nth-child(2) { + padding-top: $padding-top-2; + padding-bottom: $padding-top-2; + } + } + } + } + } +} + +html { + &.is-mobile { + @include layoutBasicPages; + .pi-layout { + &.pi-layout__02-full-width { + padding: var(--pi-spacing-x-large); + } + } + } + &.is-tablet { + @include layoutBasicPages( + calc(var(--pi-public-height-header-mobile) + var(--pi-spacing-xxxx-large)), + var(--pi-spacing-xxxx-large) + ); + .pi-layout { + &.pi-layout__02-full-width { + padding: var(--pi-spacing-xx-large); + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__02-full-width { + max-width: 100%; + } + } + } + // Desktop + &.is-desktop { + @include layoutBasicPages( + calc(var(--pi-public-height-header-desktop) + var(--pi-spacing-xxxx-large)), + var(--pi-spacing-xxxx-large) + ); + .pi-layout { + &.pi-layout__02-full-width { + padding: var(--pi-spacing-xxxx-large) var(--pi-spacing-xxxx-large); + + // Desktop M + @media screen and (min-width: 90rem) and (max-width: 120rem) { + & > *:nth-child(n + 1) { + max-width: var(--pi-public-width-container); + } + } + &:not(.pi-container-layout) { + // Desktop S + @media screen and (min-width: 77.5rem) and (max-width: 90rem) { + & > *:nth-child(n + 1) { + max-width: 100%; + } + } + // Desktop L + @media screen and (min-width: 120rem) { + & > *:nth-child(n + 1) { + max-width: var(--pi-public-width-container-l); + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-02a-full-width-auto-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-02a-full-width-auto-layout.scss new file mode 100644 index 0000000..dafcfbb --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-02a-full-width-auto-layout.scss @@ -0,0 +1,9 @@ +.pi-layout { + &.pi-layout__02-full-width { + &.pi-layout__02a-full-width--auto { + & > *:nth-child(n + 1) { + margin: 0 auto; + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-03-vertically-centered-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-03-vertically-centered-layout.scss new file mode 100644 index 0000000..3488df9 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-03-vertically-centered-layout.scss @@ -0,0 +1,117 @@ +@mixin headingsWidth($max-width: 100%) { + .pi-container-layout { + .pi-layout { + &.pi-layout__03-vertically-centered { + .pi-text__centered { + & > *:nth-child(n + 1) { + &:is(h2, h3, h4, h5) { + max-width: $max-width; + margin: auto; + } + } + } + } + } + } +} + +// + +@mixin thankYouPageHeaderHeight($device: mobile) { + .pi-layout { + &.pi-layout__03-vertically-centered { + &.pi-layout__full { + & > .pi-wrapper { + &.pi-background-image { + min-height: calc(100vh - var(--pi-public-height-header-#{$device})); + } + } + } + } + } +} + +.pi-container-layout { + .pi-layout { + &.pi-layout__03-vertically-centered { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background-size: cover; + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + @include headingsWidth; + // Small device + @media screen and (max-width: 62rem) { + @include thankYouPageHeaderHeight; + } + &:not(.is-desktop__large) { + .pi-layout { + &.pi-layout__03-vertically-centered { + &.pi-layout__02-full-width { + &:not(.pi-layout__06-edge-to-edge) { + & > *:nth-child(1) { + max-width: 100%; + } + } + } + } + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + @include thankYouPageHeaderHeight(desktop); + } + } + // Desktop + &.is-desktop { + .pi-layout { + @include headingsWidth(75%); + &.pi-layout__03-vertically-centered { + &.pi-layout__02-full-width { + &:not(.pi-layout__06-edge-to-edge) { + max-width: var(--pi-public-width-container-l); + } + + &.pi-layout__02a-full-width--auto { + margin: auto; + } + } + } + } + &:not(.is-desktop__large) { + .pi-layout { + &.pi-layout__03-vertically-centered { + & > *:nth-child(1) { + max-width: 100%; + } + } + } + } + &.is-desktop__medium { + @include headingsWidth(70%); + } + &.is-desktop__large { + @include headingsWidth(66.66%); + .pi-layout { + &.pi-layout__03-vertically-centered { + &:not(.pi-layout__06-edge-to-edge) { + & > *:nth-child(1) { + max-width: var(--pi-public-width-container-l); + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-06-edge-to-edge-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-06-edge-to-edge-layout.scss new file mode 100644 index 0000000..5f5a9ac --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-06-edge-to-edge-layout.scss @@ -0,0 +1,9 @@ +.pi-layout { + &.pi-layout__06-edge-to-edge { + width: 100%; + & > *:nth-child(n + 1) { + width: 100%; + margin: 0 auto; + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-06b-edge-to-edge-home-users-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-06b-edge-to-edge-home-users-layout.scss new file mode 100644 index 0000000..6c802ea --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-06b-edge-to-edge-home-users-layout.scss @@ -0,0 +1,57 @@ +@mixin websiteHomeUserSpacings($max-width) { + .pi-layout { + &.pi-website-home-user { + .pi-list-item-content__left { + max-width: $max-width; + } + } + } +} +@mixin edgeToEdgeLayoutCarouselBehavior($max-width) { + .pi-layout { + &.pi-layout__06-edge-to-edge { + .pi-carousel { + & > main { + & > section { + max-width: $max-width; + } + } + } + } + } +} + +.pi-layout { + &.pi-website-home-user { + .pi-carousel { + display: block; + } + .pi-switch-button { + margin-top: var(--pi-spacing-xxx-large); + } + &:not(.pi-website-home-user__advisor) { + .pi-carousel { + margin-top: var(--pi-spacing-xxx-large); + } + } + } +} + +html { + // Desktop + &.is-desktop { + // Desktop S & M + &:not(.is-desktop__large) { + @include edgeToEdgeLayoutCarouselBehavior(var(--pi-public-width-container)); + } + // Desktop M + &.is-desktop__medium { + @include websiteHomeUserSpacings(80%); + } + // Desktop L + &.is-desktop__large { + @include edgeToEdgeLayoutCarouselBehavior(var(--pi-public-width-container-l)); + @include websiteHomeUserSpacings(66.66%); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-07-list-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-07-list-layout.scss new file mode 100644 index 0000000..6f62ccf --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-07-list-layout.scss @@ -0,0 +1,164 @@ +.pi-layout { + &.pi-layout__07-list { + padding: 0; + overflow-y: hidden; + & > header { + position: fixed; + z-index: 99; + display: flex; + align-items: flex-end; + width: 100%; + margin: auto; + background-color: rgba($color: white, $alpha: 0.6); + .pi-wrapper { + &.pi-wrapper__header { + margin: auto; + padding-bottom: var(--pi-spacing-xx-small); + } + } + } + & > section { + footer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--pi-spacing-medium); + height: 7rem; + text-align: center; + background-color: var(--pi-color-white); + } + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + .pi-layout { + &.pi-layout__07-list { + & > header { + .pi-wrapper { + &.pi-wrapper__header { + min-height: var(--pi-height-layout-header); + width: 100%; + padding-right: var(--pi-spacing-x-large); + padding-left: var(--pi-spacing-x-large); + header { + display: flex; + align-items: center; + & > h1 { + margin-top: var(--pi-spacing-medium); + font: { + size: var(--pi-font-size-large); + } + } + } + } + } + } + & > section { + padding-top: calc(var(--pi-height-layout-header) - var(--pi-spacing-medium)); + + .pi-wrapper { + &.pi-wrapper__list { + max-height: calc( + 100vh - var(--pi-public-height-header-mobile) - var(--pi-height-layout-header) - + 7.35rem + ); + .pi-data-grid__mode--grid { + justify-content: center; + } + } + } + } + } + } + } + + // Desktop + &.is-desktop { + .pi-layout { + &.pi-layout__07-list { + & > header { + .pi-wrapper { + &.pi-wrapper__header { + min-height: var(--pi-height-layout-header); + padding: var(--pi-spacing-xxx-large) var(--pi-spacing-x-large); + } + } + } + & > section { + padding-top: calc(var(--pi-height-layout-header) - var(--pi-spacing-medium)); + + .pi-wrapper { + &.pi-wrapper__list { + max-height: calc( + 100vh - var(--pi-public-height-header-desktop) - var(--pi-height-layout-header) - + 7.35rem + ); + & > section { + .pi-data-grid__mode--grid { + justify-content: center; + max-width: var(--pi-width-container-l); + margin: auto; + padding-top: 3rem; + padding-bottom: var(--pi-spacing-xxxx-large); + } + } + } + } + } + } + } + + // Desktop medium & large + &:not(.is-desktop__large) { + .pi-layout { + &.pi-layout__07-list { + & > header { + .pi-wrapper { + &.pi-wrapper__header { + width: 100%; + padding-right: var(--pi-spacing-xxxx-large); + padding-left: var(--pi-spacing-xxxx-large); + header { + width: 80%; + } + } + } + } + } + } + } + // Desktop large + &.is-desktop__large { + .pi-layout { + &.pi-layout__07-list { + & > header { + .pi-wrapper { + &.pi-wrapper__header { + min-height: var(--pi-height-layout-header); + width: var(--pi-public-width-container-l); + padding-right: var(--pi-spacing-xxx-large); + padding-left: var(--pi-spacing-xxx-large); + } + } + } + & > section { + padding-top: calc(var(--pi-height-layout-header) - var(--pi-spacing-medium)); + + .pi-wrapper { + &.pi-wrapper__list { + max-height: calc( + 100vh - var(--pi-public-height-header-desktop) - var(--pi-height-layout-header) - + 7.35rem + ); + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-container-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-container-layout.scss new file mode 100644 index 0000000..67e2693 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-container-layout.scss @@ -0,0 +1,53 @@ +@mixin containerSpacings( + $padding-top: calc(var(--pi-public-height-header-mobile) + var(--pi-spacing-xxx-large)), + $default-vertical-padding: calc(var(--pi-spacing-xxx-large) + var(--pi-spacing-xxx-large)), + $default-vertical-margin: var(--pi-spacing-xx-large) +) { + .pi-container-layout { + .pi-layout { + &:nth-child(1) { + &:not(.pi-layout__09-pricing):not(.pi-website-home-user):not( + .pi-layout__03-vertically-centered + ) { + & > *:nth-child(1) { + padding-top: $padding-top; + } + } + } + } + .pi-layout + .pi-layout__03-vertically-centered { + & > *:nth-child(1) { + padding-top: $default-vertical-padding; + padding-bottom: $default-vertical-padding; + } + } + .pi-layout + .pi-layout { + margin-top: $default-vertical-margin; + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + @include containerSpacings; + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + @include containerSpacings( + calc( + var(--pi-public-height-header-desktop) + var(--pi-spacing-xxxx-large) + + var(--pi-spacing-x-large) + ), + var(--pi-spacing-xxxx-large), + var(--pi-spacing-xx-large) + ); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/_pi-layout.scss b/libs/ui/src/lib/styles/deale-website/layouts/_pi-layout.scss new file mode 100644 index 0000000..c9f1d2f --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/_pi-layout.scss @@ -0,0 +1,33 @@ +@mixin layoutSpacings($padding: var(--pi-spacing-xx-large)) { + .pi-container-layout { + .pi-layout { + &:not(.pi-layout__03-vertically-centered) { + & > *:first-child { + padding-top: $padding; + } + & > *:last-child { + padding-bottom: $padding; + } + } + } + .pi-domain-component { + display: block; + width: -webkit-fill-available; + } + } +} + +html { + // Mobile + &.is-mobile { + @include layoutSpacings; + } + // Tablet + &.is-tablet { + @include layoutSpacings(var(--pi-spacing-xxx-large)); + } + // Desktop + &.is-desktop { + @include layoutSpacings(var(--pi-spacing-xxxx-large)); + } +} diff --git a/libs/ui/src/lib/styles/deale-website/layouts/index.scss b/libs/ui/src/lib/styles/deale-website/layouts/index.scss new file mode 100644 index 0000000..b71dafa --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/layouts/index.scss @@ -0,0 +1,8 @@ +@forward 'pi-02-full-width-layout'; +@forward 'pi-02a-full-width-auto-layout'; +@forward 'pi-03-vertically-centered-layout'; +@forward 'pi-06-edge-to-edge-layout'; +@forward 'pi-06b-edge-to-edge-home-users-layout'; +@forward 'pi-07-list-layout'; +@forward 'pi-container-layout'; +@forward 'pi-layout'; diff --git a/libs/ui/src/lib/styles/deale-website/screens/_pi-00-thank-you-screen.scss b/libs/ui/src/lib/styles/deale-website/screens/_pi-00-thank-you-screen.scss new file mode 100644 index 0000000..647c33e --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/screens/_pi-00-thank-you-screen.scss @@ -0,0 +1,99 @@ +@mixin thankYouScreenHeader($device: mobile) { + .pi-website-screen { + &.pi-website-screen__00-type-thank-you { + & > header { + display: block; + width: -webkit-fill-available; + padding-top: calc(var(--pi-spacing-x-large) + var(--pi-public-height-header-#{$device})); + } + } + } +} + +.pi-website-screen { + &.pi-website-screen__00-type-thank-you { + justify-content: space-evenly !important; + + & > *:nth-child(n + 1) { + text-align: center; + } + & > picture { + & > main { + display: grid; + height: 100%; + + @for $i from 1 through 2 { + & > *:nth-child(#{$i}) { + grid-area: item#{$i}; + align-self: center; + height: fit-content; + } + } + } + } + } +} +@mixin thankYouScreenMain( + $cols: 4, + $rows: 6, + $areas: '. . . .' 'item1 item1 . .' 'item1 item1 . .' 'item1 item1 . .' '. . item2 item2' + '. . item2 item2', + $min-height: 50 +) { + .pi-website-screen { + &.pi-website-screen__00-type-thank-you { + & > picture { + & > main { + grid-template-columns: repeat(#{$cols}, 1fr); + grid-template-rows: repeat(#{$rows}, 1fr); + grid-template-areas: $areas; + min-height: #{$min-height}vh; + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include thankYouScreenMain; + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small devices + @media screen and (max-width: 62rem) { + @include thankYouScreenHeader; + } + } + + // Tablet + &.is-tablet { + @include thankYouScreenMain( + 6, + 6, + '. . . . . .' '. item1 item1 . . .' '. item1 item1 . . .' '. item1 item1 . item2 item2' + '. item1 item1 . item2 item2' '. . . . . .', + 50 + ); + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large devices + @media screen and (min-width: 62rem) { + @include thankYouScreenHeader(desktop); + } + } + + &.is-desktop { + @include thankYouScreenMain( + 12, + 6, + '. . . . . . . . . . . .' '. . item1 item1 item1 . . . . . . .' + '. . item1 item1 item1 . . . . . . .' '. . item1 item1 item1 . . item2 item2 item2 . .' + '. . . . . . . item2 item2 item2 . .' '. . . . . . . . . . . .', + 50 + ); + } +} diff --git a/libs/ui/src/lib/styles/deale-website/screens/index.scss b/libs/ui/src/lib/styles/deale-website/screens/index.scss new file mode 100644 index 0000000..48d9639 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/screens/index.scss @@ -0,0 +1 @@ +@forward 'pi-00-thank-you-screen'; diff --git a/libs/ui/src/lib/styles/deale-website/shell/_pi-footer.scss b/libs/ui/src/lib/styles/deale-website/shell/_pi-footer.scss new file mode 100644 index 0000000..e2c1096 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/_pi-footer.scss @@ -0,0 +1,193 @@ +@mixin websiteFooterTop($flex-direction: column) { + .pi-website-footer { + & > footer { + flex-direction: $flex-direction; + } + } +} + +@mixin websiteFooterBottom($flex-direction: column-reverse) { + .pi-website-footer { + & > aside { + flex-direction: $flex-direction; + } + } +} + +@mixin websiteFooterTopLeft($flex-direction: column, $width: 100%, $justify-right: flex-start) { + .pi-website-footer-content__top-left { + flex-direction: $flex-direction; + width: $width; + & > section { + justify-content: $justify-right; + } + } +} + +@mixin websiteFooterTopRight($columns: 2, $width: 100%) { + .pi-website-footer-content__top-right { + grid-template-columns: repeat($columns, 1fr); + width: $width; + } +} + +@mixin websiteFooterBottomLeft($text-align: flex-start) { + .pi-website-footer-content__bottom-left { + text-align: $text-align; + } +} + +@mixin websiteFooterBottomRight() { + .pi-website-footer-content__bottom-right { + } +} + +@mixin websiteFooterGap($gap: var(--pi-spacing-x-large)) { + .pi-website-footer { + gap: $gap; + & > footer, + & > aside { + gap: $gap; + } + & > footer { + & > .pi-website-footer-content { + &.pi-website-footer-content__top-right { + gap: $gap; + } + } + } + } +} + +@mixin websiteFooterWidth($width: calc(100% - var(--pi-spacing-xx-large))) { + .pi-website-footer { + & > *:nth-child(n + 2) { + width: $width; + margin: 0 auto; + } + } +} + +.pi-website-footer { + display: flex; + flex-direction: column; + justify-content: space-evenly; + + width: -webkit-fill-available; + background-color: var(--pi-color-grey-25); + + // divider + & > span { + width: 100%; + height: 4.25rem; + left: 0px; + background: linear-gradient( + to right bottom, + var(--pi-color-white) 49%, + var(--pi-color-grey-25) 50% + ); + } + + & > footer, + & > aside { + display: flex; + padding: var(--pi-spacing-xx-large); + } + & > footer { + & > .pi-website-footer-content { + gap: var(--pi-spacing-small); + + &.pi-website-footer-content__top-left { + display: flex; + justify-content: space-between; + .pi-headline { + & > figure { + max-width: 5.625rem; + } + } + } + + &.pi-website-footer-content__top-right { + margin: 0 auto; + display: grid; + + .pi-menu { + & > nav { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-small); + + & > .pi-menu-event-item:first-child { + margin-bottom: var(--pi-spacing-small); + font: { + weight: 600; + } + } + & > :nth-child(n + 2) { + cursor: pointer; + } + } + } + } + } + } + & > aside { + align-items: center; + justify-content: space-between; + max-height: 12.5625rem; + border-top: 1px solid var(--pi-color-grey-100); + padding: var(--pi-spacing-x-large); + } +} + +html { + // Mobile + &.is-mobile { + @include websiteFooterBottom; + @include websiteFooterBottomLeft(center); + @include websiteFooterGap; + @include websiteFooterTopLeft; + @include websiteFooterTopRight; + @include websiteFooterWidth; + } + // Tablet + &.is-tablet { + @include websiteFooterWidth(calc(100% - 3rem)); + // Tablet S + @media screen and (max-width: 62rem) { + @include websiteFooterTopLeft(row, 100%, flex-end); + @include websiteFooterTopRight(3); + } + // Tablet L + @media screen and (min-width: 62rem) { + @include websiteFooterTopLeft(column, 25%); + @include websiteFooterTopRight(3, 75%); + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Large device + @media screen and (max-width: 62rem) { + @include websiteFooterTop; + } + } + // Tablet & Desktop + &:not(.is-mobile) { + @include websiteFooterBottom(row); + @include websiteFooterGap(var(--pi-spacing-xxxx-large)); + } + // Desktop + &.is-desktop { + @include websiteFooterTop(row); + @include websiteFooterTopLeft(column, 25%); + @include websiteFooterTopRight(3, 75%); + // Desktop S & M + &:not(.is-desktop__large) { + @include websiteFooterWidth(var(--pi-public-width-container)); + } + // Desktop L + &.is-desktop__large { + @include websiteFooterWidth(var(--pi-public-width-container-l)); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/shell/_pi-main.scss b/libs/ui/src/lib/styles/deale-website/shell/_pi-main.scss new file mode 100644 index 0000000..2c97fb7 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/_pi-main.scss @@ -0,0 +1,36 @@ +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-shell { + &.pi-shell__deale-website { + display: block; + max-width: 100vw; + overflow-x: hidden; + & > div { + &.is-menu-open { + position: fixed; + + &::after { + content: ''; + position: fixed; + z-index: 9; + height: 100vh; + width: 100%; + top: 0; + left: 0; + background-color: rgba($color: #0d2a6b, $alpha: 0.5); + } + .pi-container-layout { + & > .is-fixed { + display: none; + } + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/shell/_pi-menu-theming.scss b/libs/ui/src/lib/styles/deale-website/shell/_pi-menu-theming.scss new file mode 100644 index 0000000..f9ee3af --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/_pi-menu-theming.scss @@ -0,0 +1,432 @@ +@keyframes gradient { + 0% { + background-position: 0%; + } + 25% { + background-position: 25%; + } + 50% { + background-position: 50%; + } + 75% { + background-position: 75%; + } + 100% { + background-position: 100%; + } +} + +@mixin parentMenuItemColorLargeDevice($color, $border-color, $text-gradient) { + &:hover { + color: $color; + background: $border-color; + background-color: rgba($color: white, $alpha: 0.33); + animation: gradient 1.5s linear infinite; + animation-direction: alternate-reverse; + background-size: 150% 300%; + border: 0.0625rem solid transparent; + + &::before { + background-color: rgba($color: white, $alpha: 0.0125); + } + & > span { + & > span { + background-image: $text-gradient; + } + } + } +} + +// Mode light +@mixin topbarActionButtonLargeDeviceModeLight { + .pi-website-topbar-content { + &.pi-website-topbar-content__right { + // Login + .pi-button__action { + &.pi-button__type--menu-item { + box-shadow: 0 0 0 0 rgba($color: white, $alpha: 0.66); + & > span { + & > span { + background-color: var(--pi-color-white); + } + } + + &::before { + background-color: rgba($color: white, $alpha: 0.0125); + } + + background-color: var(--pi-color-white); + border-color: var(--pi-color-primary); + & > svg { + color: var(--pi-color-highlighted); + } + & > span { + background-image: linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted-900) + ); + } + } + } + } + } +} +@mixin topbarParentMenuItemLargeDeviceModeLight { + .pi-menu { + .pi-menu-event-item { + box-shadow: 0 0 0 0 rgba($color: white, $alpha: 0.66); + & > span { + & > span { + background-color: var(--pi-color-white); + } + } + } + } +} + +@mixin parentMenuItemNotHoverColorModeLight { + &:not(:hover) { + border-color: rgba($color: white, $alpha: 0.66); + background-color: rgba($color: white, $alpha: 0.15); + } +} + +@mixin parentMenuItemColorLargeDeviceModeLight { + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + .pi-menu { + &.pi-menu__parent { + & > nav { + & > .pi-menu-event-item { + // Company + &:nth-child(1) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-primary), + ( + linear-gradient( + rgba($color: white, $alpha: 0.125), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient(to right, var(--pi-color-primary-200), var(--pi-color-primary)) + border-box + ), + linear-gradient(30deg, var(--pi-color-primary), var(--pi-color-primary-900)) + ); + } + // Investor + &:nth-child(2) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-accent), + ( + linear-gradient( + rgba($color: white, $alpha: 0.125), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient(to right, var(--pi-color-accent-200), var(--pi-color-accent)) + border-box + ), + linear-gradient(30deg, var(--pi-color-accent), var(--pi-color-accent-900)) + ); + } + // Advisor + &:nth-child(3) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient( + rgba($color: white, $alpha: 0.5), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-highlighted-200), + var(--pi-color-highlighted) + ) + border-box + ), + linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted-900) + ) + ); + } + @include parentMenuItemNotHoverColorModeLight; + } + } + } + } + } + // Right + &.pi-website-topbar-content__right { + .pi-menu { + &:not(.pi-menu__children) { + & > nav { + & > .pi-menu-event-item { + // Prices + &:nth-of-type(1) { + @include parentMenuItemNotHoverColorModeLight; + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient( + rgba($color: white, $alpha: 0.95), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-highlighted-200), + var(--pi-color-highlighted) + ) + border-box + ), + linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted-900) + ) + ); + } + } + } + } + } + // Login + .pi-button__action { + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient(rgba($color: white, $alpha: 0.95), rgba($color: white, $alpha: 1)) + padding-box, + linear-gradient(to right, var(--pi-color-highlighted-200), var(--pi-color-highlighted)) + border-box + ), + linear-gradient(30deg, var(--pi-color-highlighted), var(--pi-color-highlighted-900)) + ); + } + } + } +} + +// Mode dark +@mixin topbarActionButtonLargeDeviceModeDark { + .pi-website-topbar-content { + &.pi-website-topbar-content__right { + // Login + .pi-button__action { + box-shadow: 0 0 0 0 rgba($color: #0d2a6b, $alpha: 0.66); + & > span { + background-image: linear-gradient(30deg, var(--pi-color-white), var(--pi-color-white)); + & > span { + background-color: var(--pi-color-highlighted); + } + } + + &::before { + background-color: rgba($color: #0d2a6b, $alpha: 0.0125); + } + + background-color: var(--pi-color-highlighted); + border-color: var(--pi-color-highlighted); + & > svg { + color: var(--pi-color-white); + } + &:hover { + & > span { + background-image: linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted) + ); + & > span { + background-color: var(--pi-color-highlighted-100); + } + } + & > svg { + color: var(--pi-color-highlighted); + } + } + } + } + } +} +@mixin topbarParentMenuItemLargeDeviceModeDark { + .pi-menu { + .pi-menu-event-item { + box-shadow: 0 0 0 0 rgba($color: #3d3d3d, $alpha: 0.26); + & > span { + & > span { + background-color: var(--pi-color-grey); + } + } + } + } +} + +@mixin parentMenuItemNotHoverColorModeDark { + &:not(:hover) { + border-color: rgba($color: #3d3d3d, $alpha: 0.36); + background-color: rgba($color: white, $alpha: 0.44); + } +} + +@mixin parentMenuItemColorLargeDeviceModeDark { + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + .pi-menu { + &.pi-menu__parent { + & > nav { + & > .pi-menu-event-item { + // Company + &:nth-child(1) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-primary), + ( + linear-gradient( + rgba($color: white, $alpha: 0.125), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-primary-75), + var(--pi-color-primary-200) + ) + border-box + ), + linear-gradient(30deg, var(--pi-color-primary), var(--pi-color-primary-900)) + ); + } + // Investor + &:nth-child(2) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-accent), + ( + linear-gradient( + rgba($color: white, $alpha: 0.125), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-accent-100), + var(--pi-color-accent-200) + ) + border-box + ), + linear-gradient(30deg, var(--pi-color-accent), var(--pi-color-accent-900)) + ); + } + // Advisor + &:nth-child(3) { + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient( + rgba($color: white, $alpha: 0.5), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-highlighted-100), + var(--pi-color-highlighted-200) + ) + border-box + ), + linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted-900) + ) + ); + } + @include parentMenuItemNotHoverColorModeDark; + } + } + } + } + } + // Right + &.pi-website-topbar-content__right { + .pi-menu { + &:not(.pi-menu__children) { + & > nav { + & > .pi-menu-event-item { + // Prices + &:nth-of-type(1) { + @include parentMenuItemNotHoverColorModeDark; + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient( + rgba($color: white, $alpha: 0.95), + rgba($color: white, $alpha: 1) + ) + padding-box, + linear-gradient( + to right, + var(--pi-color-highlighted-200), + var(--pi-color-highlighted) + ) + border-box + ), + linear-gradient( + 30deg, + var(--pi-color-highlighted), + var(--pi-color-highlighted-900) + ) + ); + } + } + } + } + } + // Login + .pi-button__action { + @include parentMenuItemColorLargeDevice( + var(--pi-color-highlighted), + ( + linear-gradient(rgba($color: white, $alpha: 0.95), rgba($color: white, $alpha: 1)) + padding-box, + linear-gradient(to right, var(--pi-color-highlighted-200), var(--pi-color-highlighted)) + border-box + ), + linear-gradient(30deg, var(--pi-color-highlighted), var(--pi-color-highlighted-900)) + ); + } + } + } +} + +html { + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // Topbar + .pi-website-topbar { + // Mode light (White menu) + &:not(.is-mode-dark) { + @include parentMenuItemColorLargeDeviceModeLight; + @include topbarParentMenuItemLargeDeviceModeLight; + @include topbarActionButtonLargeDeviceModeLight; + } + + // Mode dark + &.is-mode-dark { + @include parentMenuItemColorLargeDeviceModeDark; + @include topbarParentMenuItemLargeDeviceModeDark; + @include topbarActionButtonLargeDeviceModeDark; + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/shell/_pi-menu.scss b/libs/ui/src/lib/styles/deale-website/shell/_pi-menu.scss new file mode 100644 index 0000000..e6a2c9b --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/_pi-menu.scss @@ -0,0 +1,499 @@ +@keyframes pulse { + to { + box-shadow: 0 0 0 var(--pi-spacing-xxx-small) transparent; + } +} + +@keyframes gradient { + 0% { + background-position: 0%; + } + 25% { + background-position: 25%; + } + 50% { + background-position: 50%; + } + 75% { + background-position: 75%; + } + 100% { + background-position: 100%; + } +} + +@mixin topbarParentMenuItemLargeDevice { + border: 0.0625rem solid; + border-radius: var(--pi-border-radius-medium); + transition: 0.4s all ease-in-out; + animation: pulse 1.5s infinite 2s ease-in; + &:hover { + cursor: pointer; + animation: unset; + &::before { + height: 2.75rem; + width: 6rem; + margin-top: -0.125rem; + margin-left: -0.125rem; + opacity: 1; + filter: blur(0.1rem); + } + } + &::before { + content: ''; + position: absolute; + transition: all 0.3s ease; + border-radius: 0.75rem; + opacity: 0; + height: 0; + width: 0; + margin-top: 1.1875rem; + margin-left: 2.8125rem; + } + & > span { + & > span { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 2.25rem; + width: 6rem; + padding-bottom: 0.05rem; + background-size: 100%; + background-repeat: repeat; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-background-clip: text; + -moz-text-fill-color: transparent; + transition: all 0.45s ease-Out; + + font: { + weight: 400; + } + } + } +} + +@mixin largeDeviceChildMenu( + $margin-left, + $background-color, + $grid-template-columns: repeat(2, 1fr) +) { + // Menu item + .pi-menu__children { + z-index: 99; + position: absolute; + margin-top: var(--pi-spacing-large); + margin-left: $margin-left; + border-radius: var(--pi-border-radius-large); + background-color: var(--pi-color-white); + box-shadow: var(--pi-box-shadow-double); + & > div { + display: grid; + grid-template-columns: $grid-template-columns; + gap: var(--pi-spacing-medium); + } + + & > aside { + background-color: $background-color; + } + + // Submenu entry + .pi-list-item { + min-width: 15.625rem; + &:not(:hover) { + .pi-list-item-content { + &.pi-list-item-content__left { + & > span { + // + &:nth-child(1) { + color: var(--pi-color-black); + } + &:nth-child(2) { + color: var(--pi-color-grey-300); + } + } + } + } + } + &:hover { + cursor: pointer; + } + } + } +} + +@mixin largeDeviceChildMenuItemColor($background-color) { + // Submenu entry + .pi-list-item { + &:hover { + background-color: $background-color; + } + } +} +// Small device +@mixin smallDevieMenuSize($topbar-center-content-height: calc(100vh - 10rem)) { + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + height: $topbar-center-content-height; + min-height: $topbar-center-content-height; + overflow: auto; + + // Generic Scroll + &::-webkit-scrollbar { + width: var(--pi-spacing-xxx-small); + } + + &::-webkit-scrollbar-track { + box-shadow: none; + } + + &::-webkit-scrollbar-thumb { + border-radius: var(--pi-border-radius-small); + background-color: var(--pi-color-grey-75); + + &:hover { + background-color: var(--pi-color-grey-100); + transition: var(--pi-animation); + } + } + } + } +} + +@mixin smallDeviceMenuColor($border-color, $color) { + & > span { + border-color: $border-color; + & > span { + color: $color; + } + } +} + +.pi-website-footer-content { + &.pi-website-footer-content__top-right { + .pi-menu-event-item { + &:nth-child(2) { + .pi-menu__children { + .pi-text { + color: var(--pi-color-primary); + } + } + } + &:nth-child(3) { + .pi-menu__children { + .pi-text { + color: var(--pi-color-accent); + } + } + } + &:nth-child(4) { + .pi-menu__children { + .pi-text { + color: var(--pi-color-highlighted); + } + } + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include smallDevieMenuSize; + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + .pi-menu { + // Parent + &.pi-menu__parent { + & > nav { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-xx-large); + padding-bottom: var(--pi-spacing-xx-large); + .pi-menu-event-item { + &.has-children { + & > span { + display: flex; + width: -webkit-fill-available; + border-bottom: 1px solid; + padding-bottom: var(--pi-spacing-xxx-small); + + & > span { + padding-left: var(--pi-spacing-xx-small); + font: { + size: var(--pi-font-size-x-small); + } + } + } + // Company + &:nth-child(1) { + @include smallDeviceMenuColor( + var(--pi-color-primary-100), + var(--pi-color-primary) + ); + } + // Investor + &:nth-child(2) { + @include smallDeviceMenuColor( + var(--pi-color-accent-100), + var(--pi-color-accent) + ); + } + // Advisor + &:nth-child(3) { + @include smallDeviceMenuColor( + var(--pi-color-highlighted-100), + var(--pi-color-highlighted) + ); + } + } + } + } + } + + // Children + &.pi-menu__children { + .pi-list-item { + padding-top: var(--pi-spacing-small); + padding-bottom: var(--pi-spacing-small); + &:nth-child(n + 2) { + padding-left: var(--pi-spacing-x-large); + .pi-list-item-content { + &.pi-list-item-content__left { + & > span { + &:nth-child(1) { + color: var(--pi-color-grey); + font: { + weight: 600; + } + } + } + } + } + } + } + .pi-list-item-content { + &.pi-list-item-content__left { + & > span { + &:nth-child(2) { + color: var(--pi-color-grey-300); + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } + } + } + } + } + + // Tablet + &.is-tablet { + &.is-tablet__small { + @include smallDevieMenuSize(calc(100vh - 11.5rem)); + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + .pi-shell__deale-website { + // Topbar + .pi-website-topbar { + .pi-menu { + .pi-menu-event-item { + @include topbarParentMenuItemLargeDevice; + } + // Parent + &.pi-menu__parent { + & > nav { + display: flex; + gap: var(--pi-spacing-medium); + } + .pi-menu-event-item { + &.has-children { + &:not(.is-selected) { + .pi-menu__children { + opacity: 0; + transform: scale(0); + } + } + &.is-selected { + .pi-menu__children { + opacity: 1; + transform: scale(1); + } + } + } + } + } + + // Child + &.pi-menu__children { + & > div, + & > aside { + // Menu items + & > div { + & > span { + display: flex; + align-items: center; + gap: var(--pi-spacing-xxx-small); + } + } + } + // Child menu + & > div { + padding: var(--pi-spacing-medium); + border-radius: var(--pi-border-radius-large) var(--pi-border-radius-large) 0 0; + // Child Menu items + & > div { + & > span { + display: flex; + align-items: center; + gap: var(--pi-spacing-xxx-small); + } + } + } + // Aside menu + & > aside { + display: flex; + justify-content: space-between; + + width: 100%; + padding: var(--pi-spacing-xx-small) var(--pi-spacing-x-large) + var(--pi-spacing-medium); + border-radius: 0 0 var(--pi-border-radius-large) var(--pi-border-radius-large); + // Aside Menu items + & > div { + & > span { + display: flex; + align-items: center; + gap: var(--pi-spacing-xxx-small); + } + } + } + } + } + + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + .pi-menu { + &.pi-menu__parent { + & > nav { + & > .pi-menu-event-item { + // Company + &:nth-child(1) { + @include largeDeviceChildMenu(-18.75rem, var(--pi-color-primary-25)); + @include largeDeviceChildMenuItemColor(var(--pi-color-primary-25)); + } + // Investor + &:nth-child(2) { + @include largeDeviceChildMenu(-14.125rem, var(--pi-color-accent-25)); + @include largeDeviceChildMenuItemColor(var(--pi-color-accent-25)); + } + // Advisor + &:nth-child(3) { + @include largeDeviceChildMenu(-14.625rem, var(--pi-color-highlighted-25)); + @include largeDeviceChildMenuItemColor(var(--pi-color-highlighted-25)); + } + } + } + } + } + } + // Right + &.pi-website-topbar-content__right { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--pi-spacing-medium); + @include largeDeviceChildMenu(-100px, unset, 1fr); + .pi-menu { + &:not(.pi-menu__children) { + & > nav { + & > .pi-menu-event-item { + @include topbarParentMenuItemLargeDevice; + + // Prices + &:nth-of-type(1) { + .pi-menu__children { + & > div { + .pi-list-item { + &:hover { + // Company Pricing + &:nth-child(1) { + color: var(--pi-color-primary); + background-color: var(--pi-color-primary-25); + } + // Investor Pricing + &:nth-child(2) { + color: var(--pi-color-accent); + background-color: var(--pi-color-accent-25); + } + // Advisor Pricing + &:nth-child(3) { + color: var(--pi-color-highlighted); + background-color: var(--pi-color-highlighted-25); + } + } + } + } + } + } + } + } + } + } + + // Login + .pi-button__type--menu-item { + &.pi-button__action { + display: flex; + justify-content: center; + align-items: center; + height: 2.25rem; + width: 7rem; + border-radius: var(--pi-border-radius-medium); + + @include topbarParentMenuItemLargeDevice; + & > span { + padding-bottom: 0.1875rem; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-background-clip: text; + -moz-text-fill-color: transparent; + + font: { + weight: 400; + } + } + + position: relative; + transition: all 0.45s ease-Out; + } + } + } + } + } + } + } + } + // LargeDevice + &.is-desktop { + } +} diff --git a/libs/ui/src/lib/styles/deale-website/shell/_pi-topbar.scss b/libs/ui/src/lib/styles/deale-website/shell/_pi-topbar.scss new file mode 100644 index 0000000..7bb76f8 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/_pi-topbar.scss @@ -0,0 +1,324 @@ +@mixin topbarCommonSizes( + $height: var(--pi-public-height-header-mobile), + $max-width: calc(100% - var(--pi-spacing-xx-large)), + $small-size: -webkit-fill-available, + $large-size: calc(100% - var(--pi-spacing-xxx-small)) +) { + .pi-website-topbar { + & > header { + height: $height; + max-width: $max-width; + margin: 0 auto; + + .pi-website-topbar-content { + // Left + &.pi-website-topbar-content__left { + width: $small-size; + } + // Center + &.pi-website-topbar-content__center { + width: $large-size; + } + // Right + &.pi-website-topbar-content__right { + width: $small-size; + } + } + } + } +} + +@mixin logoSize($max-width: 7.125rem) { + .pi-website-topbar { + .pi-website-topbar-content { + &.pi-website-topbar-content__left { + & > .pi-headline { + max-width: $max-width; + &:hover { + cursor: pointer; + } + } + } + } + } +} +// Topbar menu is closed +@mixin smallDeviceTopbarMenuIsClosed { + .pi-shell { + &.pi-shell__deale-website { + & > div { + // Is Open + &:not(.is-menu-open) { + .pi-website-topbar { + top: 0; + left: 0; + height: var(--pi-public-height-header-mobile); + width: -webkit-fill-available; + padding: 0; + border-radius: var(--pi-border-radius-large); + .pi-website-topbar-content { + // Center + &.pi-website-topbar-content__center { + transform: scale(0); + opacity: 0; + } + // Right + &.pi-website-topbar-content__right { + display: flex; + flex-direction: row-reverse; + gap: var(--pi-spacing-x-small); + } + } + } + } + } + } + } +} + +// Ope topbar menu +@mixin smallDeviceTopbarMenuIsOpen( + $side-spacing: var(--pi-spacing-medium), + $height: calc(100vh - var(--pi-spacing-xx-large)), + $width: calc(100% - var(--pi-spacing-xx-large)) +) { + .pi-shell { + &.pi-shell__deale-website { + & > div { + // Is Open + &.is-menu-open { + .pi-website-topbar { + position: fixed; + top: $side-spacing; + left: $side-spacing; + z-index: 99; + height: $height; + width: $width; + border-radius: var(--pi-border-radius-large); + background-color: var(--pi-color-white); + box-shadow: var(--pi-box-shadow-double); + + .pi-close-button { + position: absolute; + top: var(--pi-spacing-large); + right: var(--pi-spacing-x-large); + } + & > header { + flex-direction: column; + gap: var(--pi-spacing-xxxx-small); + } + .pi-website-topbar-content { + padding-left: calc(var(--pi-spacing-x-large) - var(--pi-spacing-xxx-small)); + padding-right: calc(var(--pi-spacing-x-large) - var(--pi-spacing-xxx-small)); + // Left (=== top) + &.pi-website-topbar-content__left { + padding-top: var(--pi-spacing-large); + padding-bottom: var(--pi-spacing-large); + } + // Center + &.pi-website-topbar-content__center { + width: calc(100% - var(--pi-spacing-xx-small)); + margin: 0 auto; + transform: scale(1); + opacity: 1; + } + // Right (=== bottom) + &.pi-website-topbar-content__right { + padding-bottom: var(--pi-spacing-large); + .pi-button__action { + &.pi-button__type--menu-item { + height: var(--pi-spacing-xxx-large); + min-width: var(--pi-spacing-xxx-large); + width: -webkit-fill-available; + border-radius: var(--pi-border-radius-medium); + -webkit-border-radius: var(--pi-border-radius-medium); + -moz-border-radius: var(--pi-border-radius-medium); + color: var(--pi-color-white); + background-image: var(--pi-gradient-primary-dark); + font: { + weight: 400; + } + } + } + } + } + } + } + } + } + } +} + +.pi-website-topbar { + position: absolute; + z-index: 99; + width: -webkit-fill-available; + background-color: transparent; + & > header { + display: flex; + justify-content: space-between; + } +} + +.pi-shell { + &.pi-shell__deale-website { + & > div { + // Is Closed + &:not(.is-menu-open) { + width: -webkit-fill-available; + } + } + } +} + +html { + // Mobile + &.is-mobile { + @include smallDeviceTopbarMenuIsOpen; + .pi-shell { + &.pi-shell__deale-website { + & > div { + // Is Closed + &:not(.is-menu-open) { + @include logoSize; + @include topbarCommonSizes; + } + // Is Open + &.is-menu-open { + @include logoSize(6.625rem); + @include topbarCommonSizes( + var(--pi-public-height-header-mobile), + -webkit-fill-available + ); + } + } + } + } + } + // Tablet + &.is-tablet { + // Tablet S + &.is-tablet__small { + @include smallDeviceTopbarMenuIsOpen( + var(--pi-spacing-x-large), + calc(100vh - 3rem), + calc(100% - 3rem) + ); + .pi-shell { + &.pi-shell__deale-website { + & > div { + // Is Closed + &:not(.is-menu-open) { + @include logoSize(7.5rem); + @include topbarCommonSizes(var(--pi-public-height-header-mobile), calc(100% - 3rem)); + } + + // Is Open + &.is-menu-open { + @include logoSize(7.125rem); + @include topbarCommonSizes( + var(--pi-public-height-header-mobile), + -webkit-fill-available + ); + } + } + } + } + } + // Tablet L + &.is-tablet__large { + @include topbarCommonSizes(var(--pi-public-height-header-mobile), calc(100% - 3rem)); + @include logoSize(8.4375rem); + } + } + + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + // Closded menu has equal properties for Mobile & Tablet S + @include smallDeviceTopbarMenuIsClosed; + .pi-shell { + &.pi-shell__deale-website { + .pi-website-topbar { + & > header { + align-items: center; + } + } + + // Override right content buttons + button { + &:not(.pi-close-button):not(.pi-hamburger):not(.mdc-switch):not(.pi-button__toggle):not( + .pi-button__type--menu-item + ).pi-color__white { + border-color: var(--pi-color-primary-100); + background-color: rgba(255, 255, 255, 0.7); + background-image: unset; + &:hover { + border-color: var(--pi-color-primary-200); + background-color: rgba(255, 255, 255, 0.9); + .mat-icon { + color: var(--pi-color-primary); + } + } + } + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large Device + @media screen and (min-width: 62rem) { + .pi-website-topbar { + & > header { + align-items: flex-end; + } + } + } + } + // Desktop + &.is-desktop { + .pi-website-topbar-content { + display: flex; + // Center + &.pi-website-topbar-content__center { + justify-content: center; + } + // Right + &.pi-website-topbar-content__right { + justify-content: flex-end; + } + } + @include logoSize(8.75rem); + // Desktop S + &.is-desktop__small { + @include topbarCommonSizes( + var(--pi-public-height-header-desktop), + var(--pi-public-width-container), + 30%, + 40% + ); + } + + // Desktop S & M + &:not(.is-desktop__large) { + @include topbarCommonSizes( + var(--pi-public-height-header-desktop), + var(--pi-public-width-container), + 30%, + 40% + ); + } + // Desktop L + &.is-desktop__large { + @include topbarCommonSizes( + var(--pi-public-height-header-desktop), + var(--pi-public-width-container-l), + 30%, + 40% + ); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/shell/index.scss b/libs/ui/src/lib/styles/deale-website/shell/index.scss new file mode 100644 index 0000000..836cf84 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/shell/index.scss @@ -0,0 +1,5 @@ +@forward 'pi-footer'; +@forward 'pi-main'; +@forward 'pi-menu'; +@forward 'pi-menu-theming'; +@forward 'pi-topbar'; diff --git a/libs/ui/src/lib/styles/deale-website/wrappers/_pi-form-wrapper.scss b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-form-wrapper.scss new file mode 100644 index 0000000..f2f0b3b --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-form-wrapper.scss @@ -0,0 +1,62 @@ +.pi-shell__deale-website { + .pi-wrapper { + &.pi-wrapper__form { + // Vertically behavior + & > form { + & > fieldset { + & > label { + &.pi-text__size--x-small { + padding-left: var(--pi-spacing-x-small); + padding-bottom: var(--pi-spacing-xxx-small); + } + } + .pi-panel { + margin-bottom: var(--pi-spacing-x-small); + border: 1px solid var(--pi-color-grey-75); + } + } + } + .pi-list-item + .pi-list-item { + margin-top: unset; + } + } + } + .pi-website-contact-form { + .pi-panel__padding--m { + padding: var(--pi-spacing-xx-small) var(--pi-spacing-medium); + } + } +} + +html { + // Mobile + &.is-mobile { + .pi-shell__deale-website { + .pi-wrapper { + &.pi-wrapper__form { + // Vertically behavior + & > form { + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-large); + } + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + .pi-shell__deale-website { + .pi-wrapper { + &.pi-wrapper__form { + // Vertically behavior + & > form { + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-xxx-small); + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/wrappers/_pi-header-wrapper.scss b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-header-wrapper.scss new file mode 100644 index 0000000..53e3f17 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-header-wrapper.scss @@ -0,0 +1,11 @@ +.pi-wrapper { + // Header Wrapper + &.pi-wrapper__header { + // Flex direction + &.pi-wrapper__flex-direction--column { + .pi-header-wrapper-content { + gap: var(--pi-spacing-medium); + } + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/wrappers/_pi-wrapper.scss b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-wrapper.scss new file mode 100644 index 0000000..d325ba2 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/wrappers/_pi-wrapper.scss @@ -0,0 +1,30 @@ +@mixin dealeWebsiteWrapperMaxWidth($max-width: calc(100% - var(--pi-spacing-xx-large))) { + .pi-shell__deale-website { + .pi-wrapper { + // Max width + &.pi-wrapper__full-width, + &.pi-wrapper__full-width-centered { + max-width: $max-width; + } + } + } +} + +html { + &.is-mobile { + @include dealeWebsiteWrapperMaxWidth; + } + &.is-tablet { + @include dealeWebsiteWrapperMaxWidth(calc(100% - 3rem)); + } + &.is-desktop { + // Desktop S & M + &:not(.is-desktop__large) { + @include dealeWebsiteWrapperMaxWidth(var(--pi-public-width-container)); + } + // Desktop L + &.is-desktop__large { + @include dealeWebsiteWrapperMaxWidth(var(--pi-public-width-container-l)); + } + } +} diff --git a/libs/ui/src/lib/styles/deale-website/wrappers/index.scss b/libs/ui/src/lib/styles/deale-website/wrappers/index.scss new file mode 100644 index 0000000..5257df3 --- /dev/null +++ b/libs/ui/src/lib/styles/deale-website/wrappers/index.scss @@ -0,0 +1,3 @@ +@forward 'pi-form-wrapper'; +@forward 'pi-header-wrapper'; +@forward 'pi-wrapper'; diff --git a/libs/ui/src/lib/styles/definitions.ts b/libs/ui/src/lib/styles/definitions.ts new file mode 100644 index 0000000..4d17b19 --- /dev/null +++ b/libs/ui/src/lib/styles/definitions.ts @@ -0,0 +1,167 @@ +// Types +export type justify = 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly'; +export type grid = + | 'quarter' + | 'third' + | 'half' + | 'fifth' + | 'reverse-quarter' + | 'reverse-third' + | 'reverse-fifth' + | 'none'; +export type flexDirection = 'column' | 'column-reverse' | 'row' | 'row-reverse'; +export type alignment = 'flex-start' | 'center' | 'flex-end' | 'stretch'; +export type gap = + | 'xxxs' + | 'xxs' + | 'xs' + | 's' + | 'm' + | 'l' + | 'xl' + | 'xxl' + | 'xxxl' + | 'xxxxl' + | 'none'; +export type borderType = 'top' | 'bottom' | 'middle'; +export type borderRadius = 'small' | 'medium' | 'large'; + +export type display = 'inline' | 'flex' | 'grid'; +export type padding = 'xxxxl' | 'xxxl' | 'xxl' | 'xl' | 'l' | 'm' | 's' | 'xs' | 'xxs' | 'none'; + +export type maxWidth = 'full-width' | 'full-width-auto' | 'edge-to-edge' | 'fit-content'; +export type itemsWidth = 'quarter' | 'third' | 'half' | 'full'; +// Fonts +export type fontSize = + | 'xxs' + | 'xs' + | 's' + | 'm' + | 'l' + | 'xl' + | 'xxl' + | 'xxxl' + | 'xs-m' + | 'xs-l' + | 'xl-m' + | 'xl-s' + | 'xxl-m' + | 'xxl-s' + | 's-xxl' + | 'm-xxl' + | 'xl-xs' + | 'm-xs' + | 'm-xl' + | 's-l' + | 's-xl' + | 's-xs' + | 's-xxs' + | 'xs-s' + | 'xs-xxs' + | 'xxs-s' + | 'xxs-xs' + | 'xs-xl' + | 'l-xs' + | 'l-s' + | 'xxxl-xs' + | 'xs-xxxl' + | 'xxxl-s' + | 's-xxxl' + | 'xxxl-m' + | 'm-xxxl' + | 'xxxl-l' + | 'l-xxxl' + | 'xxxl-xl' + | 'xl-xxxl' + | 'xxxl-xxl' + | 'xxl-xxxl'; + +export type fontStyle = 'normal' | 'italic' | 'normal-italic' | 'italic-normal'; +export type fontWeight = + | '300' + | '400' + | '600' + | '700' + | '800' + | '300-400' + | '300-600' + | '300-700' + | '300-800' + | '400-300' + | '400-700' + | '400-800' + | '600-300' + | '700-300' + | '700-400' + | '800-400'; + +export type animation = 'horizontally' | 'vertically' | 'clickable'; + +// Wrappers + +// Data Table cell +export type textStyle = 'regular' | 'bold' | 'italic' | 'indent'; + +// Buttons +export type buttonStyle = 'fill' | 'ghost' | 'outline'; + +export type buttonSize = 'x-small' | 'small' | 'medium' | 'large' | 'w100'; + +// Icons +export type iconSize = 'x-small' | 'small' | 'medium' | 'large' | 'x-large'; + +// Color +export type styleType = + | 'primary' + | 'accent' + | 'warn' + | 'success' + | 'white' + | 'black' + | 'grey' + | 'highlighted'; + +export type theming = 'grey' | 'primary' | 'accent' | 'highlighted'; + +// Mode +export type mode = 'light' | 'dark'; + +// Opacity +export type opacity = '20' | '40' | '60' | '80' | '100'; + +// Density +export type density = + | '10' + | '25' + | '50' + | '75' + | '100' + | '200' + | '300' + | '400' + | '500' + | '900' + | ''; + +export type textColor = + | 'primary' + | 'accent' + | 'warn' + | 'success' + | 'white' + | 'black' + | 'dark' + | 'grey' + | 'highlighted' + | 'black-primary' + | 'primary-black' + | 'black-accent' + | 'accent-black' + | 'dark-black' + | 'black-dark' + | 'grey-black' + | 'black-grey' + | 'highlighted-grey' + | 'grey-highlighted'; +// Context +export type context = 'public' | 'private'; diff --git a/libs/ui/src/lib/styles/index.ts b/libs/ui/src/lib/styles/index.ts new file mode 100644 index 0000000..d214c8e --- /dev/null +++ b/libs/ui/src/lib/styles/index.ts @@ -0,0 +1,5 @@ +export * from './animations'; + +export * from './configs'; + +export * from './definitions'; diff --git a/libs/ui/src/lib/ui/accordion/accordion.module.ts b/libs/ui/src/lib/ui/accordion/accordion.module.ts new file mode 100644 index 0000000..7376654 --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/accordion.module.ts @@ -0,0 +1,56 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +import { PiAccordionComponent } from './accordion/accordion.component'; + +import { CdkAccordionModule } from '@angular/cdk/accordion'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITextModule } from '../text'; +import { + PiAccordionFooterDirective, + PiAccordionHeaderDirective, + PiAccordionItemContentDirective, + PiAccordionItemDirective, + PiAccordionItemHeaderDirective, + PiAccordionSummaryContentDirective, +} from './directives/accordion-content.directive'; +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIChipModule } from '../chip'; + +const MATERIAL_MODULES = [CdkAccordionModule]; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIChipModule, + PiSharedUIIconModule, + PiSharedUITextModule, + PiSharedUITextModule, + PiSharedUtilColorModule, + TranslatePipe, TranslateDirective, + MATERIAL_MODULES, + ], + declarations: [ + PiAccordionComponent, + PiAccordionItemHeaderDirective, + PiAccordionItemContentDirective, + PiAccordionItemDirective, + PiAccordionSummaryContentDirective, + PiAccordionHeaderDirective, + PiAccordionFooterDirective, + ], + exports: [ + PiAccordionComponent, + PiAccordionItemHeaderDirective, + PiAccordionItemContentDirective, + PiAccordionItemDirective, + PiAccordionSummaryContentDirective, + PiAccordionHeaderDirective, + PiAccordionFooterDirective, + ], +}) +export class PiSharedUIAccordionModule {} diff --git a/libs/ui/src/lib/ui/accordion/accordion/accordion.component.html b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.html new file mode 100644 index 0000000..6de5faf --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.html @@ -0,0 +1,156 @@ + + + + +
+ +
+ +
+ + +
+ + +
+ + +
+ +
+ + +
+ +
+
+
+ + + + + + + + + +
+ + +
+ + +
+
+
+
+ +
+ +
+
+
diff --git a/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts new file mode 100644 index 0000000..f601df8 --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/accordion/accordion.component.ts @@ -0,0 +1,98 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + EventEmitter, + HostBinding, + Input, + OnChanges, + Output, + QueryList, + SimpleChanges, +} from '@angular/core'; +import { padding, styleType, textColor } from '../../../styles'; +import { PiAccordionItemDirective } from '../directives/accordion-content.directive'; + +@Component({ + standalone: false, + selector: 'pi-accordion', + templateUrl: './accordion.component.html', + styleUrls: ['../styles/accordion.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiAccordionComponent implements OnChanges, AfterContentInit { + @HostBinding('class') get classes(): string { + const colorClass = `pi-accordion__${this.color}`; + const appearanceClass = + this.appearance !== 'default' && `pi-accordion__appearance--${this.appearance}`; + const directionClass = + this.direction !== 'default' && `pi-accordion__direction--${this.direction}`; + const paddingClass = `pi-accordion__padding--${this.padding}`; + return ['pi-accordion', colorClass, appearanceClass, directionClass, paddingClass].join(' '); + } + + @Input() expandedItemIndex!: number; + @Input() appearance: 'default' | 'outline' | 'fill' = 'default'; + @Input() color: styleType = 'white'; + @Input() direction: 'default' | 'horizontal' = 'default'; + @Input() hasForm = false; + @Input() padding: padding = 'm'; + + @Input() hasChevron = true; + + @Output() expandedItemIndexChange: EventEmitter = new EventEmitter(); + + @ContentChildren(PiAccordionItemDirective) items!: QueryList; + + constructor(private cdr: ChangeDetectorRef) {} + + ngOnChanges(changes: SimpleChanges): void { + if ( + changes['expandedItemIndex'] && + changes['expandedItemIndex'].currentValue !== changes['expandedItemIndex'].previousValue + ) { + this.expandedItemIndex = changes['expandedItemIndex'].currentValue; + } + } + + ngAfterContentInit(): void { + this.items.changes.subscribe(() => { + this.cdr.markForCheck(); + }); + } + + toggleState = (item: PiAccordionItemDirective, index: number) => { + if (this.direction === 'horizontal' && this.isItemExpanded(index)) return; + if (item.status === 'disabled') return; + if (this.expandedItemIndex === index) { + this.expandedItemIndex = null; + } else { + this.expandedItemIndex = index; + } + this.expandedItemIndexChange.emit(this.expandedItemIndex); + }; + + isItemExpanded = (index: number) => this.expandedItemIndex === index; + + setAccordionItemClasses(item: PiAccordionItemDirective, index: number): string { + const expandedClass = this.isItemExpanded(index) ? 'is-expanded' : 'is-collapsed'; + const statusClass = item.status && 'has-status' + ' ' + `is-${item.status}`; + const isConfidentialClass = item.isConfidential ? 'is-confidential' : ''; + return [expandedClass, statusClass, isConfidentialClass].join(' '); + } + + getHeaderTextColor(): textColor { + switch (this.color) { + case 'primary': + return 'primary'; + case 'accent': + return 'accent'; + case 'warn': + return 'warn'; + default: + return 'black-grey'; + } + } +} diff --git a/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts b/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts new file mode 100644 index 0000000..e9abe36 --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/directives/accordion-content.directive.ts @@ -0,0 +1,81 @@ +import { + AfterViewInit, + ContentChild, + Directive, + HostBinding, + Input, + TemplateRef, +} from '@angular/core'; +import { status } from '../../../util/status'; + +/** + * The accordion header is a separated content of the accordion items group. + * If it is needed it must be used as a Header for the whole accordion. + */ + +@Directive({ + standalone: false, + selector: 'pi-accordion-header', +}) +export class PiAccordionHeaderDirective { + @HostBinding('class') className = 'pi-accordion-header'; +} + +/** + * The accordion contents ar parts of each accordion item. + */ + +// Accordion Item Header Content +@Directive({ + standalone: false, + selector: '[pi-accordion-item-header-content]', +}) +export class PiAccordionItemHeaderDirective { + constructor(public templateRef: TemplateRef) {} +} +// Accordion Item Summary Content +@Directive({ + standalone: false, + selector: '[pi-accordion-item-summary-content]', +}) +export class PiAccordionSummaryContentDirective { + constructor(public templateRef: TemplateRef) {} +} + +// Accordion Item Expandable Content +@Directive({ + standalone: false, + selector: '[pi-accordion-item-expandable-content]', +}) +export class PiAccordionItemContentDirective { + constructor(public templateRef: TemplateRef) {} +} + +/** + * The accordion footer is a separated content of the accordion items group. + * If it is needed it must be used as a Footer for the whole accordion. + */ +@Directive({ + standalone: false, + selector: 'pi-accordion-footer', +}) +export class PiAccordionFooterDirective { + @HostBinding('class') className = 'pi-accordion-footer'; +} + +@Directive({ + standalone: false, + selector: 'pi-accordion-item', +}) +export class PiAccordionItemDirective { + @Input() title = ''; + @Input() subtitle = ''; + @Input() status!: status; + @Input() statusLabel: string; + @Input() statusIcon: string; + @Input() isConfidential = false; + @ContentChild(PiAccordionItemContentDirective) content: PiAccordionItemContentDirective; + @ContentChild(PiAccordionSummaryContentDirective) + summaryContent: PiAccordionSummaryContentDirective; + @ContentChild(PiAccordionItemHeaderDirective) customHeader: PiAccordionItemHeaderDirective; +} diff --git a/libs/ui/src/lib/ui/accordion/index.ts b/libs/ui/src/lib/ui/accordion/index.ts new file mode 100644 index 0000000..7c8b4e8 --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/index.ts @@ -0,0 +1,5 @@ +export * from './accordion.module'; + +export * from './accordion/accordion.component'; + +export * from './directives/accordion-content.directive'; \ No newline at end of file diff --git a/libs/ui/src/lib/ui/accordion/styles/_horizontal-accordion.scss b/libs/ui/src/lib/ui/accordion/styles/_horizontal-accordion.scss new file mode 100644 index 0000000..f0705ca --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/styles/_horizontal-accordion.scss @@ -0,0 +1,137 @@ +@mixin horizontalAccordionTypeOutlineColor($color) { + &.pi-accordion__appearance--outline { + & > section { + & > header { + border: 1px solid; + border-color: var(--pi-color-#{$color}-75); + background-color: var(--pi-color-#{$color}-25); + &.is-expanded { + background-color: var(--pi-color-grey-10); + border-color: var(--pi-color-grey-25); + } + } + } + } +} + +:host { + &.pi-accordion__direction--horizontal { + & > section { + & > header { + gap: var(--pi-spacing-large); + padding: var(--pi-spacing-x-small) var(--pi-spacing-small) var(--pi-spacing-x-small); + border-radius: var(--pi-border-radius-large); + &.is-expanded { + border: 1px solid; + } + &.is-disabled { + opacity: 0.3; + } + &:not(.is-disabled) { + &:hover { + cursor: pointer; + background-color: var(--pi-color-grey-10); + .pi-icon { + transform: scale(1.15); + } + } + .pi-icon { + &:hover { + cursor: pointer; + } + } + } + } + } + & > *:nth-child(1) { + justify-content: space-between; + gap: var(--pi-spacing-small); + } + main { + &.is-collapsed { + height: 0; + } + &.is-expanded { + height: 100%; + } + } + // Color + &.pi-accordion__primary { + @include horizontalAccordionTypeOutlineColor(primary); + } + &.pi-accordion__warn { + @include horizontalAccordionTypeOutlineColor(warn); + } + &.pi-accordion__accent { + @include horizontalAccordionTypeOutlineColor(accent); + } + // Horizontal, Outline & White + &.pi-accordion__white { + & > section { + & > header { + border: 1px solid; + border-color: var(--pi-color-grey-25); + &.is-expanded { + background-color: var(--pi-color-grey-10); + border: 1px solid; + border-color: var(--pi-color-grey-75); + } + } + } + } + } +} + +html { + // Desktop + &.is-desktop { + // Desktop S & M + &:not(.is-desktop__large) { + :host { + &.pi-accordion__direction--horizontal { + & > section { + &:nth-child(1) { + display: grid; + grid-template-columns: repeat(5, 1fr); + align-items: start; + header { + min-height: 6.5rem; + } + } + } + } + } + } + // Desktop L + &.is-desktop__large { + :host { + &.pi-accordion__direction--horizontal { + display: flex; + flex-direction: row; + gap: var(--pi-spacing-x-large); + & > section { + &:nth-child(1) { + display: flex; + flex-direction: column; + width: 25%; + } + &:nth-child(2) { + width: 75%; + } + & > header { + align-items: center; + } + } + + &.pi-accordion__white { + & > section { + & > header { + border-color: transparent; + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/accordion/styles/_vertical-accordion.scss b/libs/ui/src/lib/ui/accordion/styles/_vertical-accordion.scss new file mode 100644 index 0000000..c72f0c4 --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/styles/_vertical-accordion.scss @@ -0,0 +1,236 @@ +@mixin verticalAccordionPadding($large-spacing, $small-spacing) { + &:not(.pi-accordion__direction--horizontal) { + & > .pi-accordion-item { + .pi-accordion-item-content__header { + padding: var(--pi-spacing-#{$large-spacing}); + transform: var(--pi-animation); + } + .pi-accordion-item-content__header + .pi-accordion-item-content__summary { + padding: var(--pi-spacing-#{$small-spacing}) var(--pi-spacing-#{$large-spacing}) + var(--pi-spacing-#{$large-spacing}); + } + // Expanded [header + summary + main] + .pi-accordion-item-content__header.is-expanded + + .pi-accordion-item-content__summary + + .pi-accordion-item-content__expandable, + // Expanded [header + main] + .pi-accordion-item-content__header.is-expanded + .pi-accordion-item-content__expandable { + padding: 0 var(--pi-spacing-#{$large-spacing}) var(--pi-spacing-#{$large-spacing}); + } + // Collapsed [header + summary + main] + .pi-accordion-item-content__header:not(.is-expanded) + + .pi-accordion-item-content__summary + + .pi-accordion-item-content__expandable, + // Collapsed [header + main] + .pi-accordion-item-content__header:not(.is-expanded) + .pi-accordion-item-content__expandable { + padding: 0 var(--pi-spacing-#{$large-spacing}); + } + } + } +} + +@mixin verticalAccordionAppearanceOutline { + &.pi-accordion__appearance--outline { + &:not(.pi-accordion__direction--horizontal) { + &:not(.is-disabled) { + & > .pi-accordion-item { + & > .pi-accordion-item-content__header { + &.is-expanded { + border-bottom: 1px solid; + border-top-left-radius: var(--pi-border-radius-large); + border-top-right-radius: var(--pi-border-radius-large); + } + &:not(.is-expanded) { + border-radius: var(--pi-border-radius-large); + } + } + & > .pi-accordion-item-content__expandable { + border-bottom-left-radius: var(--pi-border-radius-large); + border-bottom-right-radius: var(--pi-border-radius-large); + } + } + } + &.pi-accordion__white { + &:not(.is-disabled) { + & > .pi-accordion-item { + & > .pi-accordion-item-content__header { + &.is-expanded { + background-color: var(--pi-color-grey-10); + border-color: var(--pi-color-grey-75); + } + } + &:hover { + & > .pi-accordion-item-content__header { + &:not(.is-expanded) { + background-color: var(--pi-color-grey-10); + border-color: var(--pi-color-grey-75); + } + } + } + } + } + } + } + } +} + +@mixin verticalAccordionAppearanceOutlineAndFill { + &.pi-accordion__appearance--outline, + &.pi-accordion__appearance--fill { + &:not(.pi-accordion__direction--horizontal) { + & > section { + & > header { + transform: var(--pi-animation); + } + } + // TODO: any generic property should be previously agreed with the design Team. + // Accordion component will use just 2 spacing properties. + &.pi-accordion__padding--s { + @include verticalAccordionPadding(small, xx-small); + } + &.pi-accordion__padding--m { + @include verticalAccordionPadding(medium, x-small); + } + &.pi-accordion__padding--l { + @include verticalAccordionPadding(large, small); + } + &.pi-accordion__padding--xl { + @include verticalAccordionPadding(x-large, medium); + } + &.pi-accordion__padding--xxl { + @include verticalAccordionPadding(xx-large, large); + } + } + } +} + +@mixin verticalAccordionAppearanceOutlineColor($color) { + &.pi-accordion__appearance--outline { + &:not(.pi-accordion__direction--horizontal) { + &:not(.is-disabled) { + & > .pi-accordion-item { + & > .pi-accordion-item-content__header { + &.is-expanded { + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-100); + } + } + &:hover { + & > .pi-accordion-item-content__header { + &:not(.is-expanded) { + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-100); + } + } + } + } + } + } + } +} + +@mixin verticalAccordionTopbar() { + .pi-accordion-item-content { + &.pi-accordion-item-content__topbar { + display: flex; + justify-content: center; + align-items: center; + height: var(--pi-spacing-xx-large); + border-top-left-radius: calc(var(--pi-border-radius-large) - 0.0625rem); + border-top-right-radius: calc(var(--pi-border-radius-large) - 0.0625rem); + transition: var(--pi-animation); + &.pi-color__primary { + background-color: var(--pi-color-primary-50); + } + &.pi-color__highlighted { + background-color: var(--pi-color-highlighted-50); + } + &.pi-color__grey { + background-color: var(--pi-color-grey-50); + } + } + } +} + +:host { + // Appearance (Appearance) + @include verticalAccordionAppearanceOutline; + + @include verticalAccordionAppearanceOutlineAndFill; + + @include verticalAccordionTopbar; + + // Appearance (Appearance) + Color + &.pi-accordion__primary { + @include verticalAccordionAppearanceOutlineColor(primary); + } + &.pi-accordion__warn { + @include verticalAccordionAppearanceOutlineColor(warn); + } + &.pi-accordion__accent { + @include verticalAccordionAppearanceOutlineColor(accent); + } + + // Default (Vertical) + &:not(.pi-accordion__direction--horizontal) { + // Accordion item + & > .pi-accordion-item { + &:not(.is-disabled) { + & > .pi-accordion-item-content__header { + align-items: flex-start; + &:hover { + cursor: pointer; + } + .pi-icon { + &:hover { + cursor: pointer; + } + } + } + } + & > .pi-accordion-item-content__expandable { + transition: var(--pi-animation); + } + // Expanded + .pi-accordion-item-content__header { + &.is-expanded { + & > .pi-accordion-item-header-content__right { + .pi-icon { + transform: rotate(90deg); + } + } + } + } + .pi-accordion-item-content__header.is-expanded + .pi-accordion-item-content__expandable, + .pi-accordion-item-content__header.is-expanded + + .pi-accordion-item-content__summary + + .pi-accordion-item-content__expandable { + height: 100%; + margin-top: var(--pi-spacing-medium); + opacity: 1; + visibility: visible; + } + + // Collapsed + .pi-accordion-item-content__header { + &.is-collapsed { + & > .pi-accordion-item-header-content__right { + .pi-icon { + transform: rotate(0); + } + } + } + } + .pi-accordion-item-content__header.is-collapsed + .pi-accordion-item-content__expandable, + .pi-accordion-item-content__header.is-collapsed + + .pi-accordion-item-content__summary + + .pi-accordion-item-content__expandable { + height: auto; + flex: 1; + max-height: 0; + opacity: 0; + visibility: hidden; + } + } + } +} diff --git a/libs/ui/src/lib/ui/accordion/styles/accordion.component.scss b/libs/ui/src/lib/ui/accordion/styles/accordion.component.scss new file mode 100644 index 0000000..f6aec0d --- /dev/null +++ b/libs/ui/src/lib/ui/accordion/styles/accordion.component.scss @@ -0,0 +1,186 @@ +@use 'horizontal-accordion'; +@use 'vertical-accordion'; + +// Appearance Default +@mixin accordionAppearanceDefault { + &:not(.pi-accordion__appearance--outline):not(.pi-accordion__appearance--fill) { + // Not horizontal + &:not(.pi-accordion__direction--horizontal) { + & > section { + border-bottom: 1px solid; + border-color: var(--pi-color-grey-25); + } + } + & > section { + padding: var(--pi-spacing-x-large) var(--pi-spacing-xx-small) var(--pi-spacing-medium); + } + } +} + +// Appearance Outline +@mixin accordionAppearanceOutline { + &.pi-accordion__appearance--outline { + // Color + &.pi-accordion__primary { + @include accordionAppearanceOutlineColor(primary); + } + &.pi-accordion__warn { + @include accordionAppearanceOutlineColor(warn); + } + &.pi-accordion__accent { + @include accordionAppearanceOutlineColor(accent); + } + &.pi-accordion__white { + & > section { + border-color: var(--pi-color-white-900); + background-color: var(--pi-color-white); + } + } + } +} + +// Appearance Fill +@mixin accordionAppearanceFill { + &.pi-accordion__appearance--fill { + // Color + &.pi-accordion__primary { + @include accordionAppearanceFilleColor(primary); + } + &.pi-accordion__warn { + @include accordionAppearanceFilleColor(warn); + } + &.pi-accordion__accent { + @include accordionAppearanceFilleColor(accent); + } + + &.pi-accordion__white { + & > section { + background-color: var(--pi-color-white); + } + } + } +} + +// Fill & Outline spacings +@mixin accordionFillAndOutlineSpacings { + &.pi-accordion__appearance--outline, + &.pi-accordion__appearance--fill { + gap: var(--pi-spacing-large); + & > section { + border-radius: var(--pi-border-radius-large); + } + header { + aside { + & > .pi-icon { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + border-radius: var(--pi-border-radius-small); + background-color: var(--pi-color-white); + } + } + } + } +} + +// Appearance Fill Color +@mixin accordionAppearanceFilleColor($color) { + & > section { + background-color: var(--pi-color-#{$color}-25); + } +} + +// Appearance Outline Color +@mixin accordionAppearanceOutlineColor($color) { + & > section { + border: 1px solid; + border-color: var(--pi-color-#{$color}-75); + background-color: var(--pi-color-#{$color}-25); + } +} + +// Common +:host { + max-width: 100%; + display: flex; + flex-direction: column; + + // Accordion item + & > section { + header { + display: flex; + justify-content: space-between; + + & > aside { + display: flex; + align-items: center; + gap: var(--pi-spacing-medium); + height: 1.875rem; + width: fit-content; + & > span { + &::after { + content: '|'; + color: var(--pi-color-grey-75); + font: { + size: var(--pi-font-size-small); + } + } + } + + .pi-icon { + transition: var(--pi-animation); + } + } + & > .pi-text-viewer { + & > article { + & > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } + + // Content + & > main { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-x-large); + height: auto; + overflow: hidden; + } + // State + &.is-disabled { + & > *:nth-child(n + 1) { + opacity: 0.33; + } + } + + // Is confidential + &.is-confidential { + &.is-expanded { + border: 1px solid; + border-color: var(--pi-color-primary-100); + } + &.is-collapsed { + border: 1px solid; + border-color: var(--pi-color-grey-100); + } + } + } + + // Direction + + // Appearance (Appearance) + // Default + @include accordionAppearanceDefault; + + // Outline + @include accordionAppearanceOutline; + + // Fill + @include accordionAppearanceFill; + + // Fill & Outline spacings + @include accordionFillAndOutlineSpacings; +} diff --git a/libs/ui/src/lib/ui/banner/banner.module.ts b/libs/ui/src/lib/ui/banner/banner.module.ts new file mode 100644 index 0000000..478611a --- /dev/null +++ b/libs/ui/src/lib/ui/banner/banner.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiBannerComponent } from './banner/banner.component'; + +// Shared +import { PiSharedUITextModule } from '../text'; +import { PiSharedUIWrapperModule } from '../wrapper'; +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIImageModule } from '../image'; + +@NgModule({ + imports: [CommonModule, PiSharedUITextModule, PiSharedUIWrapperModule, PiSharedUtilColorModule, PiSharedUIButtonModule, PiSharedUIImageModule], + declarations: [PiBannerComponent], + exports: [PiBannerComponent], +}) +export class PiSharedUIBannerModule {} diff --git a/libs/ui/src/lib/ui/banner/banner/banner.component.html b/libs/ui/src/lib/ui/banner/banner/banner.component.html new file mode 100644 index 0000000..776d703 --- /dev/null +++ b/libs/ui/src/lib/ui/banner/banner/banner.component.html @@ -0,0 +1,27 @@ +
+
+
+

{{ title }}

+ {{ message }} +
+ + {{ buttonText }} +
+
diff --git a/libs/ui/src/lib/ui/banner/banner/banner.component.scss b/libs/ui/src/lib/ui/banner/banner/banner.component.scss new file mode 100644 index 0000000..5a6dd77 --- /dev/null +++ b/libs/ui/src/lib/ui/banner/banner/banner.component.scss @@ -0,0 +1,138 @@ +:host { + display: block; + width: 100%; + border-radius: var(--pi-border-radius-large); + + & > main { + gap: var(--pi-spacing-xxx-small); + & > section { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-x-large); + padding: var(--pi-spacing-medium) var(--pi-spacing-x-large); + & > header { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-xxx-small); + } + } + } + &:hover { + cursor: pointer; + } + + // Type + &.pi-banner__mode--dark { + background: linear-gradient(60deg, var(--pi-color-highlighted), var(--pi-color-primary)); + & > main { + & > section { + h3 { + color: var(--pi-color-white); + } + span { + color: var(--pi-color-white); + } + } + } + } + &.pi-banner__mode--light { + background: linear-gradient(60deg, var(--pi-color-white), var(--pi-color-primary-100)); + & > main { + & > section { + h3 { + color: var(--pi-color-grey-500); + } + span { + color: var(--pi-color-grey-400); + } + } + } + } +} + +// Media queries +html { + // Mobile + &.is-mobile { + :host { + & > main { + height: 10rem; + + & > section { + width: 60%; + padding: var(--pi-spacing-medium); + & > header { + justify-content: space-between; + h3 { + font: { + size: var(--pi-font-size-medium); + } + } + span { + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + } + } + + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + :host { + .pi-icon-button { + display: none; + } + } + } + } + + &:not(.is-mobile) { + // large device + @media screen and (min-width: 62rem) { + :host { + & > main { + & > section { + & > header { + h3 { + font: { + size: var(--pi-font-size-large); + } + } + span { + font: { + size: var(--pi-font-size-small); + } + } + } + } + } + } + } + } + + // Tablet + &.is-tablet { + :host { + & > main { + height: 9.6875rem; + & > section { + width: 66.66%; + } + } + } + } + &.is-desktop { + :host { + & > main { + height: 9.375rem; + & > section { + width: 75%; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/banner/banner/banner.component.ts b/libs/ui/src/lib/ui/banner/banner/banner.component.ts new file mode 100644 index 0000000..49205e2 --- /dev/null +++ b/libs/ui/src/lib/ui/banner/banner/banner.component.ts @@ -0,0 +1,29 @@ +import { Component, EventEmitter, HostBinding, Input, Output } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'pi-banner', + templateUrl: './banner.component.html', + styleUrls: ['./banner.component.scss'], +}) +export class PiBannerComponent { + @HostBinding('class') get classes(): string { + const modeClass = `pi-banner__mode--${this.mode}`; + return ['pi-banner', modeClass].join(' '); + } + + // Skin + @Input() mode: 'dark' | 'light' = 'dark'; + + // Text + @Input() title: string | undefined; + @Input() message: string | undefined; + @Input() buttonText: string | undefined; + + // Image + @Input() pathImg: string | undefined; + @Input() isImgResponsive: boolean = true; + @Input() extension: 'jpg' | 'svg' | 'gif' = 'gif'; + + @Output() execute: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/banner/index.ts b/libs/ui/src/lib/ui/banner/index.ts new file mode 100644 index 0000000..0a30991 --- /dev/null +++ b/libs/ui/src/lib/ui/banner/index.ts @@ -0,0 +1,3 @@ +export * from './banner.module'; + +export * from './banner/banner.component'; diff --git a/libs/ui/src/lib/ui/button/action-button/action-button.component.html b/libs/ui/src/lib/ui/button/action-button/action-button.component.html new file mode 100644 index 0000000..6125511 --- /dev/null +++ b/libs/ui/src/lib/ui/button/action-button/action-button.component.html @@ -0,0 +1,31 @@ + diff --git a/libs/ui/src/lib/ui/button/action-button/action-button.component.scss b/libs/ui/src/lib/ui/button/action-button/action-button.component.scss new file mode 100644 index 0000000..7c128a6 --- /dev/null +++ b/libs/ui/src/lib/ui/button/action-button/action-button.component.scss @@ -0,0 +1,140 @@ +@mixin actionButtonChevronSize($icon-height, $icon-width, $head-size) { + svg { + height: $icon-height; + width: $icon-width; + + // Chevron head + & > g { + &:nth-child(1) { + height: $head-size; + width: $head-size; + } + } + } +} + +@mixin actionButtonFontWeight($weight) { + & > button { + & > span { + font: { + weight: $weight; + } + } + } +} + +:host { + display: block; + + & > button { + all: unset; + display: flex; + align-items: center; + justify-content: center; + // Size + // X Small + &.pi-button__x-small { + @include actionButtonChevronSize( + var(--pi-spacing-x-small), + var(--pi-spacing-small), + var(--pi-spacing-xx-small) + ); + gap: var(--pi-spacing-xxxx-small); + } + // Small + &.pi-button__small { + @include actionButtonChevronSize( + var(--pi-spacing-x-small), + var(--pi-spacing-small), + var(--pi-spacing-xx-small) + ); + gap: var(--pi-spacing-xxx-small); + } + // Medium + &.pi-button__medium { + @include actionButtonChevronSize( + var(--pi-spacing-small), + var(--pi-spacing-medium), + var(--pi-spacing-x-small) + ); + gap: var(--pi-spacing-xxx-small); + } + // Large & w100 + &.pi-button__large, + &.pi-button__w100 { + @include actionButtonChevronSize( + var(--pi-spacing-small), + var(--pi-spacing-medium), + var(--pi-spacing-x-small) + ); + gap: var(--pi-spacing-xx-small); + } + } + svg { + transition: all 100ms ease-in-out; + opacity: 1; + transform: scaleX(1); + transition: transform 100ms ease-in-out, opacity 100ms ease-in-out; + & > g { + // Head + &:nth-child(1) { + transform: translateX(0); + transition: transform 100ms ease-in-out; + } + // Body + &:nth-child(2) { + opacity: 0; + transform: scaleX(1); + transition: transform 100ms ease-in-out, opacity 100ms ease-in-out; + } + } + } + + &:hover { + // Icon + svg { + transition: all 100ms ease-in-out; + opacity: 1; + transform: scaleX(1); + transition: transform 100ms ease-in-out, opacity 100ms ease-in-out; + & > g { + // Head + &:nth-child(1) { + transform: translateX(3px); + } + // Body + &:nth-child(2) { + opacity: 1; + transform: scaleX(1.75); + } + } + } + & > button { + cursor: pointer; + background-color: var(--pi-color-white); + } + } + // Direction + // Back + &.pi-action-button__direction--back { + & > button { + flex-direction: row-reverse; + svg { + transform: rotate(-180deg); + } + } + } + // Font weight + // 300 + &.pi-action-button__font-weight--300 { + @include actionButtonFontWeight(300); + } + // 400 + &.pi-action-button__font-weight--400 { + @include actionButtonFontWeight(400); + } + // 600 + &.pi-action-button__font-weight--600 { + @include actionButtonFontWeight(600); + } +} diff --git a/libs/ui/src/lib/ui/button/action-button/action-button.component.ts b/libs/ui/src/lib/ui/button/action-button/action-button.component.ts new file mode 100644 index 0000000..e43d919 --- /dev/null +++ b/libs/ui/src/lib/ui/button/action-button/action-button.component.ts @@ -0,0 +1,69 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { buttonSize, mode } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-action-button', + templateUrl: './action-button.component.html', + styleUrls: ['./action-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiActionButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + return ['pi-action-button', this.sizeClass, this.directionClass, this.fontWeightClass].join( + ' ' + ); + } + + @Input() previous = false; + @Input() override size: buttonSize = 'medium'; + @Input() text: string | undefined; + @Input() hasPadding = true; + @Input() hasIcon = true; + @Input() label: string | undefined; + @Input() buttonType: 'default' | 'menu-item' = 'default'; + @Input() mode: mode = 'dark'; + @Input() direction: 'back' | 'forward' = 'forward'; + @Input() fontWeight: '300' | '400' | '600' = '600'; + + get buttonColorClass(): string | null { + return this.color ? `pi-color__${this.color}` : null; + } + + get directionClass(): string | null { + return this.direction ? `pi-action-button__direction--${this.direction}` : null; + } + + override get sizeClass(): string | null { + return this.size ? `pi-button__${this.size}` : null; + } + + get textClass(): string | null { + return this.text ? 'has-text' : null; + } + + get fontWeightClass(): string | null { + return this.fontWeight ? `pi-action-button__font-weight--${this.fontWeight}` : ''; + } + + get hasPaddingClass(): string | null { + return this.hasPadding ? null : 'pi-button__trimmed'; + } + + get hasIconClass(): string | null { + return this.hasIcon ? null : 'has-icon'; + } + + override get appearanceClass(): string | null { + return this.appearance !== 'fill' ? `pi-button__appearance--${this.appearance}` : null; + } + + get buttonTypeClass(): string | null { + return this.buttonType !== 'default' ? `pi-button__type--${this.buttonType}` : null; + } + + get modeClass(): string | null { + return this.mode && `pi-color__mode--${this.mode}`; + } +} diff --git a/libs/ui/src/lib/ui/button/base-button.component.ts b/libs/ui/src/lib/ui/button/base-button.component.ts new file mode 100644 index 0000000..0fa58d5 --- /dev/null +++ b/libs/ui/src/lib/ui/button/base-button.component.ts @@ -0,0 +1,51 @@ +// Angular +import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { buttonSize, styleType, buttonStyle, flexDirection } from '../../styles'; + +// Style definitions +@Directive() +export abstract class PiBaseButtonComponent implements OnInit { + @Input() size: buttonSize = 'medium'; + @Input() color: styleType = 'primary'; + @Input() appearance: buttonStyle = 'fill'; + @Input() disabled = false; + @Input() allowPropagation!: boolean; + @Input() flexDirection!: flexDirection; + + @Output() execute: EventEmitter = new EventEmitter(); + + ngOnInit(): void { + this.sizeClass; + this.flexDirectionClass; + } + + protected get colorClass(): string { + return `pi-color__${this.color}`; + } + + protected get sizeClass(): string { + return `pi-button__${this.size}`; + } + + protected get appearanceClass(): string { + return `pi-button__appearance--${this.appearance}`; + } + + protected get flexDirectionClass(): string { + return `pi-button__flex-direction--${this.flexDirection}`; + } + + protected get disabledClass(): string { + return 'is-disabled'; + } + + onClickHandler(event: any): void { + if (!this.disabled) { + if (!this.allowPropagation) { + event.preventDefault(); + event.stopPropagation(); + } + this.execute.emit(event); + } + } +} diff --git a/libs/ui/src/lib/ui/button/button.directive.ts b/libs/ui/src/lib/ui/button/button.directive.ts new file mode 100644 index 0000000..9e5f486 --- /dev/null +++ b/libs/ui/src/lib/ui/button/button.directive.ts @@ -0,0 +1,103 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { buttonSize } from '../../styles'; + +// Style +@Directive({ + standalone: false, + selector: '[pi-ghost]', +}) +export class PiGhostButtonDirective { + @HostBinding('class') get classes(): string { + const hasPaddingClass = this.hasPadding ? '' : 'pi-button__trimmed'; + return ['pi-button', 'pi-button__appearance--ghost', hasPaddingClass].join(' '); + } + @Input() hasPadding = true; +} + +@Directive({ + standalone: false, + selector: '[pi-outline]', +}) +export class PiOutlineButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__appearance--outline'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-button-size]', +}) +export class PiButtonSizeDirective { + @HostBinding('class') get classes(): string { + return this.sizeClass; + } + + @Input() size!: buttonSize; + + protected get sizeClass(): string { + return !!this.size && `pi-button__${this.size}`; + } +} + +// Size +@Directive({ + standalone: false, + selector: '[xx-small]', +}) +export class PiXXSmallButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__xx-small'; + } +} + +// Size +@Directive({ + standalone: false, + selector: '[x-small]', +}) +export class PiXSmallButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__x-small'; + } +} + +@Directive({ + standalone: false, + selector: '[small]', +}) +export class PiSmallButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__small'; + } +} + +@Directive({ + standalone: false, + selector: '[medium]', +}) +export class PiMediumButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__medium'; + } +} + +@Directive({ + standalone: false, + selector: '[large]', +}) +export class PiLargeButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__large'; + } +} + +@Directive({ + standalone: false, + selector: '[w100]', +}) +export class PiW100ButtonDirective { + @HostBinding('class') get classes(): string { + return 'pi-button pi-button__w100'; + } +} diff --git a/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.html b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.html new file mode 100644 index 0000000..8f95a2d --- /dev/null +++ b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.html @@ -0,0 +1 @@ + diff --git a/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.scss b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.scss new file mode 100644 index 0000000..f790186 --- /dev/null +++ b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.scss @@ -0,0 +1,37 @@ +:host { + display: flex; + justify-content: center; + align-items: center; + transition: all 0.35s linear; + transition-delay: 0.1s; + + &.pi-button__medium { + height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + .mat-icon { + width: var(--pi-spacing-small); + } + } + + &:hover { + cursor: pointer; + } + + &.is-expanded { + transform: rotate(180deg); + } + + &.pi-color__primary { + background-color: var(--pi-color-primary-50); + border: 1px solid var(--pi-color-primary-100); + .mat-icon { + color: var(--pi-color-primary); + } + &:hover { + border: 1px solid var(--pi-color-highlighted-200); + .mat-icon { + color: var(--pi-color-highlighted-200); + } + } + } +} diff --git a/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.ts b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.ts new file mode 100644 index 0000000..90f3fd5 --- /dev/null +++ b/libs/ui/src/lib/ui/button/chevron-button/chevron-button.component.ts @@ -0,0 +1,27 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-chevron-button', + templateUrl: './chevron-button.component.html', + styleUrls: ['./chevron-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChevronButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + return ['pi-chevron-button', ComponentFamilyClass.Button, this.colorClass, this.sizeClass].join( + ' ' + ); + } + + @Output() toggle: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/button/close-button/close-button.component.html b/libs/ui/src/lib/ui/button/close-button/close-button.component.html new file mode 100644 index 0000000..0241d53 --- /dev/null +++ b/libs/ui/src/lib/ui/button/close-button/close-button.component.html @@ -0,0 +1 @@ + diff --git a/libs/ui/src/lib/ui/button/close-button/close-button.component.scss b/libs/ui/src/lib/ui/button/close-button/close-button.component.scss new file mode 100644 index 0000000..2b9b069 --- /dev/null +++ b/libs/ui/src/lib/ui/button/close-button/close-button.component.scss @@ -0,0 +1,140 @@ +// Close button +:host { + display: flex; + justify-content: center; + cursor: pointer; + + &::after, + &::before { + content: ''; + position: absolute; + height: 0.085rem; + margin-top: var(--pi-spacing-small); + background-color: var(--pi-color-grey); + transition: all 0.22s ease-in; + } + + &::after { + transform: rotate(45deg); + } + + &::before { + transform: rotate(-45deg); + } + label { + position: absolute; + color: var(--pi-color-primary-900); + text-transform: uppercase; + letter-spacing: var(--pi-spacing-xxx-small); + transition: all 0.22s ease-in; + transition-delay: 0.11s; + opacity: 0; + } + + &:hover { + &::after { + transform: rotate(-45deg); + background-color: var(--pi-color-primary-900); + } + + &::before { + transform: rotate(45deg); + background-color: var(--pi-color-primary-900); + } + + label { + opacity: 1; + } + } + // Sizes + &.pi-button__small { + label { + margin: var(--pi-spacing-x-large) 0 0 0; + font: { + size: 0.55rem; + } + } + } + &.pi-button__medium { + label { + margin: var(--pi-spacing-xx-large) 0 0 0; + font: { + size: var(--pi-font-size-xx-small); + } + } + } + &.pi-button__large { + label { + margin: var(--pi-spacing-xxx-large) 0 0 0; + font: { + size: var(--pi-font-size-xx-small); + } + } + } + &.pi-close-button__single { + label { + display: none; + } + } +} + +html { + &.is-mobile { + :host { + // Sizes + &.pi-button__small { + width: var(--pi-spacing-x-large); + height: var(--pi-spacing-x-large); + &::after, + &::before { + width: var(--pi-spacing-medium); + } + } + &.pi-button__medium { + width: var(--pi-spacing-xx-large); + height: var(--pi-spacing-xx-large); + &::after, + &::before { + width: var(--pi-spacing-large); + } + } + &.pi-button__large { + width: var(--pi-spacing-xx-large); + height: var(--pi-spacing-xx-large); + &::after, + &::before { + width: var(--pi-spacing-x-large); + } + } + } + } + &:not(.is-mobile) { + :host { + // Sizes + &.pi-button__small { + width: var(--pi-spacing-large); + height: var(--pi-spacing-large); + &::after, + &::before { + width: var(--pi-spacing-medium); + } + } + &.pi-button__medium { + width: var(--pi-spacing-x-large); + height: var(--pi-spacing-x-large); + &::after, + &::before { + width: var(--pi-spacing-x-large); + } + } + &.pi-button__large { + width: var(--pi-spacing-xx-large); + height: var(--pi-spacing-xx-large); + &::after, + &::before { + width: var(--pi-spacing-xx-large); + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/button/close-button/close-button.component.ts b/libs/ui/src/lib/ui/button/close-button/close-button.component.ts new file mode 100644 index 0000000..f8584c1 --- /dev/null +++ b/libs/ui/src/lib/ui/button/close-button/close-button.component.ts @@ -0,0 +1,29 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-close-button', + templateUrl: './close-button.component.html', + styleUrls: ['./close-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiCloseButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + const hasLabelClass = this.hasLabel === false ? 'pi-close-button__single' : ''; + return ['pi-close-button', ComponentFamilyClass.Button, this.sizeClass, hasLabelClass].join( + ' ' + ); + } + + @Input() hasLabel = true; + @Output() toggle: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/button/hamburger/hamburger.component.html b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.html new file mode 100644 index 0000000..ca95102 --- /dev/null +++ b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.html @@ -0,0 +1,6 @@ +
+ + + +
+ diff --git a/libs/ui/src/lib/ui/button/hamburger/hamburger.component.scss b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.scss new file mode 100644 index 0000000..12593cc --- /dev/null +++ b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.scss @@ -0,0 +1,75 @@ +:host { + div { + span { + display: block; + height: var(--pi-spacing-xxxx-small); + width: var(--pi-spacing-xx-large); + margin: 0.35rem auto; + border-radius: 0.0625rem; + transition: all 0.25s ease-in-out; + } + &:hover { + cursor: pointer; + } + } + label { + position: absolute; + margin: 0 -0.75rem 0; + color: var(--pi-color-primary-900); + text-transform: uppercase; + letter-spacing: var(--pi-spacing-xxx-small); + transition: all 0.22s ease-in; + transition-delay: 0.11s; + opacity: 0; + font: { + size: 0.55rem; + } + } + &.is-open { + div { + span { + &:nth-child(odd) { + width: var(--pi-spacing-x-large); + } + &:nth-child(even) { + opacity: 0; + } + &:nth-child(1) { + transform: translateY(0.35rem) rotate(-45deg); + } + &:nth-child(3) { + transform: translateY(-0.6rem) rotate(45deg); + } + } + } + label { + opacity: 1; + } + } + // Primary + &.pi-color__primary { + div { + span { + background-color: var(--pi-color-primary); + } + &:hover { + span { + background-color: var(--pi-color-primary-900); + } + } + } + } + // Accent + &.pi-color__accent { + div { + span { + background-color: var(--pi-color-accent); + } + &:hover { + span { + background-color: var(--pi-color-accent-900); + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/button/hamburger/hamburger.component.ts b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.ts new file mode 100644 index 0000000..913ff78 --- /dev/null +++ b/libs/ui/src/lib/ui/button/hamburger/hamburger.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Output } from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-hamburger', + templateUrl: './hamburger.component.html', + styleUrls: ['./hamburger.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiHamburgerComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + return ['pi-hamburger', ComponentFamilyClass.Button, this.colorClass].join(' '); + } + @Output() toggle: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/button/icon-button/icon-button.component.html b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.html new file mode 100644 index 0000000..ec37cac --- /dev/null +++ b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.html @@ -0,0 +1,54 @@ + + + diff --git a/libs/ui/src/lib/ui/button/icon-button/icon-button.component.scss b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.scss new file mode 100644 index 0000000..75f06e2 --- /dev/null +++ b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.scss @@ -0,0 +1,189 @@ +@mixin iconButtonExpandCollapse($rotate) { + .mat-icon { + transform: rotate(#{$rotate}); + } + &:hover { + .mat-icon { + transform: scale(1.2) rotate(#{$rotate}); + } + } +} + +:host { + transition: all 0.2s ease; + + & > button { + display: flex; + align-items: center; + justify-content: center; + gap: var(--pi-spacing-xx-small); + &:not(:disabled) { + &:hover { + cursor: pointer; + } + } + + // Single + &.pi-button__icon--single { + height: var(--pi-spacing-xxx-large); + width: var(--pi-spacing-xxx-large); + } + &:not(.pi-button__icon--single) { + button { + text-decoration: underline; + } + } + & > img { + height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + } + } + // Has Before Label + &.pi-button__icon--before-label { + & > button { + justify-content: flex-end; + text-decoration: none; + font: { + size: unset; + } + & > small { + position: absolute; + opacity: 0; + transform: translateX(-2rem); + white-space: pre-wrap; + width: min-content; + text-align: right; + line-height: initial; + color: var(--pi-color-primary-200); + font: { + size: var(--pi-font-size-xx-small); + family: 'Soehne', Helvetica, Arial, sans-serif; + } + } + &:hover { + & > small { + transition: all 0.2s ease-in-out; + transition-delay: 0.05s; + transform: translateX(-1.5rem); + opacity: 1; + color: var(--pi-color-primary); + } + } + } + } + // Sizes + &.pi-button__x-small { + & > button { + // Single Icon + &.pi-button__icon--single { + button, + .mat-icon { + width: var(--pi-spacing-medium); + height: var(--pi-spacing-medium); + } + } + // Not Single Icon + &:not(.pi-button__icon--single) { + .mat-icon { + width: var(--pi-spacing-medium); + height: var(--pi-spacing-medium); + } + } + } + } + &.pi-button__small { + & > button { + // Single Icon + &.pi-button__icon--single { + button, + .mat-icon { + width: var(--pi-spacing-medium); + height: var(--pi-spacing-medium); + } + } + // Not Single Icon + &:not(.pi-button__icon--single) { + .mat-icon { + width: var(--pi-spacing-small); + height: var(--pi-spacing-small); + } + } + } + } + &.pi-button__medium { + & > button { + // Single Icon + &.pi-button__icon--single { + button, + .mat-icon { + width: var(--pi-spacing-large); + height: var(--pi-spacing-large); + } + } + // Not Single Icon + &:not(.pi-button__icon--single) { + .mat-icon { + width: 1.125rem; + height: 1.125rem; + } + } + } + } + &.pi-button__large { + & > button { + // Single Icon + &.pi-button__icon--single { + button, + .mat-icon { + width: var(--pi-spacing-xx-large); + height: var(--pi-spacing-xx-large); + } + } + // Not Single Icon + &:not(.pi-button__icon--single) { + .mat-icon { + width: var(--pi-spacing-x-large); + height: var(--pi-spacing-x-large); + } + } + } + } + + &.pi-button__x-small { + button { + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-medium); + font: { + size: 0.75rem; + } + } + } + + &.pi-button__w100 { + width: 100%; + } + .mat-icon { + display: flex; + transition: all 0.2s ease; + } + + &:not(.is-expanded):not(.is-collapsed) { + & > button { + &:not(:disabled) { + &:hover { + .mat-icon, + img { + transform: scale(1.2); + } + } + } + } + } + + // States + &.is-collapsed { + @include iconButtonExpandCollapse(0); + } + &.is-expanded { + @include iconButtonExpandCollapse(90deg); + } +} diff --git a/libs/ui/src/lib/ui/button/icon-button/icon-button.component.ts b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.ts new file mode 100644 index 0000000..34b47a5 --- /dev/null +++ b/libs/ui/src/lib/ui/button/icon-button/icon-button.component.ts @@ -0,0 +1,71 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostBinding, + Input, + ViewChild, +} from '@angular/core'; +import { buttonSize, mode } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-icon-button', + templateUrl: './icon-button.component.html', + styleUrls: ['./icon-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiIconButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + const customIconClass = this.iconType === 'custom' && 'pi-icon-button__custom-icon'; + const isResponsiveClass = this.isResponsive ? 'is-responsive' : ''; + return [ + 'pi-icon-button', + this.sizeClass, + this.iconPositionClass, + this.widthClass, + customIconClass, + isResponsiveClass, + ].join(' '); + } + @ViewChild('content', { static: false }) + contentRef!: ElementRef; + + @Input() icon: string | undefined; + @Input() iconType: 'fill' | 'default' | 'custom' = 'default'; + @Input() previous = false; + @Input() width: buttonSize = 'medium'; + @Input() text: string | undefined; + @Input() hasPadding = true; + @Input() mode: mode = 'dark'; + @Input() isResponsive = false; + + get buttonTypeClass(): string | null { + return this.appearance ? `pi-button__appearance--${this.appearance}` : null; + } + + get buttonColorClass(): string | null { + return this.color ? `pi-color__${this.color}` : null; + } + + get widthClass(): string | null { + return this.width ? `pi-button__${this.width}` : null; + } + + get textClass(): string | null { + return this.text ? 'has-text' : 'pi-button__icon--single'; + } + + get iconPositionClass(): string | null { + return this.previous ? 'pi-button__icon--previous' : null; + } + + get hasPaddingClass(): string | null { + return this.hasPadding ? null : 'pi-button__trimmed'; + } + + get modeClass(): string | null { + return this.mode && `pi-color__mode--${this.mode}`; + } +} diff --git a/libs/ui/src/lib/ui/button/index.ts b/libs/ui/src/lib/ui/button/index.ts new file mode 100644 index 0000000..5987533 --- /dev/null +++ b/libs/ui/src/lib/ui/button/index.ts @@ -0,0 +1,17 @@ +// Export all buttons components +export * from './action-button/action-button.component'; +export * from './base-button.component'; +export * from './chevron-button/chevron-button.component'; +export * from './close-button/close-button.component'; +export * from './hamburger/hamburger.component'; +export * from './icon-button/icon-button.component'; +export * from './radio-button/radio-button.component'; +export * from './switch-button/switch-button.component'; +export * from './toggle-button/toggle-button.component'; + +// Export button's module +export * from './ui-button.module'; + +// Export button's directive +export * from './button.directive'; +export * from './switch-button/switch-button.directive'; diff --git a/libs/ui/src/lib/ui/button/radio-button/radio-button.component.html b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.html new file mode 100644 index 0000000..24976a0 --- /dev/null +++ b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.html @@ -0,0 +1,5 @@ + + + diff --git a/libs/ui/src/lib/ui/button/radio-button/radio-button.component.scss b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.scss new file mode 100644 index 0000000..772d96a --- /dev/null +++ b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.scss @@ -0,0 +1,78 @@ +@mixin radioButtonColor($color) { + border-color: var(--pi-color-#{$color}); + &.is-active { + background-color: var(--pi-color-#{$color}-25); + & > span { + background-color: var(--pi-color-#{$color}); + } + } + + & > .mat-icon { + color: var(--pi-color-#{$color}); + } +} +:host { + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border: 1px solid; + + &:hover { + cursor: pointer; + } + + &.is-disabled { + opacity: 0.5; + } + + // Color + &.pi-color__primary { + @include radioButtonColor(primary); + } + &.pi-color__accent { + @include radioButtonColor(accent); + } + &.pi-color__highlighted { + @include radioButtonColor(highlighted); + } + &.pi-color__grey { + @include radioButtonColor(grey-300); + } + &.pi-color__success { + @include radioButtonColor(success); + } + &.pi-color__warn { + @include radioButtonColor(warn); + } + + // Type default || radio + &:not(.pi-radio-button__icon--check) { + height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + & > span { + height: var(--pi-spacing-x-small); + width: var(--pi-spacing-x-small); + border-radius: 50%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + } + } + // Icon + // Check + &.pi-radio-button__icon--check { + &:not(.is-active) { + .mat-icon { + display: none; + } + } + } + + // Size + &.pi-button__small { + width: var(--pi-spacing-medium); + height: var(--pi-spacing-medium); + } +} diff --git a/libs/ui/src/lib/ui/button/radio-button/radio-button.component.ts b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.ts new file mode 100644 index 0000000..ea09218 --- /dev/null +++ b/libs/ui/src/lib/ui/button/radio-button/radio-button.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-radio-button', + templateUrl: './radio-button.component.html', + styleUrls: ['./radio-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiRadioButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + return [ + 'pi-radio-button', + this.colorClass, + this.iconClass, + this.isActiveClass, + this.sizeClass, + ].join(' '); + } + + @Input() icon: 'bullet' | 'check' = 'check'; + + // Status + @Input() isActive!: boolean; + @Input() isDisabled!: boolean; + + get iconClass(): string | null { + return this.icon === 'check' ? 'pi-radio-button__icon--check' : null; + } + + get isActiveClass(): string | null { + return this.isActive ? 'is-active' : null; + } +} diff --git a/libs/ui/src/lib/ui/button/switch-button/switch-button.component.html b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.html new file mode 100644 index 0000000..36a08aa --- /dev/null +++ b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.html @@ -0,0 +1,38 @@ +
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + {{ content }} + diff --git a/libs/ui/src/lib/ui/button/switch-button/switch-button.component.scss b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.scss new file mode 100644 index 0000000..c5a1733 --- /dev/null +++ b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.scss @@ -0,0 +1,133 @@ +@mixin textColor($color: var(--pi-color-grey)) { + div { + span { + color: $color; + } + } +} + +:host { + position: relative; + &.has-icon { + div { + width: 6rem; + border-radius: var(--pi-border-radius-large); + span { + width: 3rem; + &::before { + width: 3rem; + border-radius: var(--pi-border-radius-large); + } + &.is-active { + &::before { + background-color: var(--pi-color-primary-200); + } + .mat-icon { + color: var(--pi-color-white); + opacity: 1; + } + } + &:not(.is-active) { + .mat-icon { + color: var(--pi-color-primary); + } + } + } + .mat-icon { + height: var(--pi-spacing-large); + width: var(--pi-spacing-large); + } + } + &.pi-button__large { + .mat-icon { + height: 1.75rem; + width: 1.75rem; + } + } + } + &:not(.has-icon) { + div { + width: fit-content; + padding: var(--pi-spacing-xxx-small); + background-color: var(--pi-color-grey-25); + border-radius: var(--pi-border-radius-medium); + + span { + position: relative; + padding: var(--pi-spacing-small) var(--pi-spacing-large); + border-radius: var(--pi-border-radius-small); + @include textColor(); + &::before { + width: 100%; + bottom: 0; + left: 0; + background-color: var(--pi-color-white); + border-radius: var(--pi-border-radius-small); + } + + &.is-active { + span { + font-weight: 400; + } + } + &:not(.is-active) { + span { + font-weight: 300; + } + } + } + } + // Text color + // Primary + &.pi-color__primary { + @include textColor(var(--pi-color-primary)); + } + // Highlighted + &.pi-color__highlighted { + @include textColor(var(--pi-color-highlighted)); + } + // accent + &.pi-color__accent { + @include textColor(var(--pi-color-accent)); + } + // Warn + &.pi-color__warn { + @include textColor(var(--pi-color-warn)); + } + } + div { + display: flex; + span { + display: flex; + justify-content: center; + align-items: center; + height: var(--pi-spacing-xxx-large); + transition: all 0.3s cubic-bezier(0.77, 0, 0.125, 1) 0.0015s; + will-change: opacity; + &::before { + content: ''; + position: absolute; + z-index: -1; + height: var(--pi-spacing-xxx-large); + transition: all 0.3s cubic-bezier(0.77, 0, 0.125, 1) 0.0015s; + box-shadow: var(--pi-box-shadow); + } + + &.is-active { + &::before { + transform: translateX(0%); + opacity: 1; + } + } + &:not(.is-active) { + &::before { + transform: translateX(-100%); + opacity: 0; + } + } + &:hover { + cursor: pointer; + } + } + } +} diff --git a/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts new file mode 100644 index 0000000..6411fb4 --- /dev/null +++ b/libs/ui/src/lib/ui/button/switch-button/switch-button.component.ts @@ -0,0 +1,46 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-switch-button', + templateUrl: './switch-button.component.html', + styleUrls: ['./switch-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiSwitchButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + const contentTypeClass = this.contentType === 'icon' ? 'has-icon' : ''; + return [ + 'pi-switch-button', + ComponentFamilyClass.Button, + this.sizeClass, + this.colorClass, + contentTypeClass, + ].join(' '); + } + + @Input() isActive = true; + @Output() toggledTo = new EventEmitter(); + + @Input() contentType: 'icon' | 'text' = 'icon'; + @Input() content!: [string, string]; + + toggleSelected(): void { + if (this.isActive) { + this.isActive = false; + this.toggledTo.emit('off'); + } else { + this.isActive = true; + this.toggledTo.emit('on'); + } + } +} diff --git a/libs/ui/src/lib/ui/button/switch-button/switch-button.directive.ts b/libs/ui/src/lib/ui/button/switch-button/switch-button.directive.ts new file mode 100644 index 0000000..390100c --- /dev/null +++ b/libs/ui/src/lib/ui/button/switch-button/switch-button.directive.ts @@ -0,0 +1,35 @@ +import { Directive, HostBinding } from '@angular/core'; + +@Directive({ + standalone: false, + selector: 'pi-prefix-left', +}) +export class PiSwitchButtonPrefixLeftDirective { + @HostBinding('class') className = 'pi-switch-button__prefix--left'; +} + +@Directive({ + standalone: false, + selector: 'pi-suffix-left', +}) +export class PiSwitchButtonSuffixLeftDirective { + @HostBinding('class') className = 'pi-switch-button__suffix--left'; +} + +@Directive({ + standalone: false, + selector: 'pi-prefix-right', + }) + export class PiSwitchButtonPrefixRightDirective { + @HostBinding('class') className = 'pi-switch-button__prefix--right'; + } + + @Directive({ + standalone: false, + selector: 'pi-suffix-right', + }) + export class PiSwitchButtonSuffixRightDirective { + @HostBinding('class') className = 'pi-switch-button__suffix--right'; + } + + diff --git a/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.html b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.html new file mode 100644 index 0000000..87a3488 --- /dev/null +++ b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.html @@ -0,0 +1,9 @@ + + +{{ amount }} diff --git a/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.scss b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.scss new file mode 100644 index 0000000..de2c8ae --- /dev/null +++ b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.scss @@ -0,0 +1,166 @@ +:host { + display: flex; + flex-direction: row-reverse; + & > button { + display: flex; + justify-content: flex-start; + align-items: center; + height: 3.125rem; + white-space: pre-line; + text-align: left; + color: var(--pi-color-primary); + border-radius: var(--pi-border-radius-medium); + -webkit-border-radius: var(--pi-border-radius-medium); + -moz-border-radius: var(--pi-border-radius-medium); + line-height: 125%; + font: { + weight: 300; + } + } + & > span { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + height: 1.35rem; + width: 1.35rem; + transform: translateX(0.375rem); + margin-top: -0.375rem; + background-color: var(--pi-color-accent); + border-radius: 50%; + color: var(--pi-color-white); + line-height: 1.35rem; + small { + font: { + size: var(--pi-font-size-xx-small); + } + } + } + &:not(.is-active) { + & > button { + background-color: var(--pi-color-white); + border: 1px solid var(--pi-color-primary-50); + &:not(:disabled) { + &:hover { + cursor: pointer; + transition: var(--pi-animation); + .mat-icon { + transform: scale(1.025); + transition: var(--pi-animation); + } + } + } + } + &:hover { + & > button { + &:not(:disabled) { + background-color: var(--pi-color-primary-25); + border: 1px solid var(--pi-color-primary-200); + } + } + } + } + // Active + &.is-active { + & > button { + background-color: var(--pi-color-primary-25); + border: 1px solid var(--pi-color-primary-75); + } + } + // Disabled + &.is-disabled { + & > button { + opacity: 0.33; + } + } + + // Icon type + // Stroke + &.pi-toggle-button__icon--stroke { + & > button { + gap: var(--pi-spacing-xx-small); + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-medium); + font: { + size: var(--pi-font-size-x-small); + } + & > .mat-icon { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + min-width: var(--pi-spacing-xx-large); + padding: 0.3rem; + border: 1px solid var(--pi-color-primary-200); + border-radius: 50%; + } + } + } + // Icon type default + &:not(.pi-toggle-button__icon--stroke) { + & > button { + gap: var(--pi-spacing-x-small); + & > .mat-icon { + height: var(--pi-spacing-large); + width: var(--pi-spacing-large); + } + } + } + + // Flex firection + // Row reverse + &.pi-button__flex-direction--row-reverse { + & > button { + flex-direction: row-reverse; + justify-content: space-between; + } + } +} + +html { + // Mobile + &.is-mobile { + :host { + // Stroke + &.pi-toggle-button__icon--stroke { + & > button { + width: 10rem; + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + :host { + // Small device + @media screen and (max-width: 62rem) { + // Icon type default + &:not(.pi-toggle-button__icon--stroke) { + & > button { + padding-left: var(--pi-spacing-small); + padding-right: var(--pi-spacing-small); + } + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + :host { + // Stroke + &.pi-toggle-button__icon--stroke { + & > button { + width: 12rem; + } + } + // Large device + @media screen and (min-width: 62rem) { + // Icon type default + &:not(.pi-toggle-button__icon--stroke) { + & > button { + padding-left: var(--pi-spacing-large); + padding-right: var(--pi-spacing-medium); + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.ts b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.ts new file mode 100644 index 0000000..041151a --- /dev/null +++ b/libs/ui/src/lib/ui/button/toggle-button/toggle-button.component.ts @@ -0,0 +1,39 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { buttonSize } from '../../../styles'; +import { PiBaseButtonComponent } from '../base-button.component'; + +@Component({ + standalone: false, + selector: 'pi-toggle-button', + templateUrl: './toggle-button.component.html', + styleUrls: ['./toggle-button.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiToggleButtonComponent extends PiBaseButtonComponent { + @HostBinding('class') get classes(): string { + const isActiveClass = this.isActive && 'is-active'; + const isDisabledClass = this.isDisabled ? 'is-disabled' : ''; + const iconTypeClass = this.iconType !== 'default' && `pi-toggle-button__icon--${this.iconType}`; + return [ + 'pi-toggle-button', + this.sizeClass, + this.flexDirectionClass, + isActiveClass, + isDisabledClass, + iconTypeClass, + ].join(' '); + } + + // Content + @Input() icon: string | undefined; + @Input() text: string | undefined; + @Input() amount: number | undefined; + + // Skin + @Input() width: buttonSize = 'large'; + @Input() iconType: 'stroke' | 'default' = 'stroke'; + + // Status + @Input() isActive!: boolean; + @Input() isDisabled!: boolean; +} diff --git a/libs/ui/src/lib/ui/button/ui-button.module.ts b/libs/ui/src/lib/ui/button/ui-button.module.ts new file mode 100644 index 0000000..c7fc560 --- /dev/null +++ b/libs/ui/src/lib/ui/button/ui-button.module.ts @@ -0,0 +1,88 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +import { MatIconModule } from '@angular/material/icon'; + +import { + PiButtonSizeDirective, + PiGhostButtonDirective, + PiLargeButtonDirective, + PiMediumButtonDirective, + PiOutlineButtonDirective, + PiSmallButtonDirective, + PiW100ButtonDirective, + PiXSmallButtonDirective, + PiXXSmallButtonDirective, +} from './button.directive'; +import { PiCloseButtonComponent } from './close-button/close-button.component'; +import { PiHamburgerComponent } from './hamburger/hamburger.component'; +import { PiIconButtonComponent } from './icon-button/icon-button.component'; + +import { PiSharedUIIconModule } from '../icon'; +import { PiChevronButtonComponent } from './chevron-button/chevron-button.component'; +import { PiSwitchButtonComponent } from './switch-button/switch-button.component'; +import { PiToggleButtonComponent } from './toggle-button/toggle-button.component'; +import { PiRadioButtonComponent } from './radio-button/radio-button.component'; +import { PiActionButtonComponent } from './action-button/action-button.component'; +import { + PiSwitchButtonPrefixLeftDirective, + PiSwitchButtonSuffixLeftDirective, + PiSwitchButtonPrefixRightDirective, + PiSwitchButtonSuffixRightDirective, +} from './switch-button/switch-button.directive'; + +@NgModule({ + imports: [CommonModule, TranslatePipe, TranslateDirective, PiSharedUIIconModule, MatIconModule], + declarations: [ + PiActionButtonComponent, + PiChevronButtonComponent, + PiCloseButtonComponent, + PiGhostButtonDirective, + PiButtonSizeDirective, + PiHamburgerComponent, + PiIconButtonComponent, + PiLargeButtonDirective, + PiMediumButtonDirective, + PiOutlineButtonDirective, + PiRadioButtonComponent, + PiSmallButtonDirective, + PiSwitchButtonComponent, + PiToggleButtonComponent, + PiW100ButtonDirective, + PiXSmallButtonDirective, + PiXXSmallButtonDirective, + PiRadioButtonComponent, + PiSwitchButtonPrefixLeftDirective, + PiSwitchButtonSuffixLeftDirective, + PiSwitchButtonPrefixRightDirective, + PiSwitchButtonSuffixRightDirective, + ], + + exports: [ + PiActionButtonComponent, + PiChevronButtonComponent, + PiCloseButtonComponent, + PiButtonSizeDirective, + PiCloseButtonComponent, + PiGhostButtonDirective, + PiHamburgerComponent, + PiIconButtonComponent, + PiLargeButtonDirective, + PiMediumButtonDirective, + PiOutlineButtonDirective, + PiRadioButtonComponent, + PiSmallButtonDirective, + PiSwitchButtonComponent, + PiToggleButtonComponent, + PiW100ButtonDirective, + PiXSmallButtonDirective, + PiXXSmallButtonDirective, + PiRadioButtonComponent, + PiSwitchButtonSuffixLeftDirective, + PiSwitchButtonPrefixLeftDirective, + PiSwitchButtonPrefixRightDirective, + PiSwitchButtonSuffixRightDirective, + ], +}) +export class PiSharedUIButtonModule {} diff --git a/libs/ui/src/lib/ui/card/base-card.component.ts b/libs/ui/src/lib/ui/card/base-card.component.ts new file mode 100644 index 0000000..35e4cd3 --- /dev/null +++ b/libs/ui/src/lib/ui/card/base-card.component.ts @@ -0,0 +1,98 @@ +// Angular +import { Directive, EventEmitter, HostBinding, Input, OnInit, Output } from '@angular/core'; + +// Style definitions +import { alignment, context, gap, mode, opacity, padding } from '../../styles'; + +@Directive() +export abstract class PiBaseCardComponent implements OnInit { + @Input() hasShadow = true; + @Input() isClickable = false; + @Input() isStroked = false; + @Input() color: 'primary' | 'accent' | 'highlighted' | 'grey' | 'white' = 'white'; + @Input() mode: mode = 'light'; + @Input() alignment!: alignment; + @Input() gap!: gap; + @Input() padding: padding = 'xxl'; + @Input() linkLabel: string | undefined; + @Input() opacity: opacity; + @Input() innerPadding!: padding; + @Input() context!: context; + @Input() hasEqualPadding = false; + @Input() hasPadding = true; + + @Output() execute: EventEmitter = new EventEmitter(); + + ngOnInit(): void { + this.colorClass; + this.contextClass; + this.gapClass; + this.hasEqualPaddingClass; + this.hasLinkLabelClass; + this.hasShadowClass; + this.isClickableClass; + this.modeClass; + this.opacityClass; + this.isStrokedClass; + } + + protected get hasShadowClass(): string | boolean | undefined { + return this.hasShadow && 'has-shadow'; + } + + protected get isClickableClass(): string | undefined { + return this.isClickable && 'is-clickable'; + } + + protected get colorClass(): string { + return this.color && `pi-color__${this.color}`; + } + + protected get alignmentClass(): string | undefined { + return this.alignment && `pi-card__align-items--${this.alignment}`; + } + + protected get gapClass(): string | undefined { + return this.gap && `pi-card__gap--${this.gap}`; + } + + protected get paddingClass(): string | undefined { + return this.padding && `pi-card__padding--${this.padding}`; + } + + protected get modeClass(): string | undefined { + return this.mode ? `pi-color__mode--${this.mode}` : ''; + } + + protected get isStrokedClass(): string { + return this.isStroked ? 'is-stroked' : ''; + } + + protected get hasEqualPaddingClass(): string { + return this.hasEqualPadding ? 'has-equal-inner-padding' : ''; + } + + protected get hasLinkLabelClass(): string { + return !!this.linkLabel ? 'has-link-label' : ''; + } + + protected get opacityClass(): string { + return this.opacity ? `pi-color__opacity--${this.opacity}` : ''; + } + + protected get innerPaddingClass(): string { + return this.innerPadding ? `pi-card__inner-padding--${this.innerPadding}` : ''; + } + + protected get isContectPublicClass(): string | undefined { + return this.context === 'public' ? 'is-public' : ''; + } + + protected get hasPaddingClass(): string | undefined { + return !this.padding ? '' : 'has-padding'; + } + + protected get contextClass(): string | undefined { + return this.context && `is-${this.context}`; + } +} diff --git a/libs/ui/src/lib/ui/card/card/card.component.html b/libs/ui/src/lib/ui/card/card/card.component.html new file mode 100644 index 0000000..44d845f --- /dev/null +++ b/libs/ui/src/lib/ui/card/card/card.component.html @@ -0,0 +1,13 @@ + + + + + + + + + + {{ linkLabel }} + + + diff --git a/libs/ui/src/lib/ui/card/card/card.component.ts b/libs/ui/src/lib/ui/card/card/card.component.ts new file mode 100644 index 0000000..b2ccec3 --- /dev/null +++ b/libs/ui/src/lib/ui/card/card/card.component.ts @@ -0,0 +1,33 @@ +// Angular +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; + +// Components +import { PiBaseCardComponent } from '../base-card.component'; + +@Component({ + standalone: false, + selector: 'pi-card', + templateUrl: './card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiCardComponent extends PiBaseCardComponent { + @HostBinding('class') get classes(): string { + return [ + 'pi-component', + 'pi-card', + this.isClickableClass, + this.hasShadowClass, + this.colorClass, + this.alignmentClass, + this.gapClass, + this.modeClass, + this.isStrokedClass, + this.paddingClass, + this.opacityClass, + this.hasLinkLabelClass, + this.innerPaddingClass, + this.hasEqualPaddingClass, + this.contextClass, + ].join(' '); + } +} diff --git a/libs/ui/src/lib/ui/card/directives/card.directive.ts b/libs/ui/src/lib/ui/card/directives/card.directive.ts new file mode 100644 index 0000000..862003e --- /dev/null +++ b/libs/ui/src/lib/ui/card/directives/card.directive.ts @@ -0,0 +1,42 @@ +import { Directive, HostBinding, Input } from '@angular/core'; + +@Directive({ + standalone: false, + selector: 'pi-card-top-content', + host: { + class: 'pi-card-content pi-card-content__top', + }, +}) +export class PiCardTopContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-card-center-content', +}) +export class PiCardCenterContentDirective { + @HostBinding('class') className = 'pi-card-content pi-card-content__center'; +} + +@Directive({ + standalone: false, + selector: 'pi-card-bottom-content', +}) +export class PiCardBottomContentDirective { + @HostBinding('class') className = 'pi-card-content pi-card-content__bottom'; +} + +@Directive({ + standalone: false, + selector: 'pi-card-left-content', +}) +export class PiCardLeftContentDirective { + @HostBinding('class') className = 'pi-card-content pi-card-content__left'; +} + +@Directive({ + standalone: false, + selector: 'pi-card-right-content', +}) +export class PiCardRightContentDirective { + @HostBinding('class') className = 'pi-card-content pi-card-content__right'; +} diff --git a/libs/ui/src/lib/ui/card/index.ts b/libs/ui/src/lib/ui/card/index.ts new file mode 100644 index 0000000..4f63638 --- /dev/null +++ b/libs/ui/src/lib/ui/card/index.ts @@ -0,0 +1,5 @@ +export * from './ui-card.module'; + +export * from './card/card.component'; + +export * from './directives/card.directive'; diff --git a/libs/ui/src/lib/ui/card/ui-card.module.ts b/libs/ui/src/lib/ui/card/ui-card.module.ts new file mode 100644 index 0000000..3e1f8d4 --- /dev/null +++ b/libs/ui/src/lib/ui/card/ui-card.module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { PiSharedUIIconModule } from '../icon'; +import { PiCardComponent } from './card/card.component'; +import { + PiCardBottomContentDirective, + PiCardCenterContentDirective, + PiCardLeftContentDirective, + PiCardRightContentDirective, + PiCardTopContentDirective, +} from './directives/card.directive'; + +@NgModule({ + imports: [CommonModule, PiSharedUIIconModule], + declarations: [ + PiCardComponent, + PiCardTopContentDirective, + PiCardCenterContentDirective, + PiCardBottomContentDirective, + PiCardLeftContentDirective, + PiCardRightContentDirective, + ], + exports: [ + PiCardComponent, + PiCardTopContentDirective, + PiCardCenterContentDirective, + PiCardBottomContentDirective, + PiCardLeftContentDirective, + PiCardRightContentDirective, + ], +}) +export class PiSharedUICardModule {} diff --git a/libs/ui/src/lib/ui/carousel/carousel.directive.ts b/libs/ui/src/lib/ui/carousel/carousel.directive.ts new file mode 100644 index 0000000..dee3ff1 --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/carousel.directive.ts @@ -0,0 +1,10 @@ +import { Directive, TemplateRef, ViewContainerRef } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-carousel-item]', + host: { class: 'pi-carousel pi-carousel__item' }, +}) +export class PiCarouselItemDirective { + constructor(public templateRef: TemplateRef, public viewContainerRef: ViewContainerRef) {} +} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.html b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.html new file mode 100644 index 0000000..f4a7efd --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.html @@ -0,0 +1,31 @@ +
+ +
+ +
+ +
+
+
+ +
+ + +
+
+
diff --git a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.scss b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.scss new file mode 100644 index 0000000..1fab24b --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.scss @@ -0,0 +1,144 @@ +@mixin carouselFooter($color) { + footer { + display: flex; + justify-content: center; + gap: var(--pi-spacing-medium); + margin-top: var(--pi-spacing-xxx-large); + + div { + height: var(--pi-spacing-xx-small); + width: var(--pi-spacing-xx-small); + border-radius: var(--pi-border-radius-xx-small); + &.is-active { + background-color: var(--pi-color-#{$color}-200); + } + &:not(.is-active) { + background-color: var(--pi-color-#{$color}-50); + } + } + } +} + +@mixin carouselJustifyContent($justify-content) { + & > main { + & > section { + justify-content: $justify-content; + } + } +} + +:host { + // Common + .pi-carousel__item { + width: -webkit-fill-available; + } + // Type + &.pi-carousel__linear, + &.pi-carousel__symmetric { + & > main { + display: flex; + align-items: center; + justify-content: center; + gap: var(--pi-spacing-small); + &.pi-carousel__horizontal { + flex-direction: row; + & > section { + flex-direction: row; + padding: var(--pi-spacing-xx-large) var(--pi-spacing-large); + } + } + &.pi-carousel__vertical { + flex-direction: column; + & > section { + flex-direction: column; + padding: 0 var(--pi-spacing-large); + } + } + + & > section { + display: flex; + align-items: stretch; + flex: 1 1 0; + overflow: hidden; + transition: var(--pi-animation); + .pi-carousel__item { + user-select: none; + transition: var(--pi-animation); + overflow: hidden; + &:not(:first-child):not(:last-child) { + padding: 0 var(--pi-spacing-xx-small); + } + &:first-child { + padding-right: var(--pi-spacing-xx-small); + } + &:last-child { + padding-left: var(--pi-spacing-xx-small); + } + } + } + } + + &.is-draggable { + .has-overflow { + .pi-carousel__item { + user-select: none; + cursor: grab; + + &:active { + cursor: grabbing; + } + } + } + &.pi-carousel__primary { + @include carouselFooter(primary); + } + &.pi-carousel__accent { + @include carouselFooter(accent); + } + &.pi-carousel__highlighted { + @include carouselFooter(highlighted); + } + } + } + // Symetric + &.pi-carousel__symmetric { + .pi-carousel__item { + &.is-active { + z-index: 9; + } + &:not(.is-active) { + opacity: 0.3; + } + } + } + // Justify + // Center + &.pi-carousel__justify-content--center { + @include carouselJustifyContent(center); + } + // Flex Start + &.pi-carousel__justify-content--flex-start { + @include carouselJustifyContent(flex-start); + } + // Flex End + &.pi-carousel__justify-content--flex-end { + @include carouselJustifyContent(flex-end); + } + // Space Around + &.pi-carousel__justify-content--space-around { + @include carouselJustifyContent(space-around); + } + // Space Between + &.pi-carousel__justify-content--space-between { + @include carouselJustifyContent(space-between); + } + // Space Evenly + &.pi-carousel__justify-content--space-evenly { + & > main { + justify-content: space-between; + & > section { + max-width: 80%; + } + } + } +} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts new file mode 100644 index 0000000..e7d491f --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/carousel/carousel.component.ts @@ -0,0 +1,525 @@ +// Angular +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + QueryList, + Renderer2, + SimpleChanges, + ViewChild, +} from '@angular/core'; + +// Shared +import { ComponentFamilyClass, justify, theming } from '../../../styles'; +import { Device, DeviceConfigsFacade } from '../../../util/device-configs'; +import { PiCarouselItemDirective } from '../carousel.directive'; +import { Subject, combineLatest, skip, takeUntil } from 'rxjs'; + +@Component({ + standalone: false, + selector: 'pi-carousel', + templateUrl: './carousel.component.html', + styleUrls: ['./carousel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiCarouselComponent + implements OnInit, OnChanges, AfterViewInit, AfterViewChecked, OnDestroy +{ + @HostBinding('class') get classes(): string { + const itemsNumberClass = + this.carouselItems && `pi-carousel__${this.carouselItems.length}-items`; + const directionTypeClass = this.direction && `pi-carousel__${this.direction}`; + const colorClass = `pi-carousel__${this.color}`; + const typeClass = `pi-carousel__${this.type}`; + const isDraggableClass = this.isDraggable ? 'is-draggable' : ''; + const justifyClass = this.justify && `pi-carousel__justify-content--${this.justify}`; + return [ + ComponentFamilyClass.Carousel, + itemsNumberClass, + typeClass, + directionTypeClass, + isDraggableClass, + colorClass, + justifyClass, + ].join(' '); + } + + // Skin + @Input() type: 'symmetric' | 'linear' = 'linear'; + + @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; + + @Input() color: theming = 'primary'; + @Input() justify!: justify; + + @Input() device!: Device; + + /** + * If true, the carousel will not show the next and previous buttons. + */ + @Input() hideSwipeButtons = true; + + /** + * If true, the carousel will not show the pagination. + */ + @Input() hidePaginator = false; + + + // BEHAVIOUR + /** + * If a number of items per device (itemsDesktop, itemsTablet, itemsMobile) is provided, + * each item will take the total width of the carousel divided by the number of items. + */ + @Input() itemsDesktop!: number; + + /** + * If no number of items for tablet is provided, + * the number of items for desktop will be used. + */ + @Input() itemsTablet: number; + + /** + * If no number of items for mobile is provided, + * the number of items for desktop will be used. + */ + @Input() itemsMobile: number; + + /** + * If we want to set a custom width for each item, + * we can provide the width in pixels and this will override the number of items per device. + * The carousel will display per page the number of items that fit in the total width. + */ + @Input() itemWidth!: number; + + /** + * If true, the user will be able to drag the carousel to move across items. + */ + @Input() isDraggable: boolean = true; + + /** + * If true, when the user clicks on the next or previous button, + * the carousel will move to the next or previous page + * ie. currentItem will be incremented/decremented by itemsPerPage. + */ + @Input() movePerPage: boolean = false; + + // OUTPUTS + /** + * Emits the index of the current item when it changes. + */ + @Output() itemChanged: EventEmitter = new EventEmitter(); + + /** + * carouselItems corresponds to the items that will be displayed in the carousel. + */ + @ContentChildren(PiCarouselItemDirective, { descendants: true }) + carouselItems!: QueryList; + + /** + * carouselContentWrapper corresponds to the section that contains the carousel items and excludes the control buttons + */ + @ViewChild('carouselContentWrapper', { read: ElementRef, static: false }) + carouselContentWrapper!: ElementRef; + + carouselItemsNativeElements: HTMLElement[] = []; + + private destroyed$: Subject = new Subject(); + + private _currentItemIndex = 0; + set currentItem(index: number) { + this._currentItemIndex = index; + this.itemChanged.emit(index); + } + get currentItem(): number { + return this._currentItemIndex; + } + + itemsPerPage: number; + pages: Array; + + // Drag variables + isDragging = false; + isMouseDown = false; + dragInitPosition = 0; + dragFinalPosition = 0; + + private mousedownListener: () => void; + private mouseupListener: () => void; + private mousemoveListener: () => void; + + private touchstartListener: () => void; + private touchmoveListener: () => void; + private touchendListener: () => void; + + constructor( + private cdr: ChangeDetectorRef, + private el: ElementRef, + private renderer: Renderer2, + private deviceConfig: DeviceConfigsFacade + ) {} + + /** + * Used for symmetric carousel to know how many items to show on each side of the current item. + */ + get numberOfSideItems(): number { + return Math.floor(this.itemsPerPage / 2); + } + + /** + * Icon displayed in the next control button. + */ + get nextIcon(): string { + return this.direction === 'horizontal' ? 'chevron-forward' : 'chevron-down'; + } + + /** + * Icon displayed in the prev control button. + */ + get prevIcon(): string { + return this.direction === 'horizontal' ? 'chevron-back' : 'chevron-up'; + } + + /** + * Returns the carousel content wrapper native element. + */ + get carouselContentElement(): HTMLElement { + return this.carouselContentWrapper.nativeElement; + } + + get tallestElementHeightPx(): number { + return this.carouselItemsNativeElements.reduce( + (value, currentValue) => + value > currentValue.offsetHeight ? value : currentValue.offsetHeight, + 0 + ); + } + + /** + * Returns true if the number of items is greater than the number of items per page. + */ + get hasOverflow(): boolean { + return this.carouselItems.length > this.itemsPerPage; + } + + get canSwipeNext(): boolean { + return this.movePerPage + ? this.currentItem + this.itemsPerPage < this.carouselItems.length + : this.currentItem < this.carouselItems.length - 1; + } + + get canSwipePrev(): boolean { + return this.movePerPage ? this.currentItem - this.itemsPerPage >= 0 : this.currentItem > 0; + } + + /** + * Returns the index of the current page. + */ + get currentPageIndex(): number { + return Math.trunc(this.currentItem / this.itemsPerPage); + } + + ngOnInit(): void { + combineLatest([this.deviceConfig.device$, this.deviceConfig.size$]) + .pipe(skip(1), takeUntil(this.destroyed$)) + .subscribe(() => { + this.reloadComponent(); + }); + } + + ngOnChanges(simpleChanges: SimpleChanges): void { + if (simpleChanges['itemWidth'] && !!this.carouselContentWrapper) { + this.setItemsPerPage(); + } + } + + ngAfterViewInit(): void { + this.setItemsPerPage(); + this.setPages(); + + if (this.isDraggable) { + this.setDragEventListeners(); + } + + this.carouselItems.changes.pipe(takeUntil(this.destroyed$)).subscribe(() => { + this.cdr.markForCheck(); + }); + } + + ngAfterViewChecked(): void { + this.getCarouselItemsNativeElements(); + + this.setStyleOnInit(); + } + + ngOnDestroy(): void { + this.stopDragEventListeners(); + this.destroyed$.next(); + this.destroyed$.complete(); + } + + reloadComponent(): void { + this.ngOnDestroy(); + this.currentItem = 0; + this.ngAfterViewInit(); + this.ngAfterViewChecked(); + this.cdr.markForCheck(); + } + + next(): void { + if (this.movePerPage) { + this.currentPageIndex < this.pages.length - 1 + ? (this.currentItem = (this.currentPageIndex + 1) * this.itemsPerPage) + : null; + } else { + this.currentItem = this.currentItem + 1; + } + this.carouselTranslation(this.currentItem); + } + + prev(): void { + if (this.movePerPage) { + this.currentPageIndex > 0 + ? (this.currentItem = (this.currentPageIndex - 1) * this.itemsPerPage) + : null; + } else { + this.currentItem = this.currentItem - 1; + } + this.carouselTranslation(this.currentItem); + } + + setItemsPerPage(): void { + if (!!this.itemWidth) { + this.itemsPerPage = Math.floor(this.carouselContentElement.clientWidth / this.itemWidth) || 1; + } else { + switch (this.device) { + case 'DESKTOP': + this.itemsPerPage = this.itemsDesktop ? this.itemsDesktop : this.carouselItems.length; + break; + case 'TABLET': + this.itemsPerPage = this.itemsTablet ? this.itemsTablet : this.carouselItems.length; + break; + case 'MOBILE': + this.itemsPerPage = this.itemsMobile ? this.itemsMobile : this.carouselItems.length; + break; + } + } + } + + trackByFn(index: number, item: PiCarouselItemDirective): number { + return index; + } + + getItemClass(index: number): string { + let itemClass: string = 'pi-carousel__item pi-carousel__item--' + index; + if (index === this.currentItem) itemClass += ' ' + 'is-active'; + return itemClass; + } + + selectItem(index: number): void { + this.currentItem = index; + if (this.hasOverflow && !this.movePerPage) { + this.carouselTranslation(index); + } + } + + setPages(): void { + let totalPages: number; + let pages: Array = []; + totalPages = Math.ceil(this.carouselItems.length / this.itemsPerPage); + for (let i = 0; i < totalPages; i++) { + pages.push(i); + } + this.pages = pages; + this.cdr.detectChanges(); + } + + private stopTouchDragEventListeners(): void { + if (this.touchstartListener) this.touchstartListener(); + if (this.touchmoveListener) this.touchmoveListener(); + if (this.touchendListener) this.touchendListener(); + } + + private stopMouseDragEventListeners(): void { + if (this.mousedownListener) this.mousedownListener(); + if (this.mouseupListener) this.mouseupListener(); + if (this.mousemoveListener) this.mousemoveListener(); + } + + private stopDragEventListeners(): void { + this.stopMouseDragEventListeners(); + this.stopTouchDragEventListeners(); + } + + private setDragEventListeners(): void { + this.mousedownListener = this.renderer.listen( + this.carouselContentWrapper.nativeElement, + 'mousedown', + (event) => { + this.dragStart(event); + } + ); + + this.mousemoveListener = this.renderer.listen( + document.documentElement, + 'mousemove', + (event) => { + if (this.isMouseDown) this.dragAction(event); + } + ); + + this.mouseupListener = this.renderer.listen(document.documentElement, 'mouseup', () => { + if (this.isDragging) { + this.dragEnd(); + } + }); + + this.touchstartListener = this.renderer.listen( + this.carouselContentWrapper.nativeElement, + 'touchstart', + (event) => { + this.dragStart(event); + } + ); + + this.touchmoveListener = this.renderer.listen( + document.documentElement, + 'touchmove', + (event) => { + if (this.isMouseDown) this.dragAction(event); + } + ); + + this.touchendListener = this.renderer.listen(document.documentElement, 'touchend', () => { + if (this.isDragging) this.dragEnd(); + }); + } + + private dragStart(event: MouseEvent | TouchEvent): void { + this.isMouseDown = true; + if (event instanceof TouchEvent) { + this.stopMouseDragEventListeners(); + this.dragInitPosition = event.touches[0].clientX; + } else { + this.stopTouchDragEventListeners(); + this.dragInitPosition = event.clientX; + } + } + + /** + * + * @param event + * This function handles the logic for dragging the carousel, allowing the user to swipe to the next or previous item based on the drag direction and minimum dragging width. + */ + private dragAction(event: MouseEvent | TouchEvent): void { + /** + * Sets a minimum dragging width based on either 80% of the item width if provided + * Otherwise 80% of the width of one item in terms of the number of items displayed per page + */ + const minDraggingWidth = this.itemWidth ? 0.8 * this.itemWidth : 0.8 * (100 / this.itemsPerPage); + + this.isDragging = true; + + if (event instanceof TouchEvent) this.dragFinalPosition = event.touches[0].clientX; + if (event instanceof MouseEvent) this.dragFinalPosition = event.clientX; + + if (this.dragInitPosition - this.dragFinalPosition > minDraggingWidth && this.canSwipeNext) { + this.currentItem++; + this.carouselTranslation(this.currentItem); + this.dragInitPosition = this.dragFinalPosition; + } + + if (this.dragInitPosition - this.dragFinalPosition < -minDraggingWidth && this.canSwipePrev) { + this.currentItem--; + this.carouselTranslation(this.currentItem); + this.dragInitPosition = this.dragFinalPosition; + } + } + + private dragEnd(): void { + this.isDragging = false; + this.isMouseDown = false; + this.cdr.detectChanges(); + } + + private getCarouselItemsNativeElements(): void { + this.carouselItemsNativeElements = Array.from( + this.el.nativeElement.querySelectorAll('.pi-carousel__item') + ); + } + + private carouselTranslation(currentItem: number): void { + if (this.type === 'linear' && currentItem <= this.carouselItems.length - this.itemsPerPage) { + this.carouselItemsTranslation(currentItem, `-${currentItem * 100}%`, this.direction); + } else if (this.type === 'symmetric') { + if (currentItem < this.numberOfSideItems) { + // If we are at the beginning of the carousel + this.carouselItemsTranslation(currentItem, '0', this.direction); + } else if (this.carouselItems.length - currentItem > this.numberOfSideItems) { + // If we are at the end of the carousel + this.carouselItemsTranslation( + currentItem, + `-${(currentItem - this.numberOfSideItems) * 100}%`, + this.direction + ); + } else { + // If we are at the end of the carousel + this.carouselItemsTranslation( + currentItem, + `-${(this.carouselItems.length - this.itemsPerPage) * 100}%`, + this.direction + ); + } + } + } + + private setStyleOnInit(): void { + this.direction === 'horizontal' + ? this.setHorizontalCarouselStyle() + : this.setVerticalCarouselStyle(); + } + + private carouselItemsTranslation(currentItem: number, translation: string, axis: string) { + this.carouselItemsNativeElements.forEach((item, index) => { + const scale: number = index === currentItem && this.type === 'symmetric' ? 1.15 : 1; + const transform: string = + axis === 'horizontal' ? `translateX(${translation})` : `translateY(${translation})`; + this.renderer.setStyle(item, 'transform', `${transform} scale(${scale})`); + }); + } + + private setHorizontalCarouselStyle(): void { + this.carouselItemsNativeElements.forEach((item) => { + if (!!this.itemWidth) { + this.renderer.setStyle(item, 'min-width', `${this.itemWidth}px`); + this.renderer.setStyle(item, 'max-width', `${this.itemWidth}px`); + this.renderer.setStyle(item, 'width', 'unset'); + } else { + this.renderer.setStyle(item, 'min-width', `${100 / this.itemsPerPage}%`); + this.renderer.setStyle(item, 'max-width', `${100 / this.itemsPerPage}%`); + this.renderer.setStyle(item, 'width', `${100 / this.itemsPerPage}%`); + } + }); + } + + private setVerticalCarouselStyle(): void { + this.renderer.setStyle( + this.carouselContentElement, + 'min-height', + `${this.itemsPerPage * this.tallestElementHeightPx}px` + ); + this.carouselItemsNativeElements.forEach((item) => { + this.renderer.setStyle(item, 'min-height', `${this.tallestElementHeightPx}px`); + }); + } +} diff --git a/libs/ui/src/lib/ui/carousel/index.ts b/libs/ui/src/lib/ui/carousel/index.ts new file mode 100644 index 0000000..2d9de10 --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/index.ts @@ -0,0 +1,5 @@ +export * from './ui-carousel.module'; + +export * from './carousel/carousel.component'; + +export * from './carousel.directive'; diff --git a/libs/ui/src/lib/ui/carousel/ui-carousel.module.ts b/libs/ui/src/lib/ui/carousel/ui-carousel.module.ts new file mode 100644 index 0000000..2e5c978 --- /dev/null +++ b/libs/ui/src/lib/ui/carousel/ui-carousel.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiCarouselComponent } from './carousel/carousel.component'; +import { PiCarouselItemDirective } from './carousel.directive'; +import { PiSharedUIButtonModule } from '../button'; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + ], + declarations: [PiCarouselComponent, PiCarouselItemDirective], + exports: [PiCarouselComponent, PiCarouselItemDirective], +}) +export class PiSharedUICarouselModule {} diff --git a/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.html b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.html new file mode 100644 index 0000000..7999711 --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.html @@ -0,0 +1,10 @@ + +
+
+ +
+
+ +{{ 'common.legend' | translate }} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.scss b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.scss new file mode 100644 index 0000000..760dc6a --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.scss @@ -0,0 +1,123 @@ +@mixin chartLegendsGrid($columns: 2, $rows: 2) { + :host { + grid-template-columns: repeat(#{$columns}, 1fr); + grid-template-rows: repeat(#{$rows}, 1fr); + & > section { + justify-self: center; + } + } +} + +:host { + position: relative; + display: grid; + gap: var(--pi-spacing-small); + padding: var(--pi-spacing-x-large) var(--pi-spacing-medium); + background-color: var(--pi-color-grey-10); + border-radius: var(--pi-border-radius-large); + + & > span { + position: absolute; + min-width: 8rem; + padding: var(--pi-spacing-xxx-small) var(--pi-spacing-medium); + text-align: center; + color: var(--pi-color-grey-200); + background-color: var(--pi-color-white); + border: 1px solid var(--pi-color-grey-75); + border-radius: var(--pi-border-radius-small); + font: { + size: var(--pi-font-size-x-small); + } + } + & > section { + display: flex; + align-items: center; + gap: var(--pi-spacing-xx-small); + + cursor: pointer; + overflow: hidden; + + &.is-hovered { + & > label { + font-weight: 600; + } + } + + & > * { + cursor: pointer; + &:hover { + font-weight: 600; + } + } + + & > div { + min-width: var(--pi-spacing-small); + min-height: var(--pi-spacing-small); + border-radius: 50%; + border: 1px solid; + } + & > label { + padding-right: var(--pi-spacing-x-large); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + transition: var(--pi-animation); + } + } +} + +html { + // Tablet & Mobile + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + :host { + & > span { + top: -0.75rem; + left: 50%; + transform: translateX(-50%); + } + } + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + :host { + & > span { + top: -1rem; + margin-left: var(--pi-spacing-x-large); + } + & > section { + justify-content: center; + } + // Comparative chart + &.is-comparative { + & > section { + &:first-child { + & > label { + border-right: 1px solid var(--pi-color-grey-100); + } + } + } + } + } + } + } + &.is-mobile { + @include chartLegendsGrid; + } + + &.is-tablet { + &.is-tablet__small { + @include chartLegendsGrid; + } + &.is-tablet__large { + @include chartLegendsGrid(3, 1); + } + } + &.is-desktop { + @include chartLegendsGrid(3, 1); + } +} diff --git a/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.ts b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.ts new file mode 100644 index 0000000..5f16dbd --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart-legend/chart-legend.component.ts @@ -0,0 +1,29 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output +} from '@angular/core'; +import { Dataset } from '../../../util/models'; + +@Component({ + standalone: false, + selector: 'pi-chart-legend', + templateUrl: './chart-legend.component.html', + styleUrls: ['./chart-legend.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChartLegendComponent { + @HostBinding('class') get classes(): string { + const comparativeClass = this.isComparativeChart ? 'is-comparative' : ''; + return ['pi-chart-legend', comparativeClass].join(' '); + } + + @Input() datasets!: Array; + @Input() isComparativeChart: boolean = true; + @Input() hoveredDatasetIndex: number; + + @Output() triggerDatasetHover: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/chart/chart.configs.ts b/libs/ui/src/lib/ui/chart/chart.configs.ts new file mode 100644 index 0000000..2da693c --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart.configs.ts @@ -0,0 +1,90 @@ +import { DatasetColor } from '../../util/models'; +import { + ChartTypeRegistry, + ScaleOptionsByType, + TooltipOptions +} from 'chart.js'; + +type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial } : T; + +export const tooltipConfig: DeepPartial> = { + backgroundColor: DatasetColor.white, + bodyFont: { + family: 'Soehne', + size: 14, + }, + displayColors: true, + usePointStyle: true, + bodySpacing: 14, + cornerRadius: 20, + padding: { + left: 24, + right: 24, + top: 16, + bottom: 24, + }, + intersect: true, + borderColor: '#EEEFF1', + borderWidth: 1, + boxPadding: 6, + position: 'nearest', + yAlign: 'bottom', + caretPadding: 10, + titleSpacing: 0, + titleMarginBottom: 0, +}; + +export const scalesConfig: DeepPartial<{ + [key: string]: ScaleOptionsByType; +}> = { + x: { + display: true, + grid: { + color: DatasetColor.grey50, + }, + border: { + dash: [8, 4], + display: false, + color: DatasetColor.grey75, + }, + ticks: { + padding: 25, + color: DatasetColor.grey300, + font: { + family: 'Soehne', + size: 12, + weight: 'bold', + }, + }, + }, + y: { + display: true, + position: 'right', + grid: { + display: false, + lineWidth: 0, + drawOnChartArea: false, + }, + border: { + display: false, + }, + ticks: { + padding: 40, + color: DatasetColor.grey, + font: { + family: 'Soehne', + size: 12, + }, + }, + }, +}; + +export const datasetConfig = { + borderWidth: 1.3, + hoverBorderWidth: 3.5, + pointRadius: 8, + pointHoverRadius: 8, + pointBorderWidth: 2, + pointHoverBorderWidth: 2, +}; + diff --git a/libs/ui/src/lib/ui/chart/chart.module.ts b/libs/ui/src/lib/ui/chart/chart.module.ts new file mode 100644 index 0000000..cf226f5 --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiChartComponent } from './chart/chart.component'; +import { PiChartLegendComponent } from './chart-legend/chart-legend.component'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +@NgModule({ + imports: [ + CommonModule, + TranslatePipe, TranslateDirective, + ], + declarations: [PiChartComponent, PiChartLegendComponent], + exports: [PiChartComponent, PiChartLegendComponent], +}) +export class PiSharedUIChartModule {} diff --git a/libs/ui/src/lib/ui/chart/chart/chart.component.html b/libs/ui/src/lib/ui/chart/chart/chart.component.html new file mode 100644 index 0000000..8c2b89c --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart/chart.component.html @@ -0,0 +1,9 @@ +
+ +
+ diff --git a/libs/ui/src/lib/ui/chart/chart/chart.component.scss b/libs/ui/src/lib/ui/chart/chart/chart.component.scss new file mode 100644 index 0000000..bfef8c3 --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart/chart.component.scss @@ -0,0 +1,34 @@ +:host { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + max-width: 100%; + + & > div { + position: relative; + max-width: 100%; + } +} + +html { + &:not(.is-desktop) { + :host { + & > div { + aspect-ratio: 1; + height: 100%; + max-height: 40vh; + } + } + } + &.is-desktop { + :host { + & > div { + & > canvas { + aspect-ratio: 1; + height: 100%; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/chart/chart/chart.component.ts b/libs/ui/src/lib/ui/chart/chart/chart.component.ts new file mode 100644 index 0000000..fdcb3eb --- /dev/null +++ b/libs/ui/src/lib/ui/chart/chart/chart.component.ts @@ -0,0 +1,178 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild, +} from '@angular/core'; +import { Device } from '../../../util/device-configs'; +import { Dataset } from '../../../util/models'; +import { TranslateService } from '@ngx-translate/core'; +import { + BarController, + BarElement, + CategoryScale, + Chart, + Legend, + LineController, + LineElement, + LinearScale, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { datasetConfig, scalesConfig, tooltipConfig } from '../chart.configs'; + +function registerChartComponents() { + Chart.register(CategoryScale); + Chart.register(LinearScale); + Chart.register(BarController); + Chart.register(BarElement); + Chart.register(LineController); + Chart.register(LineElement); + Chart.register(PointElement); + Chart.register(Tooltip); + Chart.register(Legend); + Chart.register(Title); +} + +registerChartComponents(); +@Component({ + standalone: false, + selector: 'pi-chart', + templateUrl: './chart.component.html', + styleUrls: ['./chart.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChartComponent implements OnInit, AfterViewInit { + @Input() type: 'line' | 'bar' = 'line'; + @Input() datasets!: Array; + @Input() device!: Device; + @Input() xAxisMarks: Array; + @Input() isComparativeChart: boolean = true; + + private _hoveredDatasetIndex: number; + set hoveredDatasetIndex(index: number) { + this._hoveredDatasetIndex = index; + } + get hoveredDatasetIndex(): number { + return this._hoveredDatasetIndex; + } + + @ViewChild('chart', { read: ElementRef, static: false }) + chartRef!: ElementRef; + + chart: Chart; + + constructor( + private translate: TranslateService, + private cdr: ChangeDetectorRef, + ) {} + + get completedDatasets() { + return this.datasets.map((dataset) => { + const datasetWithTranslatedLabels = { + ...dataset, + label: this.translate.instant(dataset.label), + }; + return { + ...datasetWithTranslatedLabels, + ...datasetConfig, + }; + }); + } + + ngOnInit(): void { + this.hoveredDatasetIndex = -1; + } + + ngAfterViewInit(): void { + this.getLineChartConfigs(); + } + + triggerHover(datasetIndex: number) { + if (datasetIndex === -1 || this.chart.getActiveElements().length > 0) { + this.chart.setActiveElements([]); + } else { + this.chart.setActiveElements([ + { + datasetIndex, + index: 0, + }, + ]); + } + this.chart.update(); + } + + getLineChartConfigs(): void { + const isComparativeChart = this.isComparativeChart; + + this.chart = new Chart(this.chartRef.nativeElement, { + type: 'line', + data: { + labels: this.xAxisMarks, + datasets: this.completedDatasets, + }, + options: { + responsive: true, + maintainAspectRatio: this.device === 'DESKTOP' ? true : false, + hover: { + mode: 'dataset', + }, + onHover: (e: any, activeElements: any) => { + this.hoveredDatasetIndex = activeElements.length && activeElements[0].datasetIndex; + this.cdr.detectChanges(); + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + ...tooltipConfig, + filter: function (tooltipItem): boolean { + if (isComparativeChart) { + return tooltipItem.datasetIndex === 0 || tooltipItem.element.active; + } else { + return tooltipItem.element.active; + } + }, + callbacks: { + labelTextColor: function (context): string { + return context.dataset.borderColor as string; + }, + label: function (context) { + const chartLabel = + context.dataset.label.length > 12 + ? context.dataset.label.slice(0, 12) + '...' + : context.dataset.label; + let label: string; + if ((context.dataset as any).unitPosition === 'before') { + label = + chartLabel + ': ' + (context.dataset as any).unit + context.formattedValue; + } else { + label = + chartLabel + ': ' + context.formattedValue + (context.dataset as any).unit; + } + return label; + }, + title: function () {}, + }, + }, + }, + interaction: { + intersect: false, + mode: 'index', + }, + scales: scalesConfig, + }, + }); + } + + datasetHoveredInLegend(index: number): void { + this.hoveredDatasetIndex = index; + this.triggerHover(this.hoveredDatasetIndex); + } +} diff --git a/libs/ui/src/lib/ui/chart/index.ts b/libs/ui/src/lib/ui/chart/index.ts new file mode 100644 index 0000000..85c350c --- /dev/null +++ b/libs/ui/src/lib/ui/chart/index.ts @@ -0,0 +1,7 @@ +export * from './chart.module'; + +export * from './chart/chart.component'; + +export * from './chart.configs'; + +export * from './chart-legend/chart-legend.component'; diff --git a/libs/ui/src/lib/ui/chip/base-chip.component.ts b/libs/ui/src/lib/ui/chip/base-chip.component.ts new file mode 100644 index 0000000..a09c5b7 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/base-chip.component.ts @@ -0,0 +1,52 @@ +// Angular +import { Directive, HostBinding, Input } from '@angular/core'; + +// Style definitions +import { ComponentFamilyClass, density, mode, styleType } from '../../styles'; +import { chipColor } from './entities/chip.model'; +@Directive() +export abstract class PiBaseChipComponent { + @HostBinding('class') get classes(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const sizeClass = this.size && `pi-chip__size--${this.size}`; + const affixClass = this.icon || this.value ? 'has-affix' : ''; + const appearanceClass = + this.affixAppearance && `pi-chip__affix-appearance--${this.affixAppearance}`; + const colorModeClass = this.mode && `pi-color__mode--${this.mode}`; + const isDisabledClass = this.isDisabled ? 'is-disabled' : ''; + const isClosableClass = this.isClosable ? 'is-closable' : ''; + const fillClass = this.fill !== 'fit-content' && 'pi-chip__fill--fill-available'; + return [ + ComponentFamilyClass.Chip, + colorClass, + sizeClass, + isDisabledClass, + appearanceClass, + affixClass, + colorModeClass, + isClosableClass, + fillClass, + ].join(' '); + } + // Content + @Input() label: string | undefined; + @Input() value: string | number | undefined; + @Input() icon: string | undefined; + + // Skin + @Input() color: chipColor; + + @Input() mode: mode = 'light'; + @Input() size: 'small' | 'medium' | 'large' = 'small'; + @Input() fill: 'fit-content' | 'fill-available' = 'fit-content'; + + // Affix + @Input() affixAppearance: 'fill' | 'ghost' = 'ghost'; + @Input() affixPosition: 'previous' | 'after' = 'after'; + + // Behavior + @Input() isClosable: boolean = true; + + // Status + @Input() isDisabled = false; +} diff --git a/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.html b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.html new file mode 100644 index 0000000..3433a70 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.html @@ -0,0 +1,21 @@ + +
+ {{ label }} + + + {{ value }} + +
+ + +
diff --git a/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.scss b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.scss new file mode 100644 index 0000000..09cfada --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.scss @@ -0,0 +1,62 @@ +@mixin chipActionColor($color) { + border: 1px solid; + transition: var(--pi-animation); + + // Is Active + &.is-active { + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-100); + } + + &:not(.is-active):not(.is-disabled) { + background-color: var(--pi-color-white); + border-color: var(--pi-color-#{$color}-100); + color: var(--pi-color-#{$color}); + } + // Hover + &:hover { + cursor: pointer; + + &:not(.is-disabled) { + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-75); + color: var(--pi-color-#{$color}-900); + } + } +} + +:host { + & > span { + gap: var(--pi-spacing-x-small); + } + + &.pi-chip__affix-position--previous { + &.has-affix { + & > span { + & > div { + flex-direction: row-reverse; + } + } + } + } + // Colors + // Primary + &.pi-color__primary { + @include chipActionColor(primary); + } + + // Accent + &.pi-color__accent { + @include chipActionColor(accent); + } + + // Highlighted + &.pi-color__highlighted { + @include chipActionColor(highlighted); + } + + // Grey + &.pi-color__grey { + @include chipActionColor(grey); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.ts b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.ts new file mode 100644 index 0000000..52e246e --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-action/chip-action.component.ts @@ -0,0 +1,56 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass, mode, styleType } from '../../../styles'; +import { PiBaseChipComponent } from '../base-chip.component'; +import { chipColor } from '../entities/chip.model'; + +@Component({ + standalone: false, + selector: 'pi-chip-action', + templateUrl: './chip-action.component.html', + styleUrls: ['./chip-action.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipActionComponent extends PiBaseChipComponent { + @HostBinding('class') get actionClasses(): string { + const hasActionIconClass = this.hasActionIcon && 'has-action'; + const isActiveClass = this.isActive ? 'is-active' : ''; + /** + * When the chip action is dismissible, the affix position will be always previous, + that's why we force the position--previous class binding + */ + const isClosableAffixPositionClass = + this.isClosable && this.affixPosition === 'after' + ? 'pi-chip__affix-position--previous' + : `pi-chip__affix-position--${this.affixPosition}`; + const affixPositionClass = + this.affixPosition && `pi-chip__affix-position--${this.affixPosition}`; + + return [ + 'pi-chip-action', + ComponentFamilyClass.Chip, + hasActionIconClass, + isActiveClass, + isClosableAffixPositionClass, + affixPositionClass, + ].join(' '); + } + + @Output() execute: EventEmitter = new EventEmitter(); + + // Content + @Input() hasActionIcon: boolean = true; + + // Skin + @Input() override size: 'small' | 'medium' | 'large' = 'small'; + @Input() override color: chipColor = 'primary'; + + // Status + @Input() isActive!: boolean; +} diff --git a/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.html b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.html new file mode 100644 index 0000000..8fdcd1a --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.html @@ -0,0 +1,5 @@ + + {{ dateValue?.weekDay }} + {{ dateValue?.day }} + {{ dateValue?.month.toUpperCase() }} + diff --git a/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.scss b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.scss new file mode 100644 index 0000000..841e47a --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.scss @@ -0,0 +1,169 @@ +@mixin columnChipDateSize($size, $gap, $radius) { + &.pi-chip-date__flex-direction--column, + &.has-weekday.pi-chip-date__flex-direction--row { + width: $size; + height: $size; + border-radius: $radius; + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + & > span { + gap: $gap; + } + } +} + +@mixin rowChipDateSize($height, $padding, $radius) { + // Without Weekday + &:not(.has-weekday) { + // Flex direction + // Column + // Row + &.pi-chip-date__flex-direction--row { + height: $height; + padding-left: $padding; + padding-right: $padding; + border-radius: $radius; + -webkit-border-radius: $radius; + -moz-border-radius: $radius; + } + } +} + +@mixin chipDateFontSizes($day-size, $month-size) { + // Day + .pi-chip-date__day { + font: { + size: $day-size; + } + } + // Month + .pi-chip-date__month { + font: { + size: $month-size; + } + } +} + +@mixin chipDateColor($color) { + &.is-clickable { + cursor: pointer; + // Hover + &:hover { + &:not(.is-active):not(.is-disabled) { + background-color: var(--pi-color-#{$color}-75); + color: var(--pi-color-#{$color}); + } + } + // Is Active + &.is-active { + background-color: var(--pi-color-#{$color}-50); + border: 1px solid; + border-color: var(--pi-color-#{$color}-75); + } + + &:not(.is-active):not(.is-disabled) { + border: 1px solid; + border-color: var(--pi-color-#{$color}-75); + color: var(--pi-color-#{$color}); + background-color: var(--pi-color-white); + } + } + &:not(.is-clickable) { + background-color: var(--pi-color-#{$color}-25); + color: var(--pi-color-#{$color}); + &.is-stroked { + border: 1px solid; + border-color: var(--pi-color-#{$color}-50); + } + } +} + +@mixin chipDateFlexDirection { + &.pi-chip-date__flex-direction--column, + &.has-weekday.pi-chip-date__flex-direction--row { + & > span { + flex-direction: column; + align-items: center; + } + } + &:not(.has-weekday) { + &.pi-chip-date__flex-direction--row { + & > span { + flex-direction: row; + align-items: baseline; + } + } + } +} + +:host { + justify-content: center; + & > span { + display: flex; + justify-content: center; + gap: var(--pi-spacing-xxxx-small); + } + // Day + .pi-chip-date__day { + margin-bottom: unset; + font: { + weight: 600; + } + } + // Weekday + .pi-chip-date__weekday { + font: { + size: var(--pi-font-size-x-small); + } + } + // Colors + // Primary + &.pi-color__primary { + @include chipDateColor(primary); + } + // Accent + &.pi-color__accent { + @include chipDateColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include chipDateColor(highlighted); + } + // Grey + &.pi-color__grey { + @include chipDateColor(grey); + } + + // Flex direction + @include chipDateFlexDirection; + + // Sizes + // Small + &.pi-chip__size--small { + @include columnChipDateSize( + var(--pi-spacing-xxx-large), + var(--pi-spacing-xxxx-small), + 0.875rem + ); + @include rowChipDateSize(var(--pi-spacing-x-large), var(--pi-spacing-x-small), 0.5rem); + @include chipDateFontSizes(var(--pi-font-size-x-small), var(--pi-font-size-xxx-small)); + } + + // Medium + &.pi-chip__size--medium { + @include columnChipDateSize( + var(--pi-spacing-xxxx-large), + var(--pi-spacing-xxxx-small), + var(--pi-border-radius-medium) + ); + @include rowChipDateSize(var(--pi-spacing-xx-large), var(--pi-spacing-small), 0.75rem); + @include chipDateFontSizes(var(--pi-font-size-small), var(--pi-font-size-xx-small)); + } + + // Large (Has Weekday) + &.pi-chip__size--large, + &.has-weekday { + @include columnChipDateSize(4.5rem, var(--pi-spacing-xxx-small), 1.25rem); + @include chipDateFontSizes(var(--pi-font-size-large), var(--pi-font-size-xx-small)); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.ts b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.ts new file mode 100644 index 0000000..2c8d711 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-date/chip-date.component.ts @@ -0,0 +1,72 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass, theming } from '../../../styles'; +import { Date } from '../../../util/models'; +import { PiBaseChipComponent } from '../base-chip.component'; +import { DateTime } from 'luxon'; +import _ from 'lodash'; + +@Component({ + standalone: false, + selector: 'pi-chip-date', + templateUrl: './chip-date.component.html', + styleUrls: ['./chip-date.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipDateComponent extends PiBaseChipComponent { + @HostBinding('class') get dateClasses(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const isClickableClass = this.isClickable ? 'is-clickable' : ''; + const isStrokeClass = !this.isClickable && !!this.isStroked && 'is-stroked'; + const isActiveClass = !!this.isActive ? 'is-active' : ''; + const hasWeekDayClass = this.size === 'large' ? 'has-weekday' : ''; + const flexDirectionClass = + this.flexDirection && `pi-chip-date__flex-direction--${this.flexDirection}`; + return [ + 'pi-chip-date', + ComponentFamilyClass.Chip, + colorClass, + isClickableClass, + isStrokeClass, + isActiveClass, + hasWeekDayClass, + flexDirectionClass, + ].join(' '); + } + + @Output() click: EventEmitter = new EventEmitter(); + + // Content + @Input() date!: string; // ISO format in UTC time zone + + // Skin + @Input() override color: theming = 'primary'; + @Input() isStroked = false; + + // Size - weekday only available in LARGE + // No weekday - available in SMALL and MEDIUM + // row direction only available in S and M + @Input() override size: 'small' | 'medium' | 'large' = 'small'; + + @Input() flexDirection: 'row' | 'column' = 'column'; + + // Status + @Input() isClickable = true; + @Input() isActive!: boolean; + + get dateValue(): Date { + const dateObj = DateTime.fromISO(this.date).setLocale('es'); + const isDateToday = dateObj.hasSame(DateTime.now(), 'day'); + return { + day: dateObj.day.toString() || '', + month: dateObj.monthShort || '', + weekDay: isDateToday ? _.capitalize(dateObj.toRelativeCalendar()) : _.capitalize(dateObj.weekdayShort || ''), + }; + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.html b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.html new file mode 100644 index 0000000..2506344 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.html @@ -0,0 +1,6 @@ + + {{ label }} + + + + diff --git a/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.scss b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.scss new file mode 100644 index 0000000..ba12cb1 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.scss @@ -0,0 +1,28 @@ +:host { + // Affix position + // Previous + &.pi-chip__affix-position--previous { + & > span { + flex-direction: row-reverse; + } + } + // Color + $status-colors: ( + (success, success), + (error, warn), + (pending, accent), + (info, primary), + (disabled, grey) + ); + + @each $status, $color in $status-colors { + &.is-#{$status} { + color: var(--pi-color-#{$color}); + + // Fill + &.pi-chip-status__appearance--fill { + background-color: var(--pi-color-#{$color}-50); + } + } + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.ts b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.ts new file mode 100644 index 0000000..3aafa64 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-status/chip-status.component.ts @@ -0,0 +1,33 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +// Shared +import { status } from '../../../util/status'; +import { PiBaseChipComponent } from '../base-chip.component'; + +@Component({ + standalone: false, + selector: 'pi-chip-status', + templateUrl: './chip-status.component.html', + styleUrls: ['./chip-status.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipStatusComponent extends PiBaseChipComponent { + @HostBinding('class') get statusClasses(): string { + const statusClass = this.status && `is-${this.status}`; + const appearanceClass = `pi-chip-status__appearance--${this.appearance}`; + const hasLabelClass = !this.label ? 'pi-chip__single-icon' : ''; + const affixPositionClass = + this.affixPosition && `pi-chip__affix-position--${this.affixPosition}`; + return ['pi-chip-status', statusClass, appearanceClass, hasLabelClass, affixPositionClass].join( + ' ' + ); + } + + // Skin + @Input() appearance: 'ghost' | 'fill' = 'fill'; + @Input() override affixPosition: 'previous' | 'after' = 'previous'; + @Input() override size: 'small' | 'medium' = 'small'; + + /* green | red | yellow | blue | opacity */ + @Input() status!: status; +} diff --git a/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.html b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.html new file mode 100644 index 0000000..18b66e6 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.html @@ -0,0 +1,28 @@ + +
+ + {{ timeValue }}h + + {{ dateValue }} +
+ + +
+ + + +
+ + {{ timeValue }}h + + {{ dateValue }} + + +
+
+
diff --git a/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.scss b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.scss new file mode 100644 index 0000000..d237174 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.scss @@ -0,0 +1,154 @@ +@mixin chipTimeSizes($min-width, $side-padding, $ghost-padding-left, $font-size) { + gap: var(--pi-spacing-x-small); + &.has-affix { + min-width: $min-width; + padding-left: var(--pi-spacing-#{$side-padding}); + padding-right: var(--pi-spacing-#{$side-padding}); + & > span { + gap: var(--pi-spacing-small); + + .pi-chip-affix__value { + font: { + size: var(--pi-font-size-#{$font-size}); + } + } + } + &.pi-chip__affix-appearance--fill { + padding-left: var(--pi-spacing-#{$side-padding}); + } + &.is-closable { + &.pi-chip__affix-appearance--ghost { + & > span { + padding-left: var(--pi-spacing-#{$ghost-padding-left}); + } + } + } + } +} + +@mixin chipTimeCommonColor($color) { + &:not([class*='white']) { + &:not(.is-active):not(.is-disabled) { + color: var(--pi-color-#{$color}); + } + // Hover + &:hover { + &:not(.is-disabled) { + &.is-closable { + cursor: pointer; + color: var(--pi-color-#{$color}-900); + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-75); + } + } + } + // Is Active + &.is-active { + background-color: var(--pi-color-#{$color}-50); + border-color: var(--pi-color-#{$color}-100); + } + + // NOT Has affix + &:not(.has-affix) { + background-color: var(--pi-color-#{$color}-50); + } + + // Has affix + &.has-affix { + border: 1px solid; + &:not(.is-active):not(.is-disabled) { + border-color: var(--pi-color-#{$color}-100); + } + } + } +} + +@mixin chipTimeWhiteColor($color) { + &[class*='white'] { + background-color: var(--pi-color-white); + &:not(.is-active):not(.is-disabled) { + color: var(--pi-color-#{$color}); + } + // Hover + &:hover { + &:not(.is-disabled) { + &.is-closable { + cursor: pointer; + color: var(--pi-color-#{$color}-900); + border-color: var(--pi-color-#{$color}-75); + } + } + } + // Is Active + &.is-active { + border-color: var(--pi-color-#{$color}-100); + } + + // Has affix + &.has-affix { + border: 1px solid; + &:not(.is-active):not(.is-disabled) { + border-color: var(--pi-color-#{$color}-100); + } + } + } +} +:host { + justify-content: center; + + // Affix appearance + // Fill + &.pi-chip__affix-appearance--fill { + & > span { + justify-content: space-between; + } + .pi-chip-affix { + min-width: var(--pi-spacing-xxxx-large); + } + } + + // Colors + // Primary + &.pi-color__primary { + @include chipTimeCommonColor(primary); + } + &.pi-color__primary-white { + @include chipTimeWhiteColor(primary); + } + + // Accent + &.pi-color__accent { + @include chipTimeCommonColor(accent); + } + &.pi-color__accent-white { + @include chipTimeWhiteColor(accent); + } + + // Highlighted + &.pi-color__highlighted { + @include chipTimeCommonColor(highlighted); + } + // Highlighted + &.pi-color__highlighted-white { + @include chipTimeWhiteColor(highlighted); + } + + // Grey + &.pi-color__grey { + @include chipTimeCommonColor(grey); + } + // Highlighted + &.pi-color__highlighted-white { + @include chipTimeWhiteColor(grey-white); + } + + // Sizes + // Small + &.pi-chip__size--small { + @include chipTimeSizes(10rem, xxxx-small, x-small, xx-smal); + } + // Medium + &.pi-chip__size--medium { + @include chipTimeSizes(12rem, xxx-small, small, x-small); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.ts b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.ts new file mode 100644 index 0000000..325949e --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-time/chip-time.component.ts @@ -0,0 +1,66 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { DateTime } from 'luxon'; +import { PiBaseChipComponent } from '../base-chip.component'; +import { chipThemingColor } from '../entities/chip.model'; + +@Component({ + standalone: false, + selector: 'pi-chip-time', + templateUrl: './chip-time.component.html', + styleUrls: ['./chip-time.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipTimeComponent extends PiBaseChipComponent { + @HostBinding('class') get dateClasses(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const isActiveClass = this.isActive ? 'is-active' : ''; + const affixClass = this.time && this.date ? 'has-affix' : ''; + + return ['pi-chip-time', ComponentFamilyClass.Chip, colorClass, isActiveClass, affixClass].join( + ' ' + ); + } + + @Output() execute: EventEmitter = new EventEmitter(); + + // Content + @Input() date!: string; // ISO format in UTC time zone + @Input() time!: string; // ISO format in UTC time zone + @Input() timezone: string = 'utc'; + + // Skin + @Input() override color: chipThemingColor = 'primary'; + + /** + * On the chip time cases, we override the affix position property because + it will be always used by the thime input and will be always previous + */ + @Input() override affixPosition: 'previous' = 'previous'; + + // time - available in S and M + // time + date - available in S + @Input() override size: 'small' | 'medium' = 'small'; + + // Status + @Input() isActive!: boolean; + + get dateObj(): DateTime { + return DateTime.fromISO(this.date, { zone: 'utc' }); + } + + get timeValue(): string { + return this.dateObj.setZone(this.timezone).toFormat('H:mm'); + } + + get dateValue(): string { + return this.dateObj.setLocale('es').toLocaleString(DateTime.DATE_MED); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.html b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.html new file mode 100644 index 0000000..95e6164 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.html @@ -0,0 +1,11 @@ + + + + + + diff --git a/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.scss b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.scss new file mode 100644 index 0000000..d38f9aa --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.scss @@ -0,0 +1,208 @@ +// Structure +// Chip value body +@mixin chipValue { + // Has label + &.has-label { + display: flex; + align-items: center; + justify-content: flex-end; + } +} + +// Font size +@mixin chipValueFontSizes($value-font-size, $label-font-size, $unit-font-size) { + // NOT Has label + &:not(.has-label) { + // Affix + .pi-chip-affix { + font: { + size: var(--pi-font-size-#{$value-font-size}); + } + } + } + // Has label + &.has-label { + // Label + .pi-chip-label { + font: { + size: var(--pi-font-size-#{$label-font-size}); + } + } + // Affix + .pi-chip-affix { + // Value + & > .pi-chip-affix__value { + font: { + size: var(--pi-font-size-#{$value-font-size}); + } + } + // Unit + & > .pi-chip-affix__unit { + font: { + size: var(--pi-font-size-#{$unit-font-size}); + } + } + } + } +} + +// Chip Size +@mixin chipValueSize( + $gap, + $chip-has-label-height, + $chip-height, + $padding-left, + $padding-right, + $chip-border-radius, + $affix-border-radius +) { + // Has label + &.has-label { + height: var(--pi-spacing-#{$chip-has-label-height}); + padding-left: var(--pi-spacing-#{$padding-left}); + padding-right: var(--pi-spacing-#{$padding-right}); + border-radius: $chip-border-radius; + -webkit-border-radius: $chip-border-radius; + -moz-border-radius: $chip-border-radius; + & > span { + gap: var(--pi-spacing-#{$gap}); + } + // Label + .pi-chip-label { + margin-top: var(--pi-spacing-xxxxx-small); + } + } + // NOT Has label + &:not(.has-label) { + height: var(--pi-spacing-#{$chip-height}); + border-radius: $affix-border-radius; + -webkit-border-radius: $affix-border-radius; + -moz-border-radius: $affix-border-radius; + } +} + +@mixin chipValueAffixSize($gap, $height, $min-width, $border-radius) { + & > span { + // Affix wrapper + & > aside { + height: $height; + min-width: $min-width; + border-radius: $border-radius; + + & > .pi-chip-affix { + gap: var(--pi-spacing-#{$gap}); + } + } + } +} + +@mixin chipValueAffixSideSpacings($side-spacing) { + .pi-chip-affix { + padding-left: var(--pi-spacing-#{$side-spacing}); + padding-right: var(--pi-spacing-#{$side-spacing}); + } +} + +// Affix +@mixin chipValueAffix { + & > span { + // Affix wrapper + & > aside { + display: flex; + justify-content: center; + align-items: center; + + & > .pi-chip-affix { + align-items: baseline; + } + } + } + // NOT Has label + &:not(.has-label) { + .pi-chip-affix { + font: { + weight: 600; + } + } + } + // Has label + &.has-label { + .pi-chip-affix { + & > .pi-chip-affix__value { + font: { + weight: 600; + } + } + } + } +} + +@mixin chipValueUnitPosition { + &.pi-chip-value__unit-position--previous { + & > span { + & > aside { + & > .pi-chip-affix { + flex-direction: row-reverse; + } + } + } + } +} + +// Color +@mixin chipValueColor($color) { + // Affix + & > span { + & > aside { + background-color: var(--pi-color-#{$color}-25); + color: var(--pi-color-#{$color}); + } + } + + // Has label + &.has-label { + background-color: var(--pi-color-#{$color}-10); + color: var(--pi-color-#{$color}); + + & > span { + label { + color: var(--pi-color-#{$color}); + } + } + } +} + +:host { + // Structure + @include chipValue; + + // Affix + @include chipValueAffix; + // Affix unit + @include chipValueUnitPosition; + + // Size + // Medium + &.pi-chip__size--medium { + @include chipValueFontSizes(small, x-small, x-small); + @include chipValueSize(xx-small, xxx-large, x-large, small, xxx-small, 0.875rem, 0.75rem); + @include chipValueAffixSize(xxxxx-small, var(--pi-spacing-xx-large), 3.6rem, 0.75rem); + @include chipValueAffixSideSpacings(xx-small); + } + // Large + &.pi-chip__size--large { + @include chipValueFontSizes(medium, small, small); + @include chipValueSize(xx-small, xxxx-large, xxx-large, medium, xxx-small, 1.125rem, 0.875rem); + @include chipValueAffixSize(xxxx-small, 2.5rem, 4.25rem, 0.875rem); + @include chipValueAffixSideSpacings(x-small); + } + // Colors + // Success + &.pi-color__success { + @include chipValueColor(success); + } + // Warn + &.pi-color__warn { + @include chipValueColor(warn); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.ts b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.ts new file mode 100644 index 0000000..28bae84 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip-value/chip-value.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { PiBaseChipComponent } from '../base-chip.component'; +import { unit } from '../../../util/models'; + +@Component({ + standalone: false, + selector: 'pi-chip-value', + templateUrl: './chip-value.component.html', + styleUrls: ['./chip-value.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipValueComponent extends PiBaseChipComponent { + @HostBinding('class') get valueClasses(): string { + const colorClass = this.value > 0 ? 'pi-color__success' : 'pi-color__warn'; + const hasLabelClass = !!this.label ? 'has-label' : ''; + const unitPositionClass = + this.unitPosition && `pi-chip-value__unit-position--${this.unitPosition}`; + const affixPositionClass = + this.affixPosition && `pi-chip__affix-position--${this.affixPosition}`; + return ['pi-chip-value', colorClass, hasLabelClass, unitPositionClass, affixPositionClass].join( + ' ' + ); + } + + // Content + @Input() override value!: number; + @Input() unit!: unit | string; + + // Skin + @Input() override size: 'medium' | 'large' = 'large'; + + // Unit + @Input() unitPosition: 'previous' | 'after' = 'after'; +} diff --git a/libs/ui/src/lib/ui/chip/chip/chip.component.html b/libs/ui/src/lib/ui/chip/chip/chip.component.html new file mode 100644 index 0000000..140087a --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip/chip.component.html @@ -0,0 +1,23 @@ + + {{ label }} + + + + + + + + + {{ value }} + + diff --git a/libs/ui/src/lib/ui/chip/chip/chip.component.scss b/libs/ui/src/lib/ui/chip/chip/chip.component.scss new file mode 100644 index 0000000..d5f0dcc --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip/chip.component.scss @@ -0,0 +1,37 @@ +@mixin chipColor($color) { + // Outline + &.pi-chip__appearance--outline { + border: 1px solid; + background-color: var(--pi-color-white); + color: var(--pi-color-#{$color}); + border-color: var(--pi-color-#{$color}-50); + } +} + +:host { + // Colors + // Primary + &.pi-color__primary { + @include chipColor(primary); + } + + // Accent + &.pi-color__accent { + @include chipColor(accent); + } + + // Warn + &.pi-color__warn { + @include chipColor(warn); + } + + // Success + &.pi-color__success { + @include chipColor(success); + } + + // Grey + &.pi-color__grey { + @include chipColor(grey); + } +} diff --git a/libs/ui/src/lib/ui/chip/chip/chip.component.ts b/libs/ui/src/lib/ui/chip/chip/chip.component.ts new file mode 100644 index 0000000..1d1e9dc --- /dev/null +++ b/libs/ui/src/lib/ui/chip/chip/chip.component.ts @@ -0,0 +1,28 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { theming } from '../../../styles'; +import { PiBaseChipComponent } from '../base-chip.component'; + +@Component({ + standalone: false, + selector: 'pi-chip', + templateUrl: './chip.component.html', + styleUrls: ['./chip.component.scss', '../styles/base-chip.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiChipComponent extends PiBaseChipComponent { + @HostBinding('class') get chipClasses(): string { + const appearanceClass = this.appearance && `pi-chip__appearance--${this.appearance}`; + const iconTypeClass = this.iconType && `pi-chip__icon-type--${this.iconType}`; + const hasLabelClass = !this.label ? 'pi-chip__single-icon' : ''; + const affixPositionClass = + this.affixPosition && `pi-chip__affix-position--${this.affixPosition}`; + return [appearanceClass, iconTypeClass, hasLabelClass, affixPositionClass].join(' '); + } + + // Content + @Input() iconType: 'custom' | 'default' = 'default'; + + // Skin + @Input() appearance: 'outline' | 'fill' = 'outline'; + @Input() override color: theming = 'primary'; +} diff --git a/libs/ui/src/lib/ui/chip/entities/chip.model.ts b/libs/ui/src/lib/ui/chip/entities/chip.model.ts new file mode 100644 index 0000000..e99ae3e --- /dev/null +++ b/libs/ui/src/lib/ui/chip/entities/chip.model.ts @@ -0,0 +1,25 @@ +// Skin +export type chipColor = + | 'accent-white' + | 'accent' + | 'grey' + | 'grey-white' + | 'highlighted-white' + | 'highlighted' + | 'primary-white' + | 'primary' + | 'success-white' + | 'success' + | 'warn-white' + | 'warn'; + +// Skin +export type chipThemingColor = + | 'accent-white' + | 'accent' + | 'grey' + | 'grey-white' + | 'highlighted-white' + | 'highlighted' + | 'primary-white' + | 'primary'; diff --git a/libs/ui/src/lib/ui/chip/index.ts b/libs/ui/src/lib/ui/chip/index.ts new file mode 100644 index 0000000..771fed5 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/index.ts @@ -0,0 +1,9 @@ +export * from './ui-chip.module'; + +export * from './chip/chip.component'; +export * from './chip-action/chip-action.component'; +export * from './chip-date/chip-date.component'; +export * from './chip-status/chip-status.component'; +export * from './chip-value/chip-value.component'; +export * from './chip-time/chip-time.component'; +export * from './tile/tile.component'; diff --git a/libs/ui/src/lib/ui/chip/styles/_color.scss b/libs/ui/src/lib/ui/chip/styles/_color.scss new file mode 100644 index 0000000..040053c --- /dev/null +++ b/libs/ui/src/lib/ui/chip/styles/_color.scss @@ -0,0 +1,117 @@ +@mixin baseChipFontColor($color) { + // Mode light, default + &:not(.pi-color__mode--dark) { + label, + span { + color: var(--pi-color-#{$color}); + } + } + // Mode dark + &.pi-color__mode--dark { + label, + span { + color: var(--pi-color-white); + } + } +} + +@mixin baseChipBackgroundColor($color) { + &:not(.pi-chip-value):not(.pi-chip-date) { + // Mode light, default + &:not(.pi-color__mode--dark) { + // Fill + &.pi-chip__appearance--fill { + background-color: var(--pi-color-#{$color}-25); + } + + // Outline + &.pi-chip__appearance--outline { + background-color: var(--pi-color-#{$color}-10); + } + + // Has value + &.has-affix { + // NOT Density + &.pi-chip__affix-appearance--fill { + // Is clickable + &:not(.pi-chip-action):not(.pi-chip-time) { + // Affix + .pi-chip-affix { + background-color: var(--pi-color-#{$color}-50); + } + } + // NOT Is clickable + &.pi-chip-action, + &.pi-chip-time { + &:hover { + transition: var(--pi-animation); + // Affix + .pi-chip-affix { + background-color: var(--pi-color-#{$color}-75); + } + } + &:not(:hover) { + // Affix + .pi-chip-affix { + background-color: var(--pi-color-#{$color}-50); + } + } + } + } + } + } + + // Mode dark + &.pi-color__mode--dark { + background-color: var(--pi-color-#{$color}-300); + // Has value + &.has-affix { + &.pi-chip__affix-appearance--fill { + // Affix + .pi-chip-affix { + color: var(--pi-color-#{$color}); + background-color: var(--pi-color-white); + } + } + } + } + } +} + +:host { + // Colors + &.pi-color__primary { + @include baseChipFontColor(primary); + @include baseChipBackgroundColor(primary); + } + + &.pi-color__accent { + @include baseChipFontColor(accent); + @include baseChipBackgroundColor(accent); + } + + &.pi-color__success { + @include baseChipFontColor(success); + @include baseChipBackgroundColor(success); + } + + &.pi-color__warn { + @include baseChipFontColor(warn); + @include baseChipBackgroundColor(warn); + } + + &.pi-color__black { + @include baseChipFontColor(grey-900); + @include baseChipBackgroundColor(grey-900); + } + + &.pi-color__grey { + @include baseChipFontColor(grey); + @include baseChipBackgroundColor(grey); + } + + &.pi-color__highlighted { + @include baseChipFontColor(highlighted); + @include baseChipBackgroundColor(highlighted); + } +} diff --git a/libs/ui/src/lib/ui/chip/styles/_position.scss b/libs/ui/src/lib/ui/chip/styles/_position.scss new file mode 100644 index 0000000..17e539e --- /dev/null +++ b/libs/ui/src/lib/ui/chip/styles/_position.scss @@ -0,0 +1,34 @@ +// Position +@mixin chipAffixPosition { + &:not(.pi-chip-date):not(.pi-chip-value):not(.pi-chip-status):not(.pi-chip-time):not( + .pi-chip-action + ) { + // Has affix + &.has-affix { + // Affix position previous + &.pi-chip__affix-position--previous { + justify-content: flex-end; + & > span { + flex-direction: row-reverse; + } + } + } + } +} + +// Position +@mixin chipMatIconPosition { + &:not(.pi-chip-date):not(.pi-chip-value) { + .mat-icon { + display: flex; + justify-content: flex-start; + align-items: center; + } + } +} + +:host { + // Position + @include chipAffixPosition; + @include chipMatIconPosition; +} diff --git a/libs/ui/src/lib/ui/chip/styles/_size.scss b/libs/ui/src/lib/ui/chip/styles/_size.scss new file mode 100644 index 0000000..4411ca1 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/styles/_size.scss @@ -0,0 +1,281 @@ +// Size +@mixin chipSize($chip-height, $chip-border-radius) { + &:not(.pi-chip-date):not(.pi-chip-value) { + height: var(--pi-spacing-#{$chip-height}); + border-radius: $chip-border-radius; + } +} + +// Single Icon Size +@mixin chipSingleIconSize($chip-size, $icon-size, $border-radius) { + &:not(.pi-chip-date):not(.pi-chip-value):not(.pi-chip-time) { + // Single Icon + &.pi-chip__single-icon { + height: var(--pi-spacing-#{$chip-size}); + width: var(--pi-spacing-#{$chip-size}); + border-radius: $border-radius; + + .pi-chip-affix { + height: var(--pi-spacing-#{$chip-size}); + width: var(--pi-spacing-#{$chip-size}); + border-radius: $border-radius; + // Icon + & > .pi-chip-affix__icon { + height: var(--pi-spacing-#{$icon-size}); + width: var(--pi-spacing-#{$icon-size}); + } + } + } + } +} + +/** +* This mixin manages the left and right padding from the common content to the sides. +*/ +@mixin chipSideSpacings($padding, $gap) { + &:not(.pi-chip-date):not(.pi-chip-value):not(.pi-chip__single-icon) { + &:not(.has-affix), + &.pi-chip-action.has-affix { + padding-left: var(--pi-spacing-#{$padding}); + padding-right: var(--pi-spacing-#{$padding}); + } + &.pi-chip-action { + &.has-affix { + .pi-chip-close-icon { + margin-left: var(--pi-spacing-#{$padding}); + } + } + } + + &.pi-chip-time { + &.is-closable { + padding-right: var(--pi-spacing-#{$padding}); + } + } + + &:not(.has-action) { + &.has-affix { + // Previous + &.pi-chip__affix-position--previous { + padding-right: var(--pi-spacing-#{$padding}); + } + // After + &.pi-chip__affix-position--after { + padding-left: var(--pi-spacing-#{$padding}); + } + + &.pi-chip__affix-appearance--ghost { + // Previous + &.pi-chip__affix-position--previous { + padding-left: calc( + var(--pi-spacing-#{$padding}) - var(--pi-spacing-#{$gap}) - + var(--pi-spacing-xxxx-small) + ); + } + // After + &.pi-chip__affix-position--after { + padding-right: calc( + var(--pi-spacing-#{$padding}) - var(--pi-spacing-#{$gap}) - + var(--pi-spacing-xxxx-small) + ); + } + } + } + } + + &.has-affix { + &.pi-chip__affix-appearance--fill { + & > span { + gap: var(--pi-spacing-#{$gap}); + } + } + &.pi-chip__affix-appearance--ghost { + & > span { + gap: calc(var(--pi-spacing-#{$gap}) - var(--pi-spacing-xxxxx-small)); + } + } + } + } +} + +// Font sizes +@mixin chipFontSize($size) { + &:not(.pi-chip-date):not(.pi-chip-value):not(.pi-chip__single-icon) { + .pi-chip-label, + span { + font: { + size: var(--pi-font-size-#{$size}); + } + } + // Has value + .pi-chip-affix { + text-align: center; + font: { + weight: 600; + } + } + } +} + +// Affix size +@mixin chipAffixSize($size, $affix-border-radius, $icon-padding, $text-side-padding) { + &:not(.pi-chip-date):not(.pi-chip-value) { + .pi-chip-affix { + // Icon, Value or Time + & > [class*='pi-chip-affix__'] { + display: flex; + justify-content: center; + align-items: center; + } + + // Value or Time + & > [class*='pi-chip-affix__']:not(.pi-chip-affix__icon) { + height: $size; + text-align: center; + } + } + + // NOT Chip Single Icon + &:not(.pi-chip__single-icon) { + .pi-chip-affix { + height: $size; + border-radius: $affix-border-radius; + } + } + + // NOT Chip time + &:not(.pi-chip-time) { + .pi-chip-affix { + min-width: calc(#{$size} + var(--pi-spacing-xxxx-small)); + } + } + // NOT Chip time affix appearance ghost + &:not(.pi-chip-time.pi-chip__affix-appearance--ghost) { + .pi-chip-affix { + // Value or Time + & > [class*='pi-chip-affix__']:not(.pi-chip-affix__icon) { + padding-left: var(--pi-spacing-#{$text-side-padding}); + padding-right: var(--pi-spacing-#{$text-side-padding}); + } + } + } + + // NOT Chip Single Icon & Chip Time + &:not(.pi-chip__single-icon):not(.pi-chip-time) { + .pi-chip-affix { + min-width: calc(#{$size} + var(--pi-spacing-xxxx-small)); + // Icon + & > .pi-chip-affix__icon { + height: calc(#{$size} - var(--pi-spacing-#{$icon-padding})); + width: calc(#{$size} - var(--pi-spacing-#{$icon-padding})); + } + } + } + } +} + +/** +* This mixin manages the left and right padding from the affix, fill and ghost, to the sides. +*/ +@mixin chipAffixSideSpacings($affix-side-spacing) { + &:not(.pi-chip-date):not(.pi-chip-value):not(.pi-chip__single-icon) { + &.has-affix { + // Previous + &.pi-chip__affix-position--previous { + padding-left: var(--pi-spacing-#{$affix-side-spacing}); + } + /** + * After position does not includes the chip time & the chip action when it's closable. + */ + &:not(.pi-chip-time):not(.is-closable), + &:not(.pi-chip-action.is-closable) { + // After + &.pi-chip__affix-position--after:not(.pi-chip__icon-type--custom) { + padding-right: var(--pi-spacing-#{$affix-side-spacing}); + } + } + } + } +} + +// Close Action Sizes +@mixin chipCloseIconSize($icon-size) { + .pi-chip-close-icon { + height: var(--pi-spacing-#{$icon-size}); + width: var(--pi-spacing-#{$icon-size}); + } +} + +:host { + // Chip Small + &.pi-chip__size--small { + // Small Chip size + @include chipSize(x-large, 0.5rem); + + // Small Chip side spacings + @include chipSideSpacings(x-small, xxx-small); + + // Small Chip Single Icon Size + @include chipSingleIconSize(x-large, medium, 0.625rem); + + // Small Chip font size + @include chipFontSize(x-small); + + // Small Chip Affix Size + @include chipAffixSize(1.125rem, 0.375rem, xxxx-small, xx-small); + + // Small Chip Affix Side Spacings + @include chipAffixSideSpacings(xxxx-small); + + // Small Chip Close Icon Size + @include chipCloseIconSize(x-small); + } + + // Chip Medium + &.pi-chip__size--medium { + // Medium Chip size + @include chipSize(xx-large, 0.75rem); + + // Medium Chip side spacings + @include chipSideSpacings(small, xxx-small); + + // Medium Chip Single Icon Size + @include chipSingleIconSize(xx-large, large, 0.75rem); + + // Medium Chip font size + @include chipFontSize(x-small); + + // Medium Chip Affix Size + @include chipAffixSize(var(--pi-spacing-x-large), 0.5rem, xx-small, xx-small); + + // Medium Chip Affix Side Spacings + @include chipAffixSideSpacings(xxx-small); + + // Medium Chip Close Icon Size + @include chipCloseIconSize(small); + } + + // Chip Large + &.pi-chip__size--large { + // Large Chip size + @include chipSize(xxx-large, 0.875rem); + + // Large Chip side spacings + @include chipSideSpacings(medium, xx-small); + + // Large Chip Single Icon Size + @include chipSingleIconSize(xxx-large, x-large, 0.875rem); + + // Large Chip font size + @include chipFontSize(medium); + + // Large Chip Affix Size + @include chipAffixSize(1.875rem, 0.625rem, x-small, x-small); + + // Large Chip Affix Side Spacings + @include chipAffixSideSpacings(xxx-small); + + // Large Chip Close Icon Size + @include chipCloseIconSize(small); + } +} diff --git a/libs/ui/src/lib/ui/chip/styles/base-chip.component.scss b/libs/ui/src/lib/ui/chip/styles/base-chip.component.scss new file mode 100644 index 0000000..4b2df9f --- /dev/null +++ b/libs/ui/src/lib/ui/chip/styles/base-chip.component.scss @@ -0,0 +1,75 @@ +@use 'color'; +@use 'position'; +@use 'size'; + +// Common Chip styles +:host { + display: flex; + align-items: center; + width: fit-content; + & > span { + display: flex; + align-items: center; + line-height: 1; + } + + .pi-chip-affix { + display: flex; + justify-content: center; + } + + &:not(.pi-chip-value) { + .pi-chip-affix { + align-items: center; + } + } + + // Status + // Disabled + &.is-disabled { + opacity: 0.5; + cursor: default; + } + + .mat-icon, + img { + transition: var(--pi-animation); + } + &:hover { + .pi-chip-close-icon { + transform: rotate(90deg); + } + } + + &:not(.pi-chip__single-icon):not(.pi-chip-date__flex-direction--column) { + &.pi-chip__fill--fill-available { + width: -webkit-fill-available; + } + &:not(.pi-chip__fill--fill-available) { + width: fit-content; + } + } + &.pi-chip-action, + &.pi-chip-time { + & > span { + width: -webkit-fill-available; + & > div { + display: flex; + justify-content: center; + align-items: center; + gap: var(--pi-spacing-xx-small); + } + } + // Ghost + &.pi-chip__affix-appearance--ghost { + & > span { + & > div { + flex: 1 1 auto; + } + & > .pi-chip-close-icon { + flex: 0 0 auto; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/chip/tile/tile.component.html b/libs/ui/src/lib/ui/chip/tile/tile.component.html new file mode 100644 index 0000000..45edaf0 --- /dev/null +++ b/libs/ui/src/lib/ui/chip/tile/tile.component.html @@ -0,0 +1,11 @@ +
+
+ + + {{ value }} + +
+ {{ value }} +
+ + diff --git a/libs/ui/src/lib/ui/chip/tile/tile.component.scss b/libs/ui/src/lib/ui/chip/tile/tile.component.scss new file mode 100644 index 0000000..46d1cab --- /dev/null +++ b/libs/ui/src/lib/ui/chip/tile/tile.component.scss @@ -0,0 +1,127 @@ +@mixin tileColor($color) { + color: var(--pi-color-#{$color}); + background-color: var(--pi-color-#{$color}-25); + & > label { + color: var(--pi-color-#{$color}); + } + & > header { + & > div { + background-color: var(--pi-color-#{$color}-75); + } + } +} + +@mixin tileSpacings($gap, $padding, $border-radius) { + gap: var(--pi-spacing-#{$gap}); + padding: var(--pi-spacing-#{$padding}); + border-radius: $border-radius; + -webkit-border-radius: $border-radius; + -moz-border-radius: $border-radius; +} + +@mixin tileSizes($height, $width) { + height: $height; + width: $width; +} + +:host { + display: flex; + & > header { + & > div { + display: flex; + justify-content: space-between; + align-items: flex-start; + width: -webkit-fill-available; + & > span { + display: flex; + line-height: 1; + font: { + size: var(--pi-font-size-large); + weight: 700; + } + } + } + } + & > label { + font: { + size: var(--pi-font-size-x-small); + } + } + & > header { + & > div { + display: flex; + justify-content: center; + align-items: center; + padding: var(--pi-spacing-xx-small); + border-radius: 0.875rem; + -webkit-border-radius: 0.875rem; + -moz-border-radius: 0.875rem; + } + } + + // Value + &.has-value { + span { + display: flex; + justify-content: center; + align-items: center; + height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + font: { + weight: 600; + } + } + } + + &.has-icon-and-value { + } + + // Flex Direction + // Column + &.pi-tile__flex-direction--column { + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + @include tileSizes(6.375rem, 8.75rem); + @include tileSpacings(xx-small, small, 1.1875rem); + } + // Row + &.pi-tile__flex-direction--row { + align-items: center; + + @include tileSpacings(xx-small, xx-small, var(--pi-border-radius-medium)); + } + + // Color + // Primary + &.pi-color__primary { + @include tileColor(primary); + } + // Accent + &.pi-color__accent { + @include tileColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include tileColor(highlighted); + } +} + +html { + &.is-mobile { + :host { + // Row + &.pi-tile__flex-direction--row { + @include tileSizes(auto, -webkit-fill-available); + } + } + } + &:not(.is-mobile) { + :host { + // Row + &.pi-tile__flex-direction--row { + @include tileSizes(auto, 12.125rem); + } + } + } +} diff --git a/libs/ui/src/lib/ui/chip/tile/tile.component.ts b/libs/ui/src/lib/ui/chip/tile/tile.component.ts new file mode 100644 index 0000000..560ad2e --- /dev/null +++ b/libs/ui/src/lib/ui/chip/tile/tile.component.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { ComponentFamilyClass, theming } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-tile', + templateUrl: './tile.component.html', + styleUrls: ['./tile.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiTileComponent { + @HostBinding('class') get classes(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const hasIconClass = !this.icon ? 'has-value' : ''; + const hasIconAndValueClass = this.icon && this.value ? 'has-icon-and-value' : ''; + const flexDirection = this.flexDirection && `pi-tile__flex-direction--${this.flexDirection}`; + return ['pi-tile', ComponentFamilyClass.Text, colorClass, flexDirection, hasIconClass].join( + ' ' + ); + } + + // Content + @Input() label: string | undefined; + @Input() value: string | undefined; + @Input() icon: string | undefined; + + // Skin + @Input() color: theming = 'primary'; + @Input() flexDirection: 'row' | 'column' = 'column'; +} diff --git a/libs/ui/src/lib/ui/chip/ui-chip.module.ts b/libs/ui/src/lib/ui/chip/ui-chip.module.ts new file mode 100644 index 0000000..93812be --- /dev/null +++ b/libs/ui/src/lib/ui/chip/ui-chip.module.ts @@ -0,0 +1,41 @@ +// Angular +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MatIconModule } from '@angular/material/icon'; + +// Components +import { PiChipComponent } from './chip/chip.component'; +import { PiChipStatusComponent } from './chip-status/chip-status.component'; +import { PiTileComponent } from './tile/tile.component'; +import { PiChipValueComponent } from './chip-value/chip-value.component'; +import { PiChipDateComponent } from './chip-date/chip-date.component'; +import { PiChipTimeComponent } from './chip-time/chip-time.component'; +import { PiChipActionComponent } from './chip-action/chip-action.component'; +import { PiSharedUtilAffixModule } from '../../util/affix'; +import { PiSharedUtilValueModule } from '../../util/value'; + +const MATERIAL_IMPORTS = [MatIconModule]; + +@NgModule({ + imports: [CommonModule, MATERIAL_IMPORTS, PiSharedUtilAffixModule, PiSharedUtilValueModule], + declarations: [ + PiChipComponent, + PiChipActionComponent, + PiChipDateComponent, + PiChipStatusComponent, + PiChipValueComponent, + PiChipTimeComponent, + PiTileComponent, + ], + exports: [ + PiChipComponent, + PiChipActionComponent, + PiChipDateComponent, + PiChipStatusComponent, + PiChipValueComponent, + PiChipTimeComponent, + PiTileComponent, + ], +}) +export class PiSharedUIChipModule {} diff --git a/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.html b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.html new file mode 100644 index 0000000..e21aab1 --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.html @@ -0,0 +1,92 @@ + +
+ +
+ + + + + +
+ + + + + + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+ + +
+
+
+ + + + Template Error + diff --git a/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts new file mode 100644 index 0000000..a183de8 --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/data-grid/data-grid.component.ts @@ -0,0 +1,212 @@ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + EventEmitter, + HostBinding, + Input, + OnInit, + Output, + TemplateRef, + ViewChild, +} from '@angular/core'; +import { justify } from '../../../styles'; +import { ListMode } from '../../../util/list-configs'; +import { cardType, columnsNumber, rowsNumber } from '../entities/data-grid.model'; + +@Component({ + standalone: false, + selector: 'pi-data-grid', + templateUrl: './data-grid.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiDataGridComponent { + @HostBinding('class') get classes(): string { + return [ + 'pi-data-grid', + this.appearanceClass, + this.cardTypeClass, + this.columnsClass, + this.dataDirectionClass, + this.isEditableClass, + this.isExpandableClass, + this.justifyClass, + this.modeClass, + this.rowsClass, + ].join(' '); + } + + // Content + @Input() data: Array | null = []; + @Input() columns!: columnsNumber | number | null; + @Input() mode: ListMode | null = 'list'; + @Input() content: 'key' | 'values' = 'values'; + /** + * dataDirection = colummn: 'headings will be dispplayed at left side' + * dataDirection = row: 'headings will be dispplayed at top' + **/ + + @Input() rows!: rowsNumber | null; + @Input() index!: number; + // Status + @Input() isActive!: boolean; + // Behavior + @Input() isExpandable = false; + // Behavior + @Input() isEditable = false; + + // Skin + @Input() dataDirection: 'row' | 'column' = 'row'; + @Input() cardType: cardType | null = 'vertical'; + @Input() appearance: 'outline' | 'ghost' = 'outline'; + @Input() hasPdding = true; + @Input() justify: justify = 'space-between'; + + @Output() clickItem: EventEmitter = new EventEmitter(); + + @ViewChild('gridItem', { static: false }) gridItemRef: ElementRef | undefined; + + @ContentChild('keysTemplate', { static: false }) + keysTemplateRef: TemplateRef | undefined; + + @ContentChild('valuesTemplate', { static: false }) + valuesTemplateRef: TemplateRef | undefined; + + @ContentChild('expandableKeysTemplate', { static: false }) + expandableKeysTemplateRef: TemplateRef | undefined; + + @ContentChild('expandableValuesTemplate', { static: false }) + expandableValuesTemplateRef: TemplateRef | undefined; + + trackByFn(index: number): number | undefined { + return index; + } + + /** + * Defines the appearance of the Data Grid in mode === 'List' of the data grid component + * + * A) FILL: + * - The even rows in Data Direction === ROW will be displayed at color grey || theming color with density 10. + * - The even columns in Data Direction === COLUMN will be displayed at color grey || theming color with density 10. + * + * B) OUTLINE: + * - All rows, excepting the last one, in Data Direction === ROW will have a border bottom 1px solid grey || theming color with density 25 - 50, depending on the grid item state. + * - All rows, excepting the last one, in Data Direction === COLUMN will have a border right 1px solid grey || theming color with density 25 - 50, depending on the grid item state. + * + * C) GHOST: + * - The rows and the cols does not have any background color and stroked grid items. + * + */ + protected get appearanceClass(): string { + return this.appearance ? `pi-data-grid__appearance--${this.appearance}` : ''; + } + + /** + * Defines the appearance of the card (grid item) in mode === 'Grid'. + * + * A) HORIZONTAL: + * - A card design with an horizontal orientation in order to allow to see more results in screen print the Aside or in Mobile Devices. + * + * B) SQUARED: + * - A card design with an squared orientation in order show simplified items information. + * + * C) VERTICAL: + * - A card with a vertical orientation to display items with the full information. + * + * D) LIST: + * - This is not properly a card and it's only available in Data Direction === 'Column' cases. + * - The list type emulates a table with for a single item, so, a single keys <> values combination. + * + */ + protected get cardTypeClass(): string { + return this.mode === 'grid' && this.cardType ? `pi-data-grid__card-type--${this.cardType}` : ''; + } + + /** + * Defines the number of columns of the data grid. + * + * - When de data data direction === ROW, 1 column = 1 column. + * - When de data data direction === COLUMN, 1 column emulates a row. + * + */ + protected get columnsClass(): string { + return this.columns && `pi-data-grid__columns--${this.columns}`; + } + + /** + * Defines the data direction of the data grid component + * If any item is displayed as + * + * - This property only applies in Data Direction === COLUMN cases: + * + */ + protected get dataDirectionClass(): string { + return this.mode === 'list' && this.dataDirection + ? `pi-data-grid__data-direction--${this.dataDirection}` + : ''; + } + + /** + * Manage if the row can be Editable + * + * This property only applies in Data Direction === COLUMN cases: + * + */ + protected get isEditableClass(): string { + return this.dataDirection === 'column' && this.isEditable ? 'is-editable' : ''; + } + + /** + * Manage if the row can be Expandable + * + * - This property only applies in Data Direction === ROW cases: + * + */ + protected get isExpandableClass(): string { + return this.dataDirection === 'row' && this.isExpandable ? 'is-expandable' : ''; + } + + /** + * Defines the justify property between columns + * + * - This property only applies in Data Direction === COLUMN cases: + * + */ + protected get justifyClass(): string { + return this.dataDirection === 'column' && this.justify + ? `pi-data-grid__justify-content--${this.justify}` + : ''; + } + + /** + * Defines the if the list is shown in: + * + * A) Mode LIST: + * - The mode list emulates a Table component. + * - It can be avaliable in Data Direction === 'Column' and Data Direction === 'Row'. + * - The number of rows and cols must be defined. + * + * + * B) Mode GRID: + * - To display cards and 1 column lists in Data Direction Column. + * - The mode Grid is available in small and large devices. + * - The number of rows and cols must be defined to be combined + * with the cardType in order to generate grid template area to implement a specific design. + * + */ + protected get modeClass(): string { + return this.mode && `pi-data-grid__mode--${this.mode}`; + } + + /** + * Defines the number of rows of the data grid. + * + * - When de data data direction === ROW, 1 row = 1 row. + * - When de data data direction === COLUMN, 1 row emulates a column. + * + */ + protected get rowsClass(): string { + return this.rows && `pi-data-grid__rows--${this.rows}`; + } +} diff --git a/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts b/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts new file mode 100644 index 0000000..ac6d1da --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/directives/cell.directive.ts @@ -0,0 +1,28 @@ +import { AfterViewInit, Directive, ElementRef, HostBinding, Input } from '@angular/core'; +import { itemType } from '../entities/data-grid.model'; + +@Directive({ + standalone: false, + selector: '[pi-cell]', +}) +export class PiCellDirective implements AfterViewInit { + @HostBinding(`class`) get cellType(): string | undefined { + const textAlignClass = this.textAlign !== 'left' && `pi-cell__text-align--${this.textAlign}`; + const indentClass = this.indent ? 'pi-cell__indented' : ''; + const typeClass = this.type !== 'default' && `pi-cell__type--${this.type}`; + return ['pi-cell', textAlignClass, indentClass, typeClass].join(' '); + } + + @Input() type: itemType = 'default'; + @Input() indent = false; + @Input() index!: number; + @Input() textAlign: 'left' | 'center' | 'right' = 'left'; + + constructor(private elRef: ElementRef) {} + + ngAfterViewInit(): void { + if (this.index !== undefined) { + this.elRef?.nativeElement?.classList.add(`pi-cell__${this.index}`); + } + } +} diff --git a/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts b/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts new file mode 100644 index 0000000..ebd334c --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/directives/grid-item.directive.ts @@ -0,0 +1,37 @@ +import { Directive, ElementRef, HostBinding, HostListener, Input, Renderer2 } from '@angular/core'; +import { columnsNumber } from '../entities/data-grid.model'; + +@Directive({ + standalone: false, + selector: '[pi-grid-item]', +}) +export class PiGridItemDirective { + @HostBinding(`class`) get gridItemType(): string { + const isExpandableClass = this.isExpandable ? 'is-expandable' : ''; + const isSelectedClass = this.isSelected ? 'is-selected' : ''; + const contextClass = this.context && `pi-grid-item__context--${this.context}`; + return ['pi-grid-item', isExpandableClass, isSelectedClass, contextClass].join(' '); + } + + @Input() isExpandable = false; + @Input() isSelected = false; + @Input() context!: 'keys' | 'values'; + + @Input() cells!: columnsNumber | null; + @Input() isActive!: boolean | null; + + @Input() isVisited = false; + + constructor(private elRef: ElementRef, private renderer: Renderer2) {} + + @HostListener('click') selectItem(): void { + const gridItem = this.elRef.nativeElement; + const isSelected = gridItem.classList.contains('is-selected'); + + if (isSelected) { + this.renderer.removeClass(gridItem, 'is-selected'); + } else { + this.renderer.addClass(gridItem, 'is-selected'); + } + } +} diff --git a/libs/ui/src/lib/ui/data-grid/entities/data-grid.model.ts b/libs/ui/src/lib/ui/data-grid/entities/data-grid.model.ts new file mode 100644 index 0000000..ff79026 --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/entities/data-grid.model.ts @@ -0,0 +1,17 @@ +export type itemType = + | 'action' + | 'amount' + | 'checkbox' + | 'date' + | 'default' + | 'sum' + | 'icon' + | 'description'; + +export type columnsNumber = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + +export type rowsNumber = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; + +export type cardStyle = 'primary' | 'secondary' | 'tertiary' | 'horizontal' | 'single-card'; + +export type cardType = 'vertical' | 'squared' | 'horizontal' | 'list'; diff --git a/libs/ui/src/lib/ui/data-grid/index.ts b/libs/ui/src/lib/ui/data-grid/index.ts new file mode 100644 index 0000000..f07174e --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/index.ts @@ -0,0 +1,7 @@ +export * from './shared-ui-data-grid.module'; + +export * from './data-grid/data-grid.component'; + +export * from './directives/cell.directive'; + +export * from './directives/grid-item.directive'; diff --git a/libs/ui/src/lib/ui/data-grid/shared-ui-data-grid.module.ts b/libs/ui/src/lib/ui/data-grid/shared-ui-data-grid.module.ts new file mode 100644 index 0000000..bab9cb6 --- /dev/null +++ b/libs/ui/src/lib/ui/data-grid/shared-ui-data-grid.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { PiDataGridComponent } from './data-grid/data-grid.component'; +import { PiCellDirective } from './directives/cell.directive'; +import { PiGridItemDirective } from './directives/grid-item.directive'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiDataGridComponent, PiCellDirective, PiGridItemDirective], + exports: [PiDataGridComponent, PiCellDirective, PiGridItemDirective], +}) +export class PiSharedUIDataGridModule {} diff --git a/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.html b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.html new file mode 100644 index 0000000..afabe8b --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.scss b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts new file mode 100644 index 0000000..fdaec04 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/base-data-table/base-data-table.component.ts @@ -0,0 +1,60 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { DataTableItem } from '../entities/data-table-item.model'; + +import { ComponentFamilyClass } from '../../../styles'; +import { + DeviceConfigsState, + DeviceTypeSelectors, +} from '../../../util/device-configs'; +import { take } from 'rxjs/operators'; + +@Component({ + standalone: false, + selector: 'pi-base-data-table', + templateUrl: './base-data-table.component.html', + styleUrls: ['./base-data-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiBaseDataTableComponent implements OnInit { + @HostBinding('class') get classes(): string { + return [ + 'pi-base-data-table', + ComponentFamilyClass.Table, + this.dataDirectionClass, + this.typeClass, + ].join(' '); + } + @Input() items: Array; + @Input() dataDirection: 'row' | 'column' = 'row'; + @Input() type: 'header' | 'data' = 'data'; + @Input() hideValues = false; + + constructor(private store: Store) {} + + ngOnInit(): void { + this.dataDirectionClass; + this.typeClass; + this.hideHeader(); + } + + get dataDirectionClass(): string { + return `pi-base-data-table__data-direction--${this.dataDirection}`; + } + + get typeClass(): string | void { + return this.type === 'header' && 'pi-base-data-table__type--header'; + } + + private hideHeader(): void { + this.store + .select(DeviceTypeSelectors.getDevice) + .pipe(take(1)) + .subscribe((deviceType: string) => { + if (deviceType !== 'DESKTOP' && this.hideValues) { + delete this.items[0].values; + } + }); + } +} diff --git a/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.html b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.html new file mode 100644 index 0000000..33d5f68 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + {{ display }} + + + + + + {{ display }} + + + + + + {{ display }} + + + + + + + + + + diff --git a/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.scss b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.scss new file mode 100644 index 0000000..c332912 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.scss @@ -0,0 +1,3 @@ +:host { + display: flex; +} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.ts b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.ts new file mode 100644 index 0000000..029cf67 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/cell-type/cell-type.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +export type PiDropdownSelectorTemplateType = 'text' | 'date' | 'amount' | 'input' | 'action'; +@Component({ + standalone: false, + selector: 'pi-cell-type', + templateUrl: './cell-type.component.html', + styleUrls: ['./cell-type.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiCellTypeComponent { + @HostBinding('class') className = 'pi-cell-type'; + + @HostBinding('class.pi-cell-type__amount') get isAmount(): boolean { + return this.type === 'amount'; + } + @HostBinding('class.pi-cell-type__text') get isText(): boolean { + return this.type === 'text'; + } + @HostBinding('class.pi-cell-type__date') get isDate(): boolean { + return this.type === 'date'; + } + @HostBinding('class.pi-cell-type__input') get isInput(): boolean { + return this.type === 'input'; + } + @HostBinding('class.pi-cell-type__action') get isAction(): boolean { + return this.type === 'action'; + } + + @Input() type: PiDropdownSelectorTemplateType = 'text'; + + /** + * Data to display + */ + @Input() display: string | number; +} diff --git a/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.html b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.html new file mode 100644 index 0000000..f0cb2fd --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.html @@ -0,0 +1,9 @@ + +

{{ heading }}

+
+ + + +
diff --git a/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.scss b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.scss new file mode 100644 index 0000000..0f3019e --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.scss @@ -0,0 +1,64 @@ +:host { + display: flex; + // Text style + // Regular + &.pi-data-table-item__text-style--regular { + } + + // Bold + &.pi-data-table-item__text-style--bold { + font: { + weight: 700; + } + } + + // Italic + &.pi-data-table-item__text-style--italic { + } + + // Indent + &.pi-data-table-item__text-style--indent { + h4 { + padding-left: var(--pi-spacing-xxx-large); + } + } + + // Justify + // Flex-start + &.pi-data-table-item__justify--flex-start { + & > article { + & > .pi-cell-type { + justify-content: flex-start; + } + } + } + // Center + &.pi-data-table-item__justify--center { + & > article { + & > .pi-cell-type { + justify-content: center; + } + } + } + + // Flex-end + &.pi-data-table-item__justify--flex-end { + & > article { + & > .pi-cell-type { + justify-content: flex-end; + } + } + } +} + +//Mobile and tablet +@media screen and (max-width: 62rem) { + :host { + // Indent + &.pi-data-table-item__text-style--indent { + h4 { + padding-left: var(--pi-spacing-small); + } + } + } +} diff --git a/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts new file mode 100644 index 0000000..db8cc1a --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/data-table-item/data-table-item.component.ts @@ -0,0 +1,63 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ContentChild, + ElementRef, + HostBinding, + Input, + TemplateRef, + ViewChild, +} from '@angular/core'; +import { textStyle, justify, borderType } from '../../../styles'; +import { Store } from '@ngrx/store'; + +// Store +import { + DeviceConfigsState, + DeviceTypeSelectors, +} from '../../../util/device-configs'; + +// RxJS +import { take } from 'rxjs/operators'; +@Component({ + standalone: false, + selector: 'pi-data-table-item', + templateUrl: './data-table-item.component.html', + styleUrls: ['./data-table-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiDataTableItemComponent implements AfterViewInit { + @HostBinding('class') get classes(): string { + return ['pi-data-table-item', this.textStyleClass, this.justifyClass].join(' '); + } + + @Input() heading: string; + @Input() values: Array; + @Input() textStyle: textStyle = 'regular'; + @Input() justify: justify = 'flex-start'; + @Input() borderType: borderType; + + @ContentChild('item', { static: false }) item: TemplateRef; + @ViewChild('componentItem') componentItem: ElementRef; + + constructor(private store: Store) {} + + ngAfterViewInit(): void { + this.setCellClassByIndex(); + } + + setCellClassByIndex(): void { + this.values?.forEach((item: string, index: number) => { + this.componentItem.nativeElement.children[index].classList.add(`pi-cell__${index}`); + }); + } + + get textStyleClass(): string { + return `pi-data-table-item__text-style--${this.textStyle}`; + } + + get justifyClass(): string { + return `pi-data-table-item__justify--${this.justify}`; + } +} diff --git a/libs/ui/src/lib/ui/data-table/entities/data-table-item.model.ts b/libs/ui/src/lib/ui/data-table/entities/data-table-item.model.ts new file mode 100644 index 0000000..9b5f165 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/entities/data-table-item.model.ts @@ -0,0 +1,6 @@ +export interface DataTableItem { + heading: string; + key?: string; + position?: number; + values: Array; +} diff --git a/libs/ui/src/lib/ui/data-table/index.ts b/libs/ui/src/lib/ui/data-table/index.ts new file mode 100644 index 0000000..3ebf431 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/index.ts @@ -0,0 +1,7 @@ +export * from './ui-data-table.module'; + +export * from './base-data-table/base-data-table.component'; +export * from './cell-type/cell-type.component'; +export * from './data-table-item/data-table-item.component'; + +export * from './entities/data-table-item.model'; diff --git a/libs/ui/src/lib/ui/data-table/ui-data-table.module.ts b/libs/ui/src/lib/ui/data-table/ui-data-table.module.ts new file mode 100644 index 0000000..57af4b6 --- /dev/null +++ b/libs/ui/src/lib/ui/data-table/ui-data-table.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// Components +import { PiBaseDataTableComponent } from './base-data-table/base-data-table.component'; +import { PiCellTypeComponent } from './cell-type/cell-type.component'; +import { PiDataTableItemComponent } from './data-table-item/data-table-item.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiBaseDataTableComponent, PiDataTableItemComponent, PiCellTypeComponent], + exports: [PiBaseDataTableComponent, PiDataTableItemComponent, PiCellTypeComponent], +}) +export class PiSharedUIDataTableModule {} diff --git a/libs/ui/src/lib/ui/divider/divider/divider.component.html b/libs/ui/src/lib/ui/divider/divider/divider.component.html new file mode 100644 index 0000000..e09088f --- /dev/null +++ b/libs/ui/src/lib/ui/divider/divider/divider.component.html @@ -0,0 +1,10 @@ + + +
+
+ +
+ {{ label }} +
+
diff --git a/libs/ui/src/lib/ui/divider/divider/divider.component.scss b/libs/ui/src/lib/ui/divider/divider/divider.component.scss new file mode 100644 index 0000000..1aa8777 --- /dev/null +++ b/libs/ui/src/lib/ui/divider/divider/divider.component.scss @@ -0,0 +1,250 @@ +// Generic color mixin +@mixin dividerColor($color) { + & > hr { + border-color: var(--pi-color-#{$color}); + } + & > span { + color: var(--pi-color-#{$color}); + } +} + +// All colors mixin +@mixin dividerColors { + // Hr Color + &.pi-color__primary { + @include dividerColor(primary-200); + } + &.pi-color__accent { + @include dividerColor(accent-200); + } + &.pi-color__warn { + @include dividerColor(warn-200); + } + &.pi-color__success { + @include dividerColor(success-200); + } + &.pi-color__white { + @include dividerColor(white-rgba-06); + } + &.pi-color__black { + @include dividerColor(black); + } + &.pi-color__grey { + @include dividerColor(grey-75); + } + &.pi-color__highlighted { + @include dividerColor(highlighted-200); + } + &.pi-color__black-primary { + @include dividerColor(black); + } + &.pi-color__primary-black { + @include dividerColor(primary); + } + &.pi-color__black-accent { + @include dividerColor(black); + } + &.pi-color__accent-black { + @include dividerColor(accent-200); + } + &.pi-color__white { + @include dividerColor(white-900); + } + &.pi-color__dark-black { + @include dividerColor(grey-75); + } + &.pi-color__black-dark { + @include dividerColor(grey-900); + } + &.pi-color__grey-black { + @include dividerColor(grey-75); + } + &.pi-color__black-grey { + @include dividerColor(grey-900); + } + &.pi-color__highlighted-grey { + @include dividerColor(highlighted-200); + } + &.pi-color__grey-highlighted { + @include dividerColor(grey-100); + } +} + +// Horizontal divider +@mixin horizontalDivider { + &:not(.pi-divider__direction--vertical) { + & > hr { + border-radius: unset; + max-width: 100vw; + margin: 0; + } + + // Width + // TODO: check final widths + // x-small + &.pi-divider__width--xx-small { + & > hr { + width: var(--pi-spacing-xx-small); + } + } + + &.pi-divider__width--x-small { + & > hr { + width: var(--pi-spacing-large); + } + } + // small + &.pi-divider__width--small { + & > hr { + width: var(--pi-spacing-xxx-large); + } + } + // medium + &.pi-divider__width--medium { + & > hr { + width: 50%; + } + } + // large + &.pi-divider__width--large { + & > hr { + width: -webkit-fill-available; + } + } + // Height + // small + &.pi-divider__height--small { + & > hr { + height: 0.0625rem; + border-bottom: 0.0625rem solid; + } + } + // medium + &.pi-divider__height--medium { + & > hr { + height: var(--pi-spacing-xxxx-small); + border-bottom: 0.125rem solid; + } + } + + // large + &.pi-divider__height--large { + & > hr { + height: 0.1875rem; + border-bottom: 0.1875rem solid; + } + } + @include dividerColors; + + // Position + &.pi-divider__position--before { + .pi-icon { + transform: translateX(-0.6875rem); + } + } + + &.pi-divider__position--after { + .pi-icon { + transform: translateX(-0.25rem); + } + } + + // Aligment + // flex-start + &.pi-divider__aligment--flex-start { + align-items: flex-start; + } + // Center + &.pi-divider__aligment--center { + align-items: center; + } + // Flex-end + &.pi-divider__aligment--flex-end { + align-items: flex-end; + } + + // Has Icon + &.has-icon { + display: flex; + // Gap + // Xs + &.pi-divider__gap--xs { + gap: var(--pi-spacing-x-small); + } + // S + &.pi-divider__gap--s { + gap: var(--pi-spacing-small); + } + // M + &.pi-divider__gap--m { + gap: var(--pi-spacing-medium); + } + // L + &.pi-divider__gap--large { + gap: var(--pi-spacing-large); + } + // X + &.pi-divider__gap--xl { + gap: var(--pi-spacing-x-large); + } + + // Icon Position + // After + &.pi-divider__position--after { + flex-direction: row-reverse; + } + } + + // Has label + &.has-label { + justify-content: center; + gap: var(--pi-spacing-medium); + width: -webkit-fill-available; + & > hr { + max-width: 50%; + } + } + } +} + +// Vertical divider +@mixin verticalDivider { + &.pi-divider__direction--vertical { + & > hr { + all: unset; + border-left: 1px solid; + } + @include dividerColors; + // Height + // small + &.pi-divider__height--small { + & > hr { + height: var(--pi-spacing-small); + } + } + // medium + &.pi-divider__height--medium { + & > hr { + height: var(--pi-spacing-medium); + } + } + + // large + &.pi-divider__height--large { + & > hr { + height: var(--pi-spacing-large); + } + } + } +} + +:host { + display: flex; + + // Horizontal + @include horizontalDivider; + + // Vertical + @include verticalDivider; + // Color +} diff --git a/libs/ui/src/lib/ui/divider/divider/divider.component.ts b/libs/ui/src/lib/ui/divider/divider/divider.component.ts new file mode 100644 index 0000000..de19913 --- /dev/null +++ b/libs/ui/src/lib/ui/divider/divider/divider.component.ts @@ -0,0 +1,56 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +// Shared +import { alignment, gap, iconSize, textColor } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-divider', + templateUrl: './divider.component.html', + styleUrls: ['./divider.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiDividerComponent { + @HostBinding('class') get classes(): string { + const aligmentClass = `pi-divider__aligment--${this.aligment}`; + const colorClass = this.color && `pi-color__${this.color}`; + const gapClass = this.gap && `pi-divider__gap--${this.gap}`; + const hasIconClass = this.icon ? 'has-icon' : ''; + const heightClass = `pi-divider__height--${this.height}`; + const iconSizeClass = !!this.iconSize && `pi-divider__icon-size--${this.iconSize}`; + const labelClass = this.label ? 'has-label' : ''; + const positionClass = !!this.iconPosition && `pi-divider__position--${this.iconPosition}`; + const widthClass = `pi-divider__width--${this.width}`; + const directionClass = !!this.direction && `pi-divider__direction--${this.direction}`; + return [ + 'pi-divider', + aligmentClass, + colorClass, + gapClass, + hasIconClass, + heightClass, + iconSizeClass, + labelClass, + positionClass, + widthClass, + directionClass, + ].join(' '); + } + + @Input() width: 'xx-small' | 'x-small' | 'small' | 'medium' | 'large' = 'large'; + @Input() height: 'small' | 'medium' | 'large' = 'small'; + // Color: 1st hr 2nd icon + @Input() color!: textColor; + @Input() gap!: gap; + @Input() aligment: alignment = 'center'; + @Input() direction: 'horizontal' | 'vertical' = 'horizontal'; + + // Divider Icon + @Input() icon!: string; + @Input() iconSize: iconSize = 'x-large'; + @Input() iconType: 'fill' | 'default' = 'default'; + @Input() iconPosition: 'before' | 'after' = 'after'; + @Input() isStroked = false; + + @Input() label!: string; +} diff --git a/libs/ui/src/lib/ui/divider/index.ts b/libs/ui/src/lib/ui/divider/index.ts new file mode 100644 index 0000000..b96f336 --- /dev/null +++ b/libs/ui/src/lib/ui/divider/index.ts @@ -0,0 +1,3 @@ +export * from './ui-divider.module'; + +export * from './divider/divider.component'; diff --git a/libs/ui/src/lib/ui/divider/ui-divider.module.ts b/libs/ui/src/lib/ui/divider/ui-divider.module.ts new file mode 100644 index 0000000..d8dfc49 --- /dev/null +++ b/libs/ui/src/lib/ui/divider/ui-divider.module.ts @@ -0,0 +1,16 @@ +// Angular +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// Component +import { PiDividerComponent } from './divider/divider.component'; + +// Shared +import { PiSharedUIIconModule } from '../icon'; + +@NgModule({ + imports: [CommonModule, PiSharedUIIconModule], + declarations: [PiDividerComponent], + exports: [PiDividerComponent], +}) +export class PiSharedUIDividerModule {} diff --git a/libs/ui/src/lib/ui/error-status-code/index.ts b/libs/ui/src/lib/ui/error-status-code/index.ts new file mode 100644 index 0000000..1c71ca2 --- /dev/null +++ b/libs/ui/src/lib/ui/error-status-code/index.ts @@ -0,0 +1,3 @@ +export * from './ui-error-status-code.module'; + +export * from './not-found/not-found.component'; diff --git a/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.html b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.html new file mode 100644 index 0000000..5b862f9 --- /dev/null +++ b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.html @@ -0,0 +1,16 @@ + + + +

{{'404.error404' | translate}}

+
+ +

+ {{'404.pageNotFound' | translate}}

{{'404.checkUrlAndRefresh' | translate}} +

+
+
+ +
diff --git a/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts new file mode 100644 index 0000000..67dc424 --- /dev/null +++ b/libs/ui/src/lib/ui/error-status-code/not-found/not-found.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { Location } from '@angular/common'; +import { LayoutTypeClass } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-not-found', + templateUrl: './not-found.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiNotFoundComponent { + @HostBinding('class') className = LayoutTypeClass.VerticallyCentered; + + constructor(private location: Location) {} + + previous(): void { + this.location.back(); + } +} diff --git a/libs/ui/src/lib/ui/error-status-code/ui-error-status-code.module.ts b/libs/ui/src/lib/ui/error-status-code/ui-error-status-code.module.ts new file mode 100644 index 0000000..0d09f44 --- /dev/null +++ b/libs/ui/src/lib/ui/error-status-code/ui-error-status-code.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiNotFoundComponent } from './not-found/not-found.component'; +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIImageModule } from '../image'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; +import { PiSharedUIWrapperModule } from '../wrapper'; +import { PiSharedUITextModule } from '../text'; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIImageModule, + PiSharedUITextModule, + PiSharedUIWrapperModule, + TranslatePipe, TranslateDirective + ], + declarations: [PiNotFoundComponent], + exports: [PiNotFoundComponent], +}) +export class PiSharedUIErrorStatusCodeModule {} diff --git a/libs/ui/src/lib/ui/form-field/base-form-field.component.ts b/libs/ui/src/lib/ui/form-field/base-form-field.component.ts new file mode 100644 index 0000000..e093ce6 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/base-form-field.component.ts @@ -0,0 +1,81 @@ +import { Component, EventEmitter, HostBinding, Injector, Input, Output } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ComponentFamilyClass } from '../../styles'; +import { PiFormControl } from '../../util/form-control'; + +@Component({ + standalone: false, + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiBaseFormFieldComponent, + multi: true, + }, + ], +}) +export abstract class PiBaseFormFieldComponent extends PiFormControl { + @HostBinding('class') + get classes(): string { + return [ + ComponentFamilyClass.Field, + this.hasErrorClass, + this.focusedClass, + this.isDisabledClass, + ].join(' '); + } + + @Input() formControlName!: string; + + @Input() placeholder: string | undefined; + + @Input() isAutofocus = false; + + @Input() appearance: 'outline' | 'fill' = 'outline'; + + @Input() enableErrorMessage = true; + + @Input() isControlStatusReactive = false; + + @Input() + set isDisabled(value: boolean) { + this.disabled = value; + // Update control status reactively + if (this.isControlStatusReactive) { + if (this.disabled) { + this.control.disable(); + } else { + this.control.enable(); + } + } + } + get isDisabled(): boolean { + return this.disabled; + } + + @Output() isActive: EventEmitter = new EventEmitter(); + + constructor(injector: Injector) { + super(injector); + } + + get control(): FormControl { + return this.controlContainer.control?.get(this.formControlName) as FormControl; + } + + get hasError(): boolean { + return !!this.control.errors?.['message']; + } + + protected get focusedClass(): string | boolean { + return this.focused ? 'is-focused' : false; + } + + protected get hasErrorClass(): string | boolean { + return this.hasError ? 'has-error' : ''; + } + + protected get isDisabledClass(): string { + return this.isDisabled ? 'is-disabled' : ''; + } +} diff --git a/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html new file mode 100644 index 0000000..a27c6b8 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.ts b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.ts new file mode 100644 index 0000000..3179630 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/currency-field/currency-field.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { NgxCurrencyInputMode } from 'ngx-currency'; + +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-currency-field', + templateUrl: './currency-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiCurrencyFieldComponent, + multi: true, + }, + ], +}) +export class PiCurrencyFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__currency`; + + customCurrencyMaskConfig = { + align: 'left', + allowNegative: true, + decimal: ',', + precision: 2, + prefix: '', + thousands: '.', + inputMode: NgxCurrencyInputMode.Natural, + nullable: true, + }; +} diff --git a/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts b/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts new file mode 100644 index 0000000..ee5e8f1 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/directives/autofocus.directive.ts @@ -0,0 +1,24 @@ +import { Directive, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { MatInput } from '@angular/material/input'; + +@Directive({ + standalone: false, + selector: '[pi-autofocus]', +}) +export class PiAutofocusDirective implements OnInit, OnChanges { + @Input() isAutofocusSelected: boolean; + + constructor(private matInput: MatInput) {} + + ngOnInit() { + if (this.isAutofocusSelected) { + setTimeout(() => this.matInput.focus()); + } + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { + if (this.isAutofocusSelected) this.matInput.focus(); + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/directives/checkbox-grid.directive.ts b/libs/ui/src/lib/ui/form-field/directives/checkbox-grid.directive.ts new file mode 100644 index 0000000..222608e --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/directives/checkbox-grid.directive.ts @@ -0,0 +1,17 @@ +import { Directive, HostBinding, Input } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-checkbox-grid]', +}) +export class PiCheckboxGridDirective { + @HostBinding('class') get classes(): string { + const columnClass = + this.columns && `pi-checkbox-grid__column pi-checkbox-grid__column--${this.columns}`; + const fillClass = this.fill && `pi-checkbox-grid__fill pi-checkbox-grid__fill--${this.fill}`; + return ['pi-checkbox-grid', columnClass, fillClass].join(' '); + } + + @Input() columns: '1' | '2' | '3' | '4' = '2'; + @Input() fill: 'fit-content' | 'fill-available' = 'fit-content'; +} diff --git a/libs/ui/src/lib/ui/form-field/directives/label.directive.ts b/libs/ui/src/lib/ui/form-field/directives/label.directive.ts new file mode 100644 index 0000000..12e7a14 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/directives/label.directive.ts @@ -0,0 +1,27 @@ +import { Directive, ElementRef, HostBinding, Input, OnInit, Renderer2 } from '@angular/core'; + +@Directive({ + standalone: false, + selector: 'pi-label', +}) +export class PiLabelDirective implements OnInit { + @HostBinding('class') get classes(): string { + const validationClass = this.isRequired ? 'is-required' : ''; + return ['pi-label', validationClass].join(' '); + } + @HostBinding('class') className = 'pi-label'; + + @Input() isRequired: boolean = false; + + constructor(private renderer: Renderer2, private el: ElementRef) {} + + ngOnInit() { + if (this.isRequired) { + const span = this.renderer.createElement('span'); + const text = this.renderer.createText('*'); + + this.renderer.appendChild(span, text); + this.renderer.appendChild(this.el.nativeElement, span); + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/directives/radio-grid.directive.ts b/libs/ui/src/lib/ui/form-field/directives/radio-grid.directive.ts new file mode 100644 index 0000000..e612c32 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/directives/radio-grid.directive.ts @@ -0,0 +1,20 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { justify } from '../../../styles'; + +// Flex +@Directive({ + standalone: false, + selector: '[pi-radio-grid]', +}) +export class PiRadioGridDirective { + @HostBinding('class') get classes(): string { + const typeClass = this.type && `pi-radio-grid__type--${this.type}`; + const justifyClass = this.justify && `pi-radio-grid__justify--${this.justify}`; + const columnsClass = this.columns && `pi-radio-grid__columns--${this.columns}`; + return ['pi-radio-grid', typeClass, justifyClass, columnsClass].join(' '); + } + + @Input() type: 'grid' | 'flex' = 'flex'; + @Input() justify: justify = 'flex-start'; + @Input() columns: 1 | 2 | 3 | 4 = 1; +} diff --git a/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html new file mode 100644 index 0000000..da4f9b8 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/email-field/email-field.component.ts b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.ts new file mode 100644 index 0000000..075ed14 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/email-field/email-field.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-email-field', + templateUrl: './email-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiEmailFieldComponent, + multi: true, + }, + ], +}) +export class PiEmailFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__email`; +} diff --git a/libs/ui/src/lib/ui/form-field/entities/radio.model.ts b/libs/ui/src/lib/ui/form-field/entities/radio.model.ts new file mode 100644 index 0000000..dab50ae --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/entities/radio.model.ts @@ -0,0 +1,7 @@ +export interface RadioOption { + id: number; + title: string; + value: string; + disabled?: boolean; + subtitle?: string; +} diff --git a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html new file mode 100644 index 0000000..61404b1 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.html @@ -0,0 +1,24 @@ +
+ + + + + +
+ + + {{ text | translate }} + + diff --git a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss new file mode 100644 index 0000000..48c9b3c --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.scss @@ -0,0 +1,16 @@ +:host { + display: flex; + flex-direction: column; + & > section { + display: flex; + align-items: center; + gap: var(--pi-spacing-medium); + width: 100%; + .pi-icon { + margin-top: var(--pi-spacing-small); + } + } + .mat-mdc-form-field-error { + margin-left: 3.8rem; + } +} diff --git a/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.ts b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.ts new file mode 100644 index 0000000..0d03ca0 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/icon-field/icon-field.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { iconSize } from '../../../styles'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-icon-field', + templateUrl: './icon-field.component.html', + styleUrls: ['./icon-field.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiIconFieldComponent, + multi: true, + }, + ], +}) +export class PiIconFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') get componentClasses(): string { + return ['pi-form-field__text-icon'].join(' '); + } + + @Input() icon: string | undefined; + @Input() iconSize: iconSize = 'x-large'; +} diff --git a/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html new file mode 100644 index 0000000..b38d4c6 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.ts b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.ts new file mode 100644 index 0000000..8750b1f --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/incremental-number-field/incremental-number-field.component.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Injectable, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { unit } from '../../../util/models'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-incremental-number-field', + templateUrl: './incremental-number-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiIncrementalNumberFieldComponent, + multi: true, + }, + ], +}) +@Injectable() +export class PiIncrementalNumberFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = 'pi-form-field__incremental'; + + @Input() slot: number = 1; + + @Input() max: number; + + @Input() min: number = 0; + + @Input() unit: unit = ''; + + @Input() showIncrementButtons: boolean = true; + + incrementValue(): void { + if (this.isDisabled) return + let currentValue = Number(this.control.value); + // Handle decimal numbers + if (Number.isInteger(this.slot)) { + this.control.setValue(currentValue + this.slot); + } else { + this.control.setValue((currentValue + this.slot).toFixed(2)); + } + } + + decrementValue(): void { + if (this.isDisabled) return + let currentValue = Number(this.control.value); + // Handle decimal numbers + if (Number.isInteger(this.slot)) { + this.control.setValue(currentValue - this.slot); + } else { + this.control.setValue((currentValue - this.slot).toFixed(2)); + } + } + + isIncrementButtonDisabled(): boolean { + return this.isDisabled || this.control.value + this.slot > this.max; + } + + isDecrementButtonDisabled(): boolean { + if (this.min || this.min === 0) { + return this.isDisabled || this.control.value - this.slot < this.min; + } + return this.isDisabled; + } +} diff --git a/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.html b/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.html new file mode 100644 index 0000000..83211ca --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.html @@ -0,0 +1,38 @@ +
+
+ + {{ 'common.min' | translate }} + + + +
+ + + +
+ + {{ 'common.max' | translate }} + + + +
+ + + {{ text | translate }} + + +
diff --git a/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.ts b/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.ts new file mode 100644 index 0000000..0998d03 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/incremental-range-field/incremental-range-field.component.ts @@ -0,0 +1,29 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { FormGroup, FormGroupDirective } from '@angular/forms'; +import { unit } from '../../../util/models'; + +@Component({ + standalone: false, + selector: 'pi-incremental-range-field', + templateUrl: './incremental-range-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiIncrementalRangeFieldComponent { + @HostBinding('class') className = + 'pi-field pi-form-field__incremental pi-form-field__incremental--range'; + + @Input() formGroupName!: string; + @Input() minValue!: number; + @Input() maxValue!: number; + @Input() unit!: unit; + @Input() step!: number; + @Input() showError = false; + @Input() showIncrementButtons: boolean = true; + @Input() placeholder: string | undefined; + + constructor(private rootFormGroup: FormGroupDirective) {} + + get form() { + return this.rootFormGroup.control.get(this.formGroupName) as FormGroup; + } +} diff --git a/libs/ui/src/lib/ui/form-field/index.ts b/libs/ui/src/lib/ui/form-field/index.ts new file mode 100644 index 0000000..6e999c2 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/index.ts @@ -0,0 +1,26 @@ +export * from './directives/label.directive'; +export * from './directives/radio-grid.directive'; +export * from './directives/checkbox-grid.directive'; +export * from './directives/autofocus.directive'; + +export * from './entities/radio.model'; + +export * from './incremental-number-field/incremental-number-field.component'; +export * from './number-field/number-field.component'; +export * from './text-field/text-field.component'; +export * from './textarea/textarea.component'; +export * from './currency-field/currency-field.component'; +export * from './search-field/search-field.component'; +export * from './email-field/email-field.component'; +export * from './password-field/password-field.component'; +export * from './icon-field/icon-field.component'; + +export * from './ui-form-field.module'; + +export * from './phone-field/phone-field.component'; + +export * from './base-form-field.component'; + +export * from './incremental-range-field/incremental-range-field.component'; + +export * from './slide-toggle/slide-toggle.component'; diff --git a/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html new file mode 100644 index 0000000..98774c2 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/number-field/number-field.component.ts b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.ts new file mode 100644 index 0000000..af1e454 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/number-field/number-field.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Injectable } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-number-field', + templateUrl: './number-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiNumberFieldComponent, + multi: true, + }, + ], +}) + +@Injectable() +export class PiNumberFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__number`; +} diff --git a/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html new file mode 100644 index 0000000..859dd6a --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.html @@ -0,0 +1,30 @@ + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/password-field/password-field.component.ts b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.ts new file mode 100644 index 0000000..c1269f7 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/password-field/password-field.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-password-field', + templateUrl: './password-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiPasswordFieldComponent, + multi: true, + }, + ], +}) +export class PiPasswordFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__password`; + + hide = true; +} diff --git a/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html new file mode 100644 index 0000000..770b518 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.ts b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.ts new file mode 100644 index 0000000..58bd5b0 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/phone-field/phone-field.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-phone-field', + templateUrl: './phone-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiPhoneFieldComponent, + multi: true, + }, + ], +}) +export class PiPhoneFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__phone`; +} diff --git a/libs/ui/src/lib/ui/form-field/search-field/search-field.component.html b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.html new file mode 100644 index 0000000..091cc21 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.html @@ -0,0 +1,24 @@ +
+ + +
+ diff --git a/libs/ui/src/lib/ui/form-field/search-field/search-field.component.scss b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.scss new file mode 100644 index 0000000..a5a708f --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.scss @@ -0,0 +1,98 @@ +:host { + display: flex; + gap: var(--pi-spacing-small); + justify-content: center; + height: var(--pi-height-button-large); + background-color: var(--pi-color-white); + border-radius: var(--pi-border-radius-large); + + fieldset { + display: flex; + input { + all: unset; + caret-color: var(--pi-color-primary-100); + font: { + size: 0.8125rem; + weight: 400; + } + &::placeholder { + color: var(--pi-color-grey-200); + -webkit-text-fill-color: var(--pi-color-grey-200); + } + } + } + + &.is-collapsed { + width: var(--pi-height-button-large); + + cursor: pointer; + fieldset { + input { + width: 0; + opacity: 0; + } + .pi-icon { + height: var(--pi-height-button-large); + } + } + + & > .pi-icon { + display: none; + } + + &:hover { + border: 1px solid var(--pi-color-primary-100); + fieldset { + .pi-icon { + transition: var(--pi-animation); + width: var(--pi-height-button-large); + } + } + } + &:not(:hover) { + fieldset { + .pi-icon { + transition: var(--pi-animation); + width: 100%; + } + } + } + } + &.is-expanded { + justify-content: space-between; + padding-left: var(--pi-spacing-medium); + padding-right: var(--pi-spacing-medium); + fieldset { + gap: var(--pi-spacing-small); + input { + width: 100%; + opacity: 1; + min-width: 13.125rem; + } + } + &.is-focused { + border: 1px solid var(--pi-color-primary-100); + } + &:not(.is-focused) { + border: 1px solid var(--pi-color-white-900); + } + } +} + +html { + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + :host { + fieldset { + input { + font: { + size: 0.8125rem; + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/search-field/search-field.component.ts b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.ts new file mode 100644 index 0000000..45d895f --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/search-field/search-field.component.ts @@ -0,0 +1,78 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + HostBinding, + Input, + Output, + ViewChild, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { OnInit } from '@angular/core'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-search-field', + templateUrl: './search-field.component.html', + styleUrls: ['./search-field.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSearchFieldComponent, + multi: true, + }, + ], +}) +export class PiSearchFieldComponent extends PiBaseFormFieldComponent implements OnInit { + @HostBinding('class') override get classes(): string { + const isExpandedClass = this.isExpanded ? 'is-expanded' : 'is-collapsed'; + return ['pi-field pi-form-field__search', isExpandedClass].join(' '); + } + + @Output() searchClick: EventEmitter = new EventEmitter(); + + @Output() resetClick: EventEmitter = new EventEmitter(); + + @ViewChild('input', { read: ElementRef, static: true }) + inputRef!: ElementRef; + + @Input() isClosable = true; + + private _isExpanded = false; + @Input() + get isExpanded() { + return this._isExpanded; + } + + set isExpanded(value) { + this.isActive.emit(value); + this._isExpanded = value; + } + + ngOnInit(): void { + this.isExpanded = !!this.control.value || !this.isClosable; + this.control.valueChanges.subscribe((value: any) => { + if (value) { + this.isExpanded = true; + } + }); + } + + openField(): void { + this.isExpanded = true; + this.inputRef.nativeElement.focus(); + } + + closeField(): void { + if (this.isClosable) { + this.isExpanded = false; + } + if (this.control.value) { + this.control.reset(); + this.resetClick.emit(); + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.html b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.html new file mode 100644 index 0000000..5927625 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.html @@ -0,0 +1,4 @@ + diff --git a/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.scss b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.scss new file mode 100644 index 0000000..40e5399 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.scss @@ -0,0 +1,66 @@ +:host { + & > label { + display: inline-block; + position: relative; + height: 1.375rem; + width: 2.25rem; + & > input { + display: none; + } + & > span { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + border: 1px solid var(--pi-color-primary); + border-radius: 1.25rem; + transition: 0.22s; + &::before { + content: ''; + position: absolute; + left: var(--pi-spacing-xxxx-small); + bottom: var(--pi-spacing-xxxx-small); + height: var(--pi-spacing-medium); + width: var(--pi-spacing-medium); + border-radius: 50%; + transition: 0.22s; + } + } + } + + // Is checked + &.is-checked { + & > label { + & > span { + background-color: var(--pi-color-primary); + &::before { + background-color: var(--pi-color-white); + transform: translateX(calc(1.125rem - 0.25rem)); + } + } + } + } + // NOT Is checked + &:not(.is-checked) { + & > label { + & > span { + &::before { + background-color: var(--pi-color-primary); + } + } + } + } + + &.is-disabled { + opacity: 0.3; + } + + &:not(.is-disabled) { + & > label { + & > span { + cursor: pointer; + } + } + } +} diff --git a/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.ts b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.ts new file mode 100644 index 0000000..8f7c0c1 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/slide-toggle/slide-toggle.component.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-slide-toggle', + templateUrl: './slide-toggle.component.html', + styleUrls: ['./slide-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSlideToggleComponent, + multi: true, + }, + ], +}) +export class PiSlideToggleComponent extends PiBaseFormFieldComponent implements OnInit { + @HostBinding('class') override get classes(): string { + const isCheckedClass = this.isChecked ? 'is-checked' : ''; + return ['pi-slide-toggle', isCheckedClass, this.isDisabledClass].join(' '); + } + + @Input() label: string | undefined; + + isChecked!: boolean; + + ngOnInit(): void { + this.control.valueChanges.subscribe((value: boolean) => { + this.isChecked = value; + }); + } +} diff --git a/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html new file mode 100644 index 0000000..1d4e8d9 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + {{ text | translate }} + + + diff --git a/libs/ui/src/lib/ui/form-field/text-field/text-field.component.ts b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.ts new file mode 100644 index 0000000..a53b7e7 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/text-field/text-field.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, HostBinding } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-text-field', + templateUrl: './text-field.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiTextFieldComponent, + multi: true, + }, + ], +}) +export class PiTextFieldComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = `pi-form-field__text`; +} diff --git a/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html new file mode 100644 index 0000000..2b42778 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.html @@ -0,0 +1,23 @@ + + + + + + + + {{ text | translate }} + + +{{ control.value?.length || 0 }} / {{ maxLength }} diff --git a/libs/ui/src/lib/ui/form-field/textarea/textarea.component.ts b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.ts new file mode 100644 index 0000000..fc32e78 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/textarea/textarea.component.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiBaseFormFieldComponent } from '../base-form-field.component'; + +@Component({ + standalone: false, + selector: 'pi-textarea', + templateUrl: './textarea.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiTextareaComponent, + multi: true, + }, + ], +}) +export class PiTextareaComponent extends PiBaseFormFieldComponent { + @HostBinding('class') className = 'pi-form-field__textarea'; + + @Input() maxLength: number = 1e6; +} diff --git a/libs/ui/src/lib/ui/form-field/ui-form-field.module.ts b/libs/ui/src/lib/ui/form-field/ui-form-field.module.ts new file mode 100644 index 0000000..8cab362 --- /dev/null +++ b/libs/ui/src/lib/ui/form-field/ui-form-field.module.ts @@ -0,0 +1,113 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +// CDK +import { OverlayModule } from '@angular/cdk/overlay'; + +// Material +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatSelectModule } from '@angular/material/select'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; +import { MatIconModule } from '@angular/material/icon'; + +// Ngx Currency +import { NgxCurrencyDirective } from 'ngx-currency'; +// Shared +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITooltipModule } from '../tooltip'; +import { PiSharedUIWrapperModule } from '../wrapper'; + +// Components +import { PiCurrencyFieldComponent } from './currency-field/currency-field.component'; +import { PiEmailFieldComponent } from './email-field/email-field.component'; +import { PiIconFieldComponent } from './icon-field/icon-field.component'; +import { PiIncrementalNumberFieldComponent } from './incremental-number-field/incremental-number-field.component'; +import { PiNumberFieldComponent } from './number-field/number-field.component'; +import { PiPasswordFieldComponent } from './password-field/password-field.component'; +import { PiPhoneFieldComponent } from './phone-field/phone-field.component'; +import { PiSearchFieldComponent } from './search-field/search-field.component'; +import { PiTextFieldComponent } from './text-field/text-field.component'; +import { PiTextareaComponent } from './textarea/textarea.component'; +import { PiSharedUtilPipesModule } from '../../util/pipes'; + +import { PiCheckboxGridDirective } from './directives/checkbox-grid.directive'; +import { PiLabelDirective } from './directives/label.directive'; +import { PiRadioGridDirective } from './directives/radio-grid.directive'; +import { PiSharedUIDividerModule } from '../divider'; +import { PiSharedUtilAffixModule } from '../../util/affix'; +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiSharedUtilStatusModule } from '../../util/status'; +import { PiAutofocusDirective } from './directives/autofocus.directive'; +import { PiIncrementalRangeFieldComponent } from './incremental-range-field/incremental-range-field.component'; +import { PiSlideToggleComponent } from './slide-toggle/slide-toggle.component'; + +const MATERIAL_IMPORTS = [ + MatFormFieldModule, + MatInputModule, + MatProgressBarModule, + MatProgressSpinnerModule, + MatSelectModule, + MatIconModule, +]; +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIDividerModule, + PiSharedUIIconModule, + PiSharedUITooltipModule, + PiSharedUIWrapperModule, + PiSharedUtilAffixModule, + PiSharedUtilColorModule, + PiSharedUtilPipesModule, + PiSharedUtilStatusModule, + FormsModule, + MATERIAL_IMPORTS, + NgxCurrencyDirective, + OverlayModule, + ReactiveFormsModule, + TranslatePipe, TranslateDirective, + ], + declarations: [ + PiAutofocusDirective, + PiCheckboxGridDirective, + PiCurrencyFieldComponent, + PiEmailFieldComponent, + PiIconFieldComponent, + PiIncrementalNumberFieldComponent, + PiIncrementalRangeFieldComponent, + PiLabelDirective, + PiNumberFieldComponent, + PiPasswordFieldComponent, + PiPhoneFieldComponent, + PiRadioGridDirective, + PiSearchFieldComponent, + PiSlideToggleComponent, + PiTextareaComponent, + PiTextFieldComponent, + ], + exports: [ + PiAutofocusDirective, + PiCheckboxGridDirective, + PiCurrencyFieldComponent, + PiEmailFieldComponent, + PiIconFieldComponent, + PiIncrementalNumberFieldComponent, + PiIncrementalRangeFieldComponent, + PiLabelDirective, + PiNumberFieldComponent, + PiPasswordFieldComponent, + PiPhoneFieldComponent, + PiRadioGridDirective, + PiSearchFieldComponent, + PiSlideToggleComponent, + PiTextareaComponent, + PiTextFieldComponent, + ], +}) +export class PiSharedUIFormFieldModule {} diff --git a/libs/ui/src/lib/ui/icon/icon/icon.component.html b/libs/ui/src/lib/ui/icon/icon/icon.component.html new file mode 100644 index 0000000..2ead17b --- /dev/null +++ b/libs/ui/src/lib/ui/icon/icon/icon.component.html @@ -0,0 +1,5 @@ + + + diff --git a/libs/ui/src/lib/ui/icon/icon/icon.component.scss b/libs/ui/src/lib/ui/icon/icon/icon.component.scss new file mode 100644 index 0000000..c87edb7 --- /dev/null +++ b/libs/ui/src/lib/ui/icon/icon/icon.component.scss @@ -0,0 +1,245 @@ +@mixin fillIconSize($fill-area-size, $icon-size, $padding) { + height: $fill-area-size; + min-width: $fill-area-size; + width: $fill-area-size; + padding: $padding; + img, + .mat-icon { + width: $icon-size; + } +} + +@mixin defaultIconSize($size) { + img, + .mat-icon { + height: $size; + width: $size; + } +} + +@mixin fillIconColor($background-color, $color, $border-color) { + background-color: $background-color; + .mat-icon { + color: $color; + } + &.is-stroked { + border-color: $border-color; + } +} + +@mixin defaultIconColor { + .mat-icon { + color: var(--pi-color-primary); + } +} + +:host { + display: flex; + justify-content: center; + align-items: center; + + .mat-icon { + display: flex; + } + + // Sizes + // X-Small + &:not(.pi-icon__appearance--fill) { + &.pi-icon__x-small { + @include defaultIconSize(var(--pi-spacing-medium)); + } + // Small + &.pi-icon__small { + @include defaultIconSize(1.125rem); + } + // Medium + &.pi-icon__medium { + @include defaultIconSize(var(--pi-spacing-large)); + } + // Large + &.pi-icon__large { + @include defaultIconSize(1.75rem); + } + // X-Large + &.pi-icon__x-large { + @include defaultIconSize(var(--pi-spacing-xx-large)); + } + } + + &.pi-icon__appearance--fill { + display: flex; + align-items: center; + justify-content: center; + + // Sizes type fill + + // Small + // X Small + // Medium + &.pi-icon__x-small { + @include fillIconSize(var(--pi-spacing-medium), var(--pi-spacing-small), 0); + } + &.pi-icon__small { + @include fillIconSize( + var(--pi-spacing-x-large), + var(--pi-spacing-medium), + var(--pi-spacing-xxxx-small) + ); + } + + // Large & X Large + + &.pi-icon__medium { + @include fillIconSize( + var(--pi-spacing-xx-large), + var(--pi-spacing-x-large), + var(--pi-spacing-xxx-small) + ); + } + &.pi-icon__large { + @include fillIconSize( + var(--pi-spacing-xxx-large), + var(--pi-spacing-x-large), + var(--pi-spacing-xx-small) + ); + } + + &.pi-icon__x-large { + @include fillIconSize( + var(--pi-spacing-xxxx-large), + var(--pi-spacing-x-large), + var(--pi-spacing-xxx-small) + ); + } + + &:not(.is-stroked) { + padding: 0.4375rem; + } + // Small + &.pi-icon__border-radius--small { + border-radius: 0.75rem; + } + // Medium + &.pi-icon__border-radius--medium { + border-radius: var(--pi-border-radius-medium); + } + // Large & + // Stroke default + &.pi-icon__border-radius--large, + &:not(.pi-icon__border-radius--small):not(.pi-icon__border-radius--medium) { + border-radius: 1rem; + } + &.is-stroked { + border: 1px solid; + } + &.pi-color__primary { + @include fillIconColor( + var(--pi-color-primary-25), + var(--pi-color-primary), + var(--pi-color-primary-75) + ); + } + &.pi-color__accent { + @include fillIconColor( + var(--pi-color-accent-25), + var(--pi-color-accent), + var(--pi-color-accent-75) + ); + } + &.pi-color__warn { + @include fillIconColor( + var(--pi-color-warn-25), + var(--pi-color-warn-100), + var(--pi-color-warn) + ); + } + &.pi-color__highlighted { + @include fillIconColor( + var(--pi-color-highlighted-25), + var(--pi-color-highlighted), + var(--pi-color-highlighted-75) + ); + } + &.pi-color__white { + @include fillIconColor( + var(--pi-color-white), + var(--pi-color-primary), + var(--pi-color-primary-200) + ); + } + + &.pi-color__grey { + @include fillIconColor( + var(--pi-color-grey-25), + var(--pi-color-grey), + var(--pi-color-grey-100) + ); + } + } + // Animation + // Horizontally + &.pi-icon__animation--horizontally { + animation: alternateOpacity 2s ease infinite alternate, swing 3s linear infinite alternate; + } + // Vertically + &.pi-icon__animation--vertically { + animation: alternateOpacity 2s ease infinite alternate, bounce 3s linear infinite alternate; + } + + &.pi-icon__animation--clickable { + &:not(.is-disabled) { + .mat-icon { + transition: var(--pi-animation); + } + &:hover { + cursor: pointer; + .mat-icon { + transform: scale(1.15); + } + } + } + } + &.is-disabled { + opacity: 0.66; + } +} + +@keyframes alternateOpacity { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +@keyframes bounce { + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(0.5rem); + } + + 80% { + transform: translateY(-0.25rem); + } +} + +@keyframes swing { + 0%, + 100% { + transform: translateX(0); + } + + 50% { + transform: translateX(0.5rem); + } + + 80% { + transform: translateX(-0.25rem); + } +} diff --git a/libs/ui/src/lib/ui/icon/icon/icon.component.ts b/libs/ui/src/lib/ui/icon/icon/icon.component.ts new file mode 100644 index 0000000..7ba5001 --- /dev/null +++ b/libs/ui/src/lib/ui/icon/icon/icon.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +// Shared +import { + animation, + borderRadius, + ComponentFamilyClass, + iconSize, + styleType, +} from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-icon', + templateUrl: './icon.component.html', + styleUrls: ['./icon.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiIconComponent { + @HostBinding('class') get classes(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const sizeClass = this.size && `pi-icon__${this.size}`; + const animationClass = this.animation && `pi-icon__animation--${this.animation}`; + const appearanceClass = + this.appearance !== 'default' ? `pi-icon__appearance--${this.appearance}` : ''; + const typeClass = this.type !== 'default' ? `pi-icon__type--${this.type}` : ''; + const borderRadiusClass = this.borderRadius && `pi-icon__border-radius--${this.borderRadius}`; + const strokeClass = this.isStroked === true && 'is-stroked'; + return [ + 'pi-icon', + ComponentFamilyClass.Text, + colorClass, + sizeClass, + animationClass, + appearanceClass, + borderRadiusClass, + strokeClass, + typeClass, + ].join(' '); + } + @Input() color: styleType; + @Input() icon: string; + @Input() size: iconSize = 'medium'; + @Input() animation: animation; + @Input() appearance: 'fill' | 'default' = 'default'; + @Input() type: 'custom' | 'default' = 'default'; + @Input() borderRadius: borderRadius; + @Input() isStroked = false; +} diff --git a/libs/ui/src/lib/ui/icon/index.ts b/libs/ui/src/lib/ui/icon/index.ts new file mode 100644 index 0000000..c0c450d --- /dev/null +++ b/libs/ui/src/lib/ui/icon/index.ts @@ -0,0 +1,3 @@ +export * from './ui-icon.module'; + +export * from './icon/icon.component'; diff --git a/libs/ui/src/lib/ui/icon/ui-icon.module.ts b/libs/ui/src/lib/ui/icon/ui-icon.module.ts new file mode 100644 index 0000000..974d07d --- /dev/null +++ b/libs/ui/src/lib/ui/icon/ui-icon.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { PiIconComponent } from './icon/icon.component'; +import { MatIconModule } from '@angular/material/icon'; + +@NgModule({ + declarations: [PiIconComponent], + imports: [CommonModule, MatIconModule], + exports: [PiIconComponent], +}) +export class PiSharedUIIconModule {} diff --git a/libs/ui/src/lib/ui/image/background-image.directive.ts b/libs/ui/src/lib/ui/image/background-image.directive.ts new file mode 100644 index 0000000..c769ab1 --- /dev/null +++ b/libs/ui/src/lib/ui/image/background-image.directive.ts @@ -0,0 +1,91 @@ +// Angular +import { + AfterViewInit, + Directive, + ElementRef, + HostBinding, + Input, + Renderer2, + OnDestroy, +} from '@angular/core'; + +// RxJS + +// Store +import { Device, DeviceConfigsFacade } from '../../util/device-configs'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Directive({ + standalone: false, + selector: '[pi-background-image]', +}) +export class PiBackgroundImageDirective implements AfterViewInit, OnDestroy { + @HostBinding('class') get classes(): string { + const animationClass = this.animation && `pi-background-image__animation--${this.animation}`; + return ['pi-image', 'pi-background-image', animationClass].join(' '); + } + + @Input() extension: 'jpg' | 'png' | 'svg' | 'gif' | 'webp' = 'svg'; + @Input() animation: 'infinite'; + @Input() image: string | undefined; + @Input() isResponsive = false; + @Input() origin: 'content-box' | 'border-box' = 'content-box'; + @Input() size: 'auto' | 'contain' | 'cover' = 'cover'; + @Input() position: + | 'left top' + | 'left center' + | 'left bottom' + | 'right top' + | 'right center' + | 'right bottom' + | 'center top' + | 'center center' + | 'center bottom' = 'center center'; + + private destroy$: Subject = new Subject(); + + constructor( + private deviceConfigsFacade: DeviceConfigsFacade, + private element: ElementRef, + private renderer: Renderer2 + ) {} + + ngAfterViewInit(): void { + this.deviceConfigsFacade.device$.pipe(takeUntil(this.destroy$)).subscribe((device: Device) => { + this.setBackgroundImage(device.toLocaleLowerCase()); + }); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + private setBackgroundImage(device: string): void { + const el = this.element.nativeElement; + + this.renderer.setStyle(el, 'background-repeat', 'no-repeat'); + this.renderer.setStyle(el, 'background-position', this.position); + this.renderer.setStyle(el, 'background-size', this.size); + if (this.origin === 'content-box') { + this.renderer.setStyle(el, 'background-origin', 'content-box'); + } else { + this.renderer.setStyle(el, 'background-origin', 'border-box'); + } + + if (this.isResponsive) { + this.renderer.setStyle( + el, + 'background-image', + `url(https://d2m4265u67f2xu.cloudfront.net/assets/images/${this.image}-${device}.${this.extension})` + ); + } else { + this.renderer.setStyle( + el, + 'background-image', + `url(https://d2m4265u67f2xu.cloudfront.net/assets/images/${this.image}.${this.extension})` + ); + } + } +} diff --git a/libs/ui/src/lib/ui/image/headline/headline.component.html b/libs/ui/src/lib/ui/image/headline/headline.component.html new file mode 100644 index 0000000..1de8f9f --- /dev/null +++ b/libs/ui/src/lib/ui/image/headline/headline.component.html @@ -0,0 +1,21 @@ +
+ + + + +
+ diff --git a/libs/ui/src/lib/ui/image/headline/headline.component.scss b/libs/ui/src/lib/ui/image/headline/headline.component.scss new file mode 100644 index 0000000..3e897d4 --- /dev/null +++ b/libs/ui/src/lib/ui/image/headline/headline.component.scss @@ -0,0 +1,318 @@ +:host { + display: flex; + + h2 { + padding-top: unset; + font: { + size: var(--pi-font-size-xx-large); + } + } + // Type + &.pi-headline__type--avatar { + & > figure { + border-radius: var(--pi-border-radius-medium); + & > img { + border-radius: var(--pi-border-radius-medium); + } + } + } + // Flex direction + // Row + &.pi-headline__flex--row { + align-items: center; + } + // Row-reverse + &.pi-headline__flex--row-reverse { + flex-direction: row-reverse; + } + // Column + &.pi-headline__flex--column { + align-items: center; + } + // Column-reverse + &.pi-headline__flex--column-reverse { + flex-direction: column-reverse; + } + + // Gap + // xxx small + &.pi-headline__gap--xxxs { + gap: var(--pi-spacing-xxx-small); + } + // XX Small + &.pi-headline__gap--xxs { + gap: var(--pi-spacing-xx-small); + } + // X Small + &.pi-headline__gap--xs { + gap: var(--pi-spacing-x-small); + } + // Small + &.pi-headline__gap--s { + gap: var(--pi-spacing-small); + } + + // Medium + &.pi-headline__gap--m { + gap: var(--pi-spacing-medium); + } + + // Large + &.pi-headline__gap--l { + gap: var(--pi-spacing-large); + } + + // X-large + &.pi-headline__gap--xl { + gap: var(--pi-spacing-x-large); + } + + // XX-large + &.pi-headline__gap--xxl, + &:not(.pi-headline__gap--s):not(.pi-headline__gap--m):not(.pi-headline__gap--l):not( + .pi-headline__gap--xl + ):not(.pi-headline__gap--xxxl):not(.pi-headline__gap--xxxxl):not(.pi-headline__gap--xxxs):not( + .pi-headline__gap--xxs + ):not(.pi-headline__gap--xs):not(.pi-headline__gap--none) { + gap: var(--pi-spacing-xx-large); + } + + // XXX-large + &.pi-headline__gap--xxxl { + gap: var(--pi-spacing-xxx-large); + } + + // XXXX-large + &.pi-headline__gap--xxxxl { + gap: var(--pi-spacing-xxxx-large); + } + + // Alignment + // Flex Start + &.pi-headline__align--flex-start { + align-items: flex-start; + } + + // Center + &.pi-headline__align--center { + align-items: center; + } + + // Flex End + &.pi-headline__align--flex-end { + align-items: flex-end; + } + + // Width + // Mobile & Phablet + @media screen and (max-width: 62rem) { + // Width + // 2 / 3 + &.pi-headline__width--two-thirds { + & > figure { + max-width: 70%; + } + } + // 3 / 4 + &.pi-headline__width--three-quarters { + & > figure { + max-width: 80%; + } + } + // 1 / 2 + &.pi-headline__width--half { + & > figure { + max-width: 60%; + } + } + // 1 / 3 + &.pi-headline__width--third { + & > figure { + max-width: 50%; + } + } + // 1 / 4 + &.pi-headline__width--quarter { + & > figure { + max-width: 33.33%; + } + } + // 1 / 5 + &.pi-headline__width--fifth { + & > figure { + max-width: 25%; + } + } + // 1 / 6 + &.pi-headline__width--sixth { + & > figure { + max-width: 20%; + } + } + + // Padding + // XXXXL - 3.375rem - 54px + &.pi-headline__padding--xxxxl { + padding: var(--pi-spacing-xxx-large); + } + // xxxl - 2.5rem - 40px + &.pi-headline__padding--xxxl { + padding: var(--pi-spacing-xx-large); + } + // xxl - 2rem - 32px + &.pi-headline__padding--xxl { + padding: var(--pi-spacing-x-large); + } + // xl - 1.5rem - 24px + &.pi-headline__padding--xl { + padding: var(--pi-spacing-large); + } + // l - 1.125rem - 18px + &.pi-headline__padding--l { + padding: var(--pi-spacing-medium); + } + // m - 1rem - 16px + &.pi-headline__padding--m { + padding: var(--pi-spacing-small); + } + } + // Tablet & Desktop + @media screen and (min-width: 62rem) { + // Width + // 2 / 3 + &.pi-headline__width--two-thirds { + & > figure { + max-width: 66.66%; + } + } + // 3 / 4 + &.pi-headline__width--three-quarters { + & > figure { + max-width: 75%; + } + } + // 1 / 2 + &.pi-headline__width--half { + & > figure { + max-width: 50%; + } + } + // 1 / 3 + &.pi-headline__width--third { + & > figure { + max-width: 33.33%; + } + } + // 1 / 4 + &.pi-headline__width--quarter { + & > figure { + max-width: 25%; + } + } + // 1 / 5 + &.pi-headline__width--fifth { + & > figure { + max-width: 20%; + } + } + // 1 / 6 + &.pi-headline__width--sixth { + & > figure { + max-width: 16.66%; + } + } + // Padding + // XXXXL - 3.375rem - 54px + &.pi-headline__padding--xxxxl { + padding: var(--pi-spacing-xxxx-large); + } + // xxxl - 2.5rem - 40px + &.pi-headline__padding--xxxl { + padding: var(--pi-spacing-xxx-large); + } + // xxl - 2rem - 32px + &.pi-headline__padding--xxl { + padding: var(--pi-spacing-xx-large); + } + // xl - 1.5rem - 24px + &.pi-headline__padding--xl { + padding: var(--pi-spacing-x-large); + } + // l - 1.125rem - 18px + &.pi-headline__padding--l { + padding: var(--pi-spacing-large); + } + // m - 1rem - 16px + &.pi-headline__padding--m { + padding: var(--pi-spacing-medium); + } + } +} + +html { + &.is-mobile { + // Media queries + // Mobile max 640px + @media screen and (max-width: 30rem) { + .pi-headline { + &.pi-headline__flex-row { + flex-direction: column; + & > figure { + width: 80%; + } + & > aside { + width: 100%; + } + } + } + } + @media screen and (min-width: 30rem) and (max-width: 40rem) { + .pi-headline { + &.pi-headline__flex-row { + flex-direction: column; + & > figure { + width: 55%; + } + & > aside { + width: 95%; + } + } + } + } + } + + // Desktop & tablet min 640px + &:not(.is-mobile) { + .pi-headline { + &.pi-headline__flex--row { + & > figure { + width: 45%; + } + & > aside { + width: 55%; + } + } + } + } + + // Tablet + &.is-tablet { + .pi-headline { + &.pi-headline__type-logo { + & > figure { + max-width: 12.5rem; + } + } + } + } + // Desktop + &.is-desktop { + .pi-headline { + &.pi-headline__type--logo { + & > figure { + max-width: 6.875rem; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/image/headline/headline.component.ts b/libs/ui/src/lib/ui/image/headline/headline.component.ts new file mode 100644 index 0000000..7302af3 --- /dev/null +++ b/libs/ui/src/lib/ui/image/headline/headline.component.ts @@ -0,0 +1,79 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Directive, + HostBinding, + Input, +} from '@angular/core'; + +// Shared +import { + alignment, + ComponentFamilyClass, + flexDirection, + gap, +} from '../../../styles'; + +@Directive({ + standalone: false, + selector: 'pi-headline-label-content', + host: { class: 'pi-headline-content pi-headline-content__label' }, +}) +export class PiHeadlineLabelContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-headline-description-content', + host: { class: 'pi-headline-content pi-headline-content__description' }, +}) +export class PiHeadlineDescriptionContentDirective {} + +@Component({ + standalone: false, + selector: 'pi-headline', + templateUrl: './headline.component.html', + styleUrls: ['./headline.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiHeadlineComponent { + @HostBinding('class') get classes(): string { + const alignmentClass = this.alignment && `pi-headline__align--${this.alignment}`; + const flexDirectionClass = this.flexDirection && `pi-headline__flex--${this.flexDirection}`; + const gapClass = this.gap && `pi-headline__gap--${this.gap}`; + const paddingClass = this.padding && `pi-headline__padding--${this.padding}`; + const typeClass = this.type !== 'default' && `pi-headline__type--${this.type}`; + const widthClass = this.width && `pi-headline__width--${this.width}`; + + return [ + ComponentFamilyClass.Image, + 'pi-headline', + typeClass, + flexDirectionClass, + gapClass, + alignmentClass, + widthClass, + paddingClass, + ].join(' '); + } + @Input() alt: string | undefined; + @Input() hasContent = true; + @Input() link!: string | undefined; + @Input() pathImg!: string | undefined; + + // Skin + @Input() alignment: alignment = 'center'; + @Input() flexDirection: flexDirection = 'column'; + @Input() gap!: gap | undefined; + @Input() type: 'logo' | 'default' | 'avatar' = 'default'; + @Input() padding: 'xxxxl' | 'xxxl' | 'xxl' | 'xl' | 'l' | 'm'; + @Input() width: + | 'quarter' + | 'third' + | 'half' + | 'fifth' + | 'sixth' + | 'three-quarters' + | 'two-thirds' + | undefined; +} diff --git a/libs/ui/src/lib/ui/image/index.ts b/libs/ui/src/lib/ui/image/index.ts new file mode 100644 index 0000000..aca21a6 --- /dev/null +++ b/libs/ui/src/lib/ui/image/index.ts @@ -0,0 +1,5 @@ +export * from './ui-image.module'; + +export * from './background-image.directive'; + +export * from './headline/headline.component'; diff --git a/libs/ui/src/lib/ui/image/ui-image.module.ts b/libs/ui/src/lib/ui/image/ui-image.module.ts new file mode 100644 index 0000000..7f0baa8 --- /dev/null +++ b/libs/ui/src/lib/ui/image/ui-image.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// Shared +import { PiSharedUtilPipesModule } from '../../util/pipes'; + +// Components +import { + PiHeadlineComponent, + PiHeadlineDescriptionContentDirective, + PiHeadlineLabelContentDirective, +} from './headline/headline.component'; +import { PiBackgroundImageDirective } from './background-image.directive'; + +@NgModule({ + imports: [CommonModule, PiSharedUtilPipesModule], + declarations: [ + PiHeadlineComponent, + PiHeadlineDescriptionContentDirective, + PiHeadlineLabelContentDirective, + PiBackgroundImageDirective, + ], + exports: [ + PiHeadlineComponent, + PiHeadlineDescriptionContentDirective, + PiHeadlineLabelContentDirective, + PiBackgroundImageDirective, + ], +}) +export class PiSharedUIImageModule {} diff --git a/libs/ui/src/lib/ui/list-item/index.ts b/libs/ui/src/lib/ui/list-item/index.ts new file mode 100644 index 0000000..066edee --- /dev/null +++ b/libs/ui/src/lib/ui/list-item/index.ts @@ -0,0 +1,3 @@ +export * from './ui-list-item.module'; + +export * from './list-item/list-item.component'; diff --git a/libs/ui/src/lib/ui/list-item/list-item/list-item.component.html b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.html new file mode 100644 index 0000000..8e91a1e --- /dev/null +++ b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/libs/ui/src/lib/ui/list-item/list-item/list-item.component.scss b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.scss new file mode 100644 index 0000000..2f25487 --- /dev/null +++ b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.scss @@ -0,0 +1,251 @@ +@mixin listItemColor($color) { + &[class*='pi-list-item__border'] { + border-color: var(--pi-color-#{$color}-75); + } + &.pi-list-item__appearance--fill { + // Translucent + &[class*='pi-color__opacity'] { + // Light + &.pi-color__mode--light, + &:not(.pi-color__mode--dark) { + background-image: var(--pi-gradient-#{$color}-light-rgba); + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + } + // Clickable + &.is-clickable { + &:hover { + &:not(.is-disabled) { + &:not(.is-active) { + background-image: var(--pi-gradient-#{$color}-light-rgba-hover); + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + } + } + } + } + &.is-active { + background-image: var(--pi-gradient-#{$color}-light-rgba-hover); + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + } + } + } + } + + // Dark + &.pi-color__mode--dark { + background-image: var(--pi-gradient-#{$color}-dark-rgba); + &.is-stroked { + border-color: var(--pi-color-#{$color}); + } + &.is-clickable { + &:hover { + &:not(.is-disabled) { + &:not(.is-active) { + background-image: var(--pi-gradient-#{$color}-dark-rgba-hover); + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + } + &.is-active { + background-image: var(--pi-gradient-#{$color}-dark-rgba-hover); + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + } + } + // Opaque + &:not([class*='pi-color__opacity']) { + // Light + &.pi-color__mode--light, + &:not(.pi-color__mode--dark) { + background-color: var(--pi-color-#{$color}-25); + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + } + &.is-clickable { + &:hover { + &:not(.is-disabled) { + &:not(.is-active) { + background-color: var(--pi-color-#{$color}-50); + &.is-stroked { + border-color: var(--pi-color-#{$color}-100); + } + } + } + } + &.is-active { + background-color: var(--pi-color-#{$color}-25); + &.is-stroked { + border-color: var(--pi-color-#{$color}-75); + } + } + } + } + + // Dark + &.pi-color__mode--dark { + background-color: var(--pi-color-#{$color}-400); + &.is-stroked { + border-color: var(--pi-color-#{$color}); + } + &.is-clickable { + &:hover { + &:not(.is-disabled) { + &:not(.is-active) { + background-color: var(--pi-color-#{$color}); + &.is-stroked { + border-color: var(--pi-color-#{$color}-900); + } + } + } + } + &.is-active { + background-color: var(--pi-color-#{$color}); + &.is-stroked { + border-color: var(--pi-color-#{$color}); + } + } + } + } + } + } +} + +@mixin listItemColorWhite { + // Light Translucent + &[class*='pi-color__opacity'] { + // Light Glass gradient + background-image: var(--pi-gradient-white-rgba); + &.is-clickable { + &:hover { + // Light Glass gradient hover + background-image: var(--pi-gradient-white-rgba-hover); + } + } + } + // Light Opaque + &:not([class*='pi-color__opacity']) { + background-color: var(--pi-color-white); + } + // Light & Is stroked + &.is-stroked { + border-color: var(--pi-color-white); + } +} + +:host { + display: flex; + width: 100%; + transition: var(--pi-animation); + &:hover { + transition: var(--pi-animation); + } + label { + margin-bottom: unset; + } + &.is-stroked { + border: 1px solid; + } + &.is-clickable { + &:hover { + cursor: pointer; + } + } + &[class*='pi-color__opacity'] { + -webkit-backdrop-filter: blur(0.75rem); + backdrop-filter: blur(0.75rem); + } + + // Appearance + // Fill + &.pi-list-item__appearance--fill { + border-radius: var(--pi-border-radius-large); + &.is-stroked { + border: 1px solid; + border-radius: var(--pi-border-radius-large); + } + &.pi-color__opacity--80 { + opacity: 0.8; + } + &.pi-color__opacity--50 { + opacity: 0.5; + } + } + + // Alignment + &.pi-list-item__align-items--center, + &:not(.pi-list-item__align-items--flex-start):not(.pi-list-item__align-items--flex-end) { + align-items: center; + } + &.pi-list-item__align-items--flex-start { + align-items: flex-start; + } + &.pi-list-item__align-items--flex-end { + align-items: flex-end; + } + + // Padding + &.pi-list-item__padding--xxxl { + padding: var(--pi-spacing-xxx-large); + } + &.pi-list-item__padding--xl { + padding: var(--pi-spacing-x-large); + } + &.pi-list-item__padding--m { + padding: var(--pi-spacing-medium); + } + &.pi-list-item__padding--s { + padding: var(--pi-spacing-small); + } + &.pi-list-item__padding--xs { + padding: var(--pi-spacing-x-small); + } + &.pi-list-item__padding--xxs { + padding: var(--pi-spacing-xx-small); + } + &.pi-list-item__padding--none { + padding: 0; + } + + // Border + &.pi-list-item__border--top { + border-top: 1px solid; + } + &.pi-list-item__border--bottom { + border-bottom: 1px solid; + } + + // Colors + &:not(.pi-color__primary):not(.pi-color__accent):not(.pi-color__warn):not( + .pi-color__highlighted + ):not(.pi-color__white) { + @include listItemColor(grey); + } + // Colors + // Primary + &.pi-color__primary { + @include listItemColor(primary); + } + // Accent + &.pi-color__accent { + @include listItemColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include listItemColor(highlighted); + } + // Warn + &.pi-color__warn { + @include listItemColor(warn); + } + + &.pi-color__white { + @include listItemColorWhite; + } +} diff --git a/libs/ui/src/lib/ui/list-item/list-item/list-item.component.ts b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.ts new file mode 100644 index 0000000..931b810 --- /dev/null +++ b/libs/ui/src/lib/ui/list-item/list-item/list-item.component.ts @@ -0,0 +1,134 @@ +import { ChangeDetectionStrategy, Component, Directive, HostBinding, Input } from '@angular/core'; + +import { + alignment, + borderType, + ComponentFamilyClass, + mode, + opacity, + padding, + styleType, +} from '../../../styles'; + +@Directive({ + standalone: false, + selector: 'pi-list-item-left-icon', + host: { class: 'pi-list-item-icon pi-list-item-icon__left' }, +}) +export class PiListItemLeftIconDirective {} + +@Directive({ + standalone: false, + selector: 'pi-list-item-left-content', + host: { class: 'pi-list-item-content pi-list-item-content__left' }, +}) +export class PiListItemLeftContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-list-item-right-content', + host: { class: 'pi-list-item-content pi-list-item-content__right' }, +}) +export class PiListItemRightContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-list-item-right-icon', + host: { class: 'pi-list-item-icon pi-list-item-icon__right' }, +}) +export class PiListItemRightIconDirective {} + +@Component({ + standalone: false, + selector: 'pi-list-item', + templateUrl: './list-item.component.html', + styleUrls: ['./list-item.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiListItemComponent { + @HostBinding('class') get classes(): string { + return [ + 'pi-list-item', + ComponentFamilyClass.List, + this.borderTypeClass, + this.colorClass, + this.isActiveClass, + this.isClickableClass, + this.isDisabledClass, + this.isResponsiveClass, + this.paddingClass, + this.appearanceClass, + this.maxWidthClass, + this.isStrokedClass, + this.opacityClass, + this.alignmentClass, + this.modeClass, + ].join(' '); + } + + @Input() alignment: alignment = 'center'; + @Input() appearance: 'default' | 'fill' = 'default'; + @Input() borderType!: borderType; + @Input() color!: styleType; + @Input() isActive = false; + @Input() isClickable = false; + @Input() isDisabled = false; + @Input() isResponsive = false; + @Input() isStroked = false; + @Input() maxWidth: 'full' | 'half' | 'third' = 'full'; + @Input() mode: mode = 'light'; + @Input() opacity: opacity; + @Input() padding: padding = 'xl'; + + protected get alignmentClass(): string { + return this.alignment ? `pi-list-item__align-items--${this.alignment}` : ''; + } + + protected get borderTypeClass(): string { + return this.borderType ? `pi-list-item__border--${this.borderType}` : ''; + } + + protected get colorClass(): string { + return this.color ? `pi-color__${this.color}` : ''; + } + + protected get maxWidthClass(): string { + return this.maxWidth !== 'full' && `pi-list-item__max-width--${this.maxWidth}`; + } + + protected get isActiveClass(): string { + return this.isActive ? 'is-active' : ''; + } + + protected get isClickableClass(): string { + return this.isClickable ? 'is-clickable' : ''; + } + + protected get isDisabledClass(): string { + return this.isDisabled ? 'is-disabled' : ''; + } + + protected get isResponsiveClass(): string { + return this.isResponsive ? 'is-responsive' : ''; + } + + protected get isStrokedClass(): string { + return this.isStroked && this.appearance === 'fill' ? 'is-stroked' : ''; + } + + protected get paddingClass(): string { + return this.padding ? `pi-list-item__padding--${this.padding}` : ''; + } + + protected get appearanceClass(): string { + return this.appearance !== 'default' ? `pi-list-item__appearance--${this.appearance}` : ''; + } + + protected get opacityClass(): string { + return this.opacity ? `pi-color__opacity--${this.opacity}` : ''; + } + + protected get modeClass(): string | undefined { + return this.mode ? `pi-color__mode--${this.mode}` : ''; + } +} diff --git a/libs/ui/src/lib/ui/list-item/ui-list-item.module.ts b/libs/ui/src/lib/ui/list-item/ui-list-item.module.ts new file mode 100644 index 0000000..b75b5f2 --- /dev/null +++ b/libs/ui/src/lib/ui/list-item/ui-list-item.module.ts @@ -0,0 +1,28 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + PiListItemComponent, + PiListItemLeftIconDirective, + PiListItemLeftContentDirective, + PiListItemRightContentDirective, + PiListItemRightIconDirective, +} from './list-item/list-item.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [ + PiListItemComponent, + PiListItemLeftIconDirective, + PiListItemLeftContentDirective, + PiListItemRightContentDirective, + PiListItemRightIconDirective, + ], + exports: [ + PiListItemComponent, + PiListItemLeftIconDirective, + PiListItemLeftContentDirective, + PiListItemRightContentDirective, + PiListItemRightIconDirective, + ], +}) +export class PiSharedUIListItemModule {} diff --git a/libs/ui/src/lib/ui/modal/entities/modal-configs.ts b/libs/ui/src/lib/ui/modal/entities/modal-configs.ts new file mode 100644 index 0000000..4afce4d --- /dev/null +++ b/libs/ui/src/lib/ui/modal/entities/modal-configs.ts @@ -0,0 +1,33 @@ +import { MatDialogConfig } from '@angular/material/dialog'; + +// Modal +export enum ModalConfigKeys { + Small = 'small', + Medium = 'medium', + Full = 'full', +} + +export type ModalConfigs = { + [key in ModalConfigKeys]: MatDialogConfig; +}; + +export const modalConfigs: ModalConfigs = { + [ModalConfigKeys.Small]: { + maxHeight: '85vh', + minHeight: '50vh', + backdropClass: 'pi-backdrop', + panelClass: ['pi-modal__panel', 'pi-modal__panel--small'], + }, + [ModalConfigKeys.Medium]: { + maxHeight: '85vh', + minHeight: '66.66vh', + backdropClass: 'pi-backdrop', + panelClass: ['pi-modal__panel', 'pi-modal__panel--medium'], + }, + [ModalConfigKeys.Full]: { + maxHeight: '95vh', + maxWidth: '95vw', + backdropClass: 'pi-backdrop', + panelClass: ['pi-modal__panel', 'pi-modal__panel--full'], + }, +}; diff --git a/libs/ui/src/lib/ui/modal/entities/modal.model.ts b/libs/ui/src/lib/ui/modal/entities/modal.model.ts new file mode 100644 index 0000000..7d25109 --- /dev/null +++ b/libs/ui/src/lib/ui/modal/entities/modal.model.ts @@ -0,0 +1,35 @@ +import { TemplateRef } from '@angular/core'; + +export enum ModalType { + DIALOG = 'dialog', + CUSTOM = 'custom', +} + +export enum ModalStyleType { + PRIMARY = 'primary', + WARN = 'warn', +} + +// TODO: remove question mark once it's going to be refacorized +interface ModalData { + title?: string; + subtitle?: string; +} + +export interface DialogModalData extends ModalData { + type: ModalType.DIALOG; + pathImg: string; + template?: TemplateRef; + description?: string; + label?: string; + buttonText: { + primaryAction?: string; + secondaryAction?: string; + }; + style?: ModalStyleType; +} + +export interface CustomModalData extends ModalData { + type: ModalType.CUSTOM; + template: TemplateRef; +} diff --git a/libs/ui/src/lib/ui/modal/index.ts b/libs/ui/src/lib/ui/modal/index.ts new file mode 100644 index 0000000..c6c86ba --- /dev/null +++ b/libs/ui/src/lib/ui/modal/index.ts @@ -0,0 +1,9 @@ +export * from './modal/modal.component'; + +export * from './ui-modal.module'; + +export * from './entities/modal.model'; + +export * from './entities/modal-configs'; + +export * from './utils'; diff --git a/libs/ui/src/lib/ui/modal/modal/modal.component.html b/libs/ui/src/lib/ui/modal/modal/modal.component.html new file mode 100644 index 0000000..ad09f48 --- /dev/null +++ b/libs/ui/src/lib/ui/modal/modal/modal.component.html @@ -0,0 +1,59 @@ +
+ +

+ {{ title }} + {{ subtitle }} +

+
+ + +
+ + + + + +
+ + + + {{ dialogModalData.label }} + + {{ + dialogModalData.description + }} + +
+ +
+
+ + + + + +
+ + +
+ +
+
diff --git a/libs/ui/src/lib/ui/modal/modal/modal.component.scss b/libs/ui/src/lib/ui/modal/modal/modal.component.scss new file mode 100644 index 0000000..12a4d0e --- /dev/null +++ b/libs/ui/src/lib/ui/modal/modal/modal.component.scss @@ -0,0 +1,147 @@ +:host { + display: flex; + flex-direction: column; + gap: var(--pi-spacing-xx-small); + flex-grow: 1; + min-height: 50vh; + width: 100%; + + .pi-close-button { + display: flex; + justify-content: flex-end; + align-items: flex-start; + height: var(--pi-spacing-xxx-large); + } + + &.pi-modal { + // Dialog type + &.pi-modal__dialog { + justify-content: space-between; + + .pi-close-button { + width: 100%; + } + .pi-modal__main-content { + overflow-y: auto; + & > section { + text-align: center; + } + } + } + // Custom type + &.pi-modal__custom { + overflow: hidden; + & > header { + display: flex; + justify-content: space-between; + padding: var(--pi-spacing-x-large) var(--pi-spacing-x-large) var(--pi-spacing-xx-small); + & > span { + width: calc(100% - var(--pi-spacing-xxx-large)); + text-align: center; + margin-left: var(--pi-spacing-xxx-large); + } + h3 { + display: flex; + justify-content: space-between; + flex-direction: column; + + span { + font: { + size: var(--pi-font-size-small); + weight: 300; + } + } + } + .pi-close-button { + width: var(--pi-spacing-xxx-large); + } + } + .pi-modal__main-content { + &.pi-modal__main-content--custom-template { + height: 100%; + + & > *:first-child { + overflow-y: auto; + padding: 0 var(--pi-spacing-x-large); + } + } + } + } + } +} + +html { + &.is-mobile { + :host { + gap: 0; + // Center content + &.pi-modal { + // Dialog type + &:not(.pi-modal__custom) { + // Action wrapper + & > *:nth-child(n + 1) { + padding: var(--pi-spacing-large); + } + } + // Custom + &.pi-modal__custom { + h3 { + &:not(span) { + font: { + size: var(--pi-font-size-medium); + } + } + } + } + } + } + } + &.is-tablet { + :host { + // Center content + &.pi-modal { + // Dialog type + &:not(.pi-modal__custom) { + // Action wrapper + & > *:nth-child(n + 1) { + padding: var(--pi-spacing-x-large); + } + } + // Custom + &.pi-modal__custom { + h3 { + &:not(span) { + font: { + size: var(--pi-font-size-large); + } + } + } + } + } + } + } + &.is-desktop { + :host { + // Center content + &.pi-modal { + // Dialog type + &:not(.pi-modal__custom) { + // Action wrapper + & > *:nth-child(n + 1) { + padding: var(--pi-spacing-x-large); + } + } + // Custom + &.pi-modal__custom { + h3 { + &:not(span) { + font: { + size: var(--pi-font-size-large); + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/modal/modal/modal.component.ts b/libs/ui/src/lib/ui/modal/modal/modal.component.ts new file mode 100644 index 0000000..2c15288 --- /dev/null +++ b/libs/ui/src/lib/ui/modal/modal/modal.component.ts @@ -0,0 +1,75 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + CustomModalData, + DialogModalData, + ModalStyleType, + ModalType, +} from '../entities/modal.model'; + +@Component({ + standalone: false, + selector: 'pi-modal', + templateUrl: './modal.component.html', + styleUrls: ['./modal.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiModalComponent { + @HostBinding('class') + get classes(): string { + const typeClass = this.type && `pi-modal__${this.type}`; + const disableCloseClass = this.dialogRef.disableClose ? '' : 'pi-modal__is-closable'; + return ['pi-modal', typeClass, disableCloseClass].join(' '); + } + + styleType: ModalStyleType; + buttonColor: ModalStyleType; + + title: string | undefined; + subtitle: string | undefined; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CustomModalData | DialogModalData + ) { + this.title = data.title; + this.subtitle = data.subtitle; + if (data.type === ModalType.DIALOG) { + this.styleType = data.style || ModalStyleType.PRIMARY; + this.buttonColor = + data.style === ModalStyleType.WARN ? ModalStyleType.WARN : ModalStyleType.PRIMARY; + } + } + + get type(): string { + return this.data.type; + } + + get dialogModalData() { + return this.data as DialogModalData; + } + + get customModalData() { + return this.data as CustomModalData; + } + + get buttonColorClass(): string { + return this.buttonColor ? `pi-button pi-color__${this.buttonColor}` : null; + } + + closeModal(): void { + this.dialogRef.close(); + } + + primaryAction(): void { + this.dialogRef.close(true); + } + + secondaryAction(): void { + this.dialogRef.close(false); + } + + instanceOfDialogModal(): boolean { + return this.type === ModalType.DIALOG; + } +} diff --git a/libs/ui/src/lib/ui/modal/ui-modal.module.ts b/libs/ui/src/lib/ui/modal/ui-modal.module.ts new file mode 100644 index 0000000..4053c10 --- /dev/null +++ b/libs/ui/src/lib/ui/modal/ui-modal.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiModalComponent } from './modal/modal.component'; + +// Material +import { MatDialogModule } from '@angular/material/dialog'; + +// Shared +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIImageModule } from '../image'; +import { PiSharedUITextModule } from '../text'; +import { PiSharedUIWrapperModule } from '../wrapper'; +import { PiSharedUtilColorModule } from '../../util/color'; + +const MATERIAL_IMPORTS = [MatDialogModule]; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIImageModule, + PiSharedUITextModule, + PiSharedUIWrapperModule, + PiSharedUtilColorModule, + MATERIAL_IMPORTS, + ], + declarations: [PiModalComponent], + exports: [PiModalComponent], +}) +export class PiSharedUIModalModule {} diff --git a/libs/ui/src/lib/ui/modal/utils.ts b/libs/ui/src/lib/ui/modal/utils.ts new file mode 100644 index 0000000..e1ab407 --- /dev/null +++ b/libs/ui/src/lib/ui/modal/utils.ts @@ -0,0 +1,16 @@ +import { MatDialogConfig } from '@angular/material/dialog'; +import { ModalConfigKeys, modalConfigs } from './entities/modal-configs'; +import { CustomModalData, DialogModalData } from './entities/modal.model'; + +// Modal config +export function getModalConfigs( + key: ModalConfigKeys, + data?: D, + disableClose?: boolean +): MatDialogConfig { + return { + ...(disableClose && { disableClose }), + ...modalConfigs[key], + ...(data && { data }), + }; +} diff --git a/libs/ui/src/lib/ui/note/entities/note.model.ts b/libs/ui/src/lib/ui/note/entities/note.model.ts new file mode 100644 index 0000000..99565e3 --- /dev/null +++ b/libs/ui/src/lib/ui/note/entities/note.model.ts @@ -0,0 +1,6 @@ +import { status } from '../../../util/status'; + +export interface NoteStatus { + statusType: status; + icon: string; +} diff --git a/libs/ui/src/lib/ui/note/index.ts b/libs/ui/src/lib/ui/note/index.ts new file mode 100644 index 0000000..5f6cce3 --- /dev/null +++ b/libs/ui/src/lib/ui/note/index.ts @@ -0,0 +1,3 @@ +export * from './ui-note.module'; + +export * from './note/note.component'; diff --git a/libs/ui/src/lib/ui/note/infrastructure/note-data.service.ts b/libs/ui/src/lib/ui/note/infrastructure/note-data.service.ts new file mode 100644 index 0000000..81aafbf --- /dev/null +++ b/libs/ui/src/lib/ui/note/infrastructure/note-data.service.ts @@ -0,0 +1,29 @@ +// Angular +import { Injectable } from '@angular/core'; +import { NoteStatus } from '../entities/note.model'; + +@Injectable({ + providedIn: 'root', +}) +export class NoteDataService { + getNoteStatusData(): Array { + return [ + { + statusType: 'success', + icon: 'check-rounded', + }, + { + statusType: 'error', + icon: 'close-rounded', + }, + { + statusType: 'pending', + icon: 'exclamation', + }, + { + statusType: 'info', + icon: 'info', + }, + ]; + } +} diff --git a/libs/ui/src/lib/ui/note/note/note.component.html b/libs/ui/src/lib/ui/note/note/note.component.html new file mode 100644 index 0000000..64663f5 --- /dev/null +++ b/libs/ui/src/lib/ui/note/note/note.component.html @@ -0,0 +1,7 @@ +
+ +
{{ title }}
+ {{ text }} + +
+ diff --git a/libs/ui/src/lib/ui/note/note/note.component.scss b/libs/ui/src/lib/ui/note/note/note.component.scss new file mode 100644 index 0000000..302ea42 --- /dev/null +++ b/libs/ui/src/lib/ui/note/note/note.component.scss @@ -0,0 +1,279 @@ +@mixin noteAlignItems($align-items) { + align-items: $align-items; + & > div { + align-items: $align-items; + } +} + +@mixin noteJustifyContent($justify-content) { + &:not(.pi-note__flex-direction--column) { + justify-content: $justify-content; + } +} + +@mixin noteGap($gap) { + gap: var(--pi-spacing-#{$gap}); +} + +@mixin lightFillNoteColor($background-color, $border-color, $color) { + &.pi-note__mode--light { + background-color: var(--pi-color-#{$background-color}); + border-color: var(--pi-color-#{$border-color}); + color: var(--pi-color-#{$color}); + & > *:nth-child(n + 1) { + color: var(--pi-color-#{$color}); + } + } +} + +:host { + display: flex; + width: 100%; + transition: var(--pi-animation); + + & > div { + display: flex; + gap: var(--pi-spacing-xx-small); + & > span { + display: flex; + line-height: 1; + } + } + + small { + display: block; + line-height: 1.2; + } + + .mat-icon { + display: flex; + align-items: center; + min-height: var(--pi-spacing-xx-large); + min-width: var(--pi-spacing-xx-large); + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + } + + // Alignment + // flex start + &.pi-note__align-items--flex-start { + @include noteAlignItems(flex-start); + } + // center + &.pi-note__align-items--center { + @include noteAlignItems(center); + } + // flex end + &.pi-note__align-items--flex-end { + @include noteAlignItems(flex-end); + } + + // Alignment + + // center + &.pi-note__justify-content--center { + @include noteJustifyContent(center); + } + + // space-between + &.pi-note__justify-content--space-between { + @include noteJustifyContent(space-between); + } + + // Gap + // 0.25rem - 4px + &.pi-note__gap--xxxs { + @include noteGap(xxx-small); + } + // 0.5rem - 8px + &.pi-note__gap--xxs { + @include noteGap(xx-small); + } + // 0.625rem - 10px + &.pi-note__gap--xs { + @include noteGap(x-small); + } + // 0.75rem - 12px + &.pi-note__gap--s { + @include noteGap(small); + } + // 1rem - 16px + &.pi-note__gap--m { + @include noteGap(medium); + } + // 1.25rem - 20px + &.pi-note__gap--l { + @include noteGap(large); + } + // 1.5rem - 24px + &.pi-note__gap--xl { + @include noteGap(x-large); + } + // 2rem - 32px + &.pi-note__gap--xxl { + @include noteGap(xx-large); + } + // 2.5rem - 40px + &.pi-note__gap--xxxl { + @include noteGap(xxx-large); + } + // 3.375rem - 54px + &.pi-note__gap--xxxxl { + @include noteGap(xxxx-large); + } + // Flex direction + &.pi-note__flex-direction--column { + flex-direction: column; + & > div { + flex-direction: column; + justify-content: center; + align-items: center; + gap: var(--pi-spacing-medium); + text-align: center; + & > .mat-icon { + height: var(--pi-spacing-xxx-large); + width: var(--pi-spacing-xxx-large); + } + } + } + // Note type + // Status + &.pi-note__status { + // Fill + &.pi-note__appearance--fill { + padding: var(--pi-spacing-x-small) var(--pi-spacing-medium); + border: 1px solid; + border-radius: var(--pi-border-radius-medium); + // Successful + &.pi-note__status--success { + @include lightFillNoteColor(success-50, success-100, success); + } + // Error + &.pi-note__status--error { + @include lightFillNoteColor(warn-25, warn-50, warn); + } + // Pending + &.pi-note__status--pending { + @include lightFillNoteColor(accent-50, accent-100, accent); + &.pi-note__mode--dark { + background-image: linear-gradient(135deg, #f6bc63 0%, #db971d 51.21%, #f6bc63 200%); + background-size: 200% 100%; + border-color: var(--pi-color-accent-200); + color: var(--pi-color-white); + & > *:nth-child(n + 1) { + color: var(--pi-color-white); + } + &:hover { + background-position: right center; + } + } + } + // Info + &.pi-note__status--info { + @include lightFillNoteColor(primary-50, primary-100, primary); + } + } + + // Ghost + &.pi-note__appearance--ghost { + // Successful + &.pi-note__status--success { + &.pi-note__mode--light { + color: var(--pi-color-success); + } + } + // Error + &.pi-note__status--error { + &.pi-note__mode--light { + color: var(--pi-color-warn); + } + } + // Pending + &.pi-note__status--pending { + &.pi-note__mode--light { + color: var(--pi-color-accent); + } + } + // Info + &.pi-note__status--info { + &.pi-note__mode--light { + color: var(--pi-color-primary); + } + } + } + } + // Simple + &.pi-note__simple { + .mat-icon { + display: none; + } + padding: var(--pi-spacing-x-large); + background-color: var(--pi-color-primary-25); + border-radius: var(--pi-border-radius-large); + } +} +html { + // Mobile + &.is-mobile { + :host { + &.pi-note__status { + // Fill + &.pi-note__appearance--fill { + & > div { + .mat-icon { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + } + } + } + // Ghost + &.pi-note__appearance--ghost { + & > div { + .mat-icon { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + } + } + } + + // responsive + &.is-responsive { + flex-direction: column; + gap: var(--pi-spacing-x-small); + } + } + } + } + + // Desktop & Tablet + &:not(.is-mobile) { + :host { + &.pi-note__status { + &:not(.pi-note__flex-direction--column) { + // Fill + &.pi-note__appearance--fill { + & > div { + .mat-icon { + height: var(--pi-spacing-x-large); + min-height: var(--pi-spacing-x-large); + width: var(--pi-spacing-x-large); + min-width: var(--pi-spacing-x-large); + } + } + } + // Ghost + &.pi-note__appearance--ghost { + & > div { + .mat-icon { + height: var(--pi-spacing-large); + min-height: var(--pi-spacing-large); + width: var(--pi-spacing-large); + min-width: var(--pi-spacing-large); + } + } + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/note/note/note.component.ts b/libs/ui/src/lib/ui/note/note/note.component.ts new file mode 100644 index 0000000..34ff0f8 --- /dev/null +++ b/libs/ui/src/lib/ui/note/note/note.component.ts @@ -0,0 +1,97 @@ +import { + ChangeDetectionStrategy, + Component, + Directive, + HostBinding, + Input, + OnInit, +} from '@angular/core'; +import { NoteDataService } from '../infrastructure/note-data.service'; +import { NoteStatus } from '../entities/note.model'; + +import { status } from '../../../util/status'; +import { gap, alignment } from '../../../styles'; + +@Directive({ + standalone: false, + selector: 'pi-note-label-content', + host: { class: 'pi-note-content pi-note-content__label' }, +}) +export class PiNoteLabelContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-note-action-content', + host: { class: 'pi-note-content pi-note-content__action' }, +}) +export class PiNoteActionContentDirective {} + +@Component({ + standalone: false, + selector: 'pi-note', + templateUrl: './note.component.html', + styleUrls: ['./note.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiNoteComponent implements OnInit { + @HostBinding('class') get classes(): string { + const alignmentClass = this.alignment && `pi-note__align-items--${this.alignment}`; + const flexDirectionClass = + this.flexDirection && `pi-note__flex-direction--${this.flexDirection}`; + const justifyContentClass = this.justify && `pi-note__justify-content--${this.justify}`; + const gapClass = this.gap && `pi-note__gap--${this.gap}`; + const isResponsiveClass = this.isResponsive && 'is-responsive'; + const isStickyClass = this.isSticky && 'is-sticky'; + const modeClass = this.mode && `pi-note__mode--${this.mode}`; + const positionClass = this.position && `pi-note__position--${this.position}`; + const statusClass = this.status && `pi-note__status--${this.status}`; + const appearanceClass = this.appearance && `pi-note__appearance--${this.appearance}`; + const typeClass = this.type && `pi-note__${this.type}`; + return [ + 'pi-note', + alignmentClass, + flexDirectionClass, + gapClass, + isResponsiveClass, + isStickyClass, + modeClass, + positionClass, + statusClass, + appearanceClass, + typeClass, + justifyContentClass, + ].join(' '); + } + + // Content + @Input() title!: string | undefined; + @Input() text!: string | undefined; + + // Behavior + @Input() status!: status; + + // Skin + @Input() alignment: alignment = 'center'; + @Input() justify: 'center' | 'space-between' = 'space-between'; + @Input() flexDirection: 'row' | 'column' = 'row'; + @Input() gap!: gap | undefined; + @Input() isResponsive = false; + @Input() isSticky = false; + @Input() mode: 'dark' | 'light' = 'light'; + @Input() position: 'top' | 'bottom' = 'bottom'; + @Input() appearance: 'fill' | 'ghost' = 'fill'; + @Input() type: 'status' | 'simple' = 'status'; + + noteStatus!: NoteStatus; + + constructor(private noteDataService: NoteDataService) {} + + ngOnInit(): void { + this.noteStatus = this.getNoteStatus(); + } + + getNoteStatus(): NoteStatus { + const noteStatusOptions = this.noteDataService.getNoteStatusData(); + return noteStatusOptions.find((noteStatus) => this.status === noteStatus.statusType); + } +} diff --git a/libs/ui/src/lib/ui/note/ui-note.module.ts b/libs/ui/src/lib/ui/note/ui-note.module.ts new file mode 100644 index 0000000..8cd61a5 --- /dev/null +++ b/libs/ui/src/lib/ui/note/ui-note.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { MatIconModule } from '@angular/material/icon'; + +import { PiSharedUITextModule } from '../text'; +import { PiSharedUtilColorModule } from '../../util/color'; + +import { + PiNoteActionContentDirective, + PiNoteComponent, + PiNoteLabelContentDirective, +} from './note/note.component'; + +@NgModule({ + imports: [CommonModule, MatIconModule, PiSharedUITextModule, PiSharedUtilColorModule], + declarations: [PiNoteComponent, PiNoteLabelContentDirective, PiNoteActionContentDirective], + exports: [PiNoteComponent, PiNoteLabelContentDirective, PiNoteActionContentDirective], +}) +export class PiSharedUINoteModule {} diff --git a/libs/ui/src/lib/ui/notification/entities/notification.data.ts b/libs/ui/src/lib/ui/notification/entities/notification.data.ts new file mode 100644 index 0000000..dfb08ea --- /dev/null +++ b/libs/ui/src/lib/ui/notification/entities/notification.data.ts @@ -0,0 +1,27 @@ +import { MatSnackBarConfig } from '@angular/material/snack-bar'; + +// Notification +export enum NotificationConfigKeys { + Success = 'success', + Error = 'error', + Info = 'info', +} + +export type NotificationConfigs = { + [key in NotificationConfigKeys]: MatSnackBarConfig; +}; + +export const notificationConfigs: NotificationConfigs = { + [NotificationConfigKeys.Success]: { + duration: 3000, + panelClass: ['pi-snack-bar', 'pi-snack-bar__success'], + }, + [NotificationConfigKeys.Error]: { + duration: 3000, + panelClass: ['pi-snack-bar', 'pi-snack-bar__error'], + }, + [NotificationConfigKeys.Info]: { + duration: 3000, + panelClass: ['pi-snack-bar', 'pi-snack-bar__info'], + }, +}; diff --git a/libs/ui/src/lib/ui/notification/entities/notification.model.ts b/libs/ui/src/lib/ui/notification/entities/notification.model.ts new file mode 100644 index 0000000..27d1467 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/entities/notification.model.ts @@ -0,0 +1,13 @@ +interface BaseNotificationData { + message: string; + type: 'success' | 'info' | 'error'; + dismissible: boolean; +} + +export interface ButtonNotificationData extends BaseNotificationData { + buttonText: string; +} + +export interface IconNotificationData extends BaseNotificationData { + icon: string; +} diff --git a/libs/ui/src/lib/ui/notification/index.ts b/libs/ui/src/lib/ui/notification/index.ts new file mode 100644 index 0000000..fa4e5ce --- /dev/null +++ b/libs/ui/src/lib/ui/notification/index.ts @@ -0,0 +1,19 @@ +// Entities +export * from './entities/notification.model'; +export * from './entities/notification.data'; + +// Services +export * from './infrastructure/notification.service'; + +// State +export * from './state/notification.actions'; +export * from './state/notification.effects'; + +// Utils +export * from './utils'; + +// Module +export * from './shared-ui-notification.module'; + +// Components +export * from './snack-bar/snack-bar.component'; diff --git a/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts b/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts new file mode 100644 index 0000000..a365eb3 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/infrastructure/notification.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; + +// Material +import { MatSnackBar } from '@angular/material/snack-bar'; + +// Component +import { PiSnackBarComponent } from '../snack-bar/snack-bar.component'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationService { + constructor(private _snackBar: MatSnackBar) {} + + success(message: string, description?: string): void { + this._snackBar.openFromComponent(PiSnackBarComponent, { + data: { + message, + description, + icon: 'check-rounded', + type: 'success', + dismissible: false, + }, + duration: 3000, + panelClass: ['pi-snackbar', 'pi-snackbar__success'], + }); + } + + error(message: string, description?: string): void { + this._snackBar.openFromComponent(PiSnackBarComponent, { + data: { + message, + description, + icon: 'close-rounded', + type: 'error', + dismissible: false, + }, + duration: 3000, + panelClass: ['pi-snackbar', 'pi-snackbar__error'], + }); + } + + info(message: string, description?: string): void { + this._snackBar.openFromComponent(PiSnackBarComponent, { + data: { + message, + description, + type: 'info-rounded', + dismissible: false, + }, + duration: 3000, + panelClass: ['pi-snackbar', 'pi-snackbar__info'], + }); + } +} diff --git a/libs/ui/src/lib/ui/notification/shared-ui-notification.module.ts b/libs/ui/src/lib/ui/notification/shared-ui-notification.module.ts new file mode 100644 index 0000000..766e881 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/shared-ui-notification.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { MatSnackBarModule } from '@angular/material/snack-bar'; + +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITextModule } from '../text'; + +import { PiSnackBarComponent } from './snack-bar/snack-bar.component'; + +@NgModule({ + imports: [ + CommonModule, + MatSnackBarModule, + PiSharedUIButtonModule, + PiSharedUIIconModule, + PiSharedUITextModule, + ], + declarations: [PiSnackBarComponent], + exports: [PiSnackBarComponent], +}) +export class PiSharedUINotificationModule {} diff --git a/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.html b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.html new file mode 100644 index 0000000..19d5860 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.html @@ -0,0 +1,12 @@ +
+ +
+
{{ message }}
+ {{ description }} +
+
+ + + diff --git a/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.scss b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.scss new file mode 100644 index 0000000..87d6e67 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.scss @@ -0,0 +1,58 @@ +@mixin stateColor($color) { + color: $color; + h5 { + color: $color; + } + span { + color: var(--pi-color-grey); + } + .pi-button { + color: $color; + } +} + +:host { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--pi-spacing-x-small); + margin: auto; + border-radius: var(--pi-border-radius-large); + text-align: center; + div { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + } + &.pi-snackbar__type--info { + color: var(--pi-color-primary); + background-color: var(--pi-color-primary-25); + border-color: var(--pi-color-primary-50); + } + &.pi-snackbar__type--error { + color: var(--pi-color-warn); + background-color: var(--pi-color-warn-25); + border-color: var(--pi-color-warn-50); + } + &.pi-snackbar__type--success { + color: var(--pi-color-success); + background-color: var(--pi-color-success-25); + border-color: var(--pi-color-success-50); + } + + & > section { + display: flex; + align-items: center; + gap: var(--pi-spacing-medium); + } + &.pi-snackbar__type--error { + @include stateColor(var(--pi-color-warn)); + } + &.pi-snackbar__type--success { + @include stateColor(var(--pi-color-success)); + } + &.pi-snackbar__type--info { + @include stateColor(var(--pi-color-primary)); + } +} diff --git a/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts new file mode 100644 index 0000000..9f745c7 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/snack-bar/snack-bar.component.ts @@ -0,0 +1,73 @@ +import { + ChangeDetectionStrategy, + Component, + HostBinding, + Inject, + Input, + Optional, +} from '@angular/core'; +import { MatSnackBarRef, MAT_SNACK_BAR_DATA } from '@angular/material/snack-bar'; +import { styleType } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-snack-bar', + templateUrl: './snack-bar.component.html', + styleUrls: ['./snack-bar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiSnackBarComponent { + @HostBinding('class') className = 'pi-snackbar'; + + @HostBinding('class') get classes(): string { + const TypeClass = this.type && `pi-snackbar__type--${this.type}`; + const HasIconClass = this.icon && 'pi-snackbar__has--icon'; + const IsDimsissibleClass = this.icon && 'pi-snackbar__is--dismissible'; + + return ['pi-snackbar', TypeClass, HasIconClass, IsDimsissibleClass].join(' '); + } + + @Input() message: string; + + @Input() text: string; + @Input() icon: string; + @Input() type: 'success' | 'error' | 'info' = 'success'; + @Input() dismissible = false; + @Input() description: string; + + iconColor!: styleType; + buttonColor!: styleType; + + /** The instance of the component making up the content of the snack bar. */ + private readonly snackBarRef: MatSnackBarRef; + + constructor(@Optional() @Inject(MAT_SNACK_BAR_DATA) public data: any) { + if (data) { + this.message = data.message; + this.icon = data.icon; + this.type = data.type; + this.dismissible = data.dismissible; + this.description = data.description; + if (this.icon) { + switch (data.type) { + case 'error': + this.iconColor = 'warn'; + break; + case 'success': + this.iconColor = 'success'; + break; + case 'info': + this.iconColor = 'primary'; + break; + } + } + } + } + + /** Dismisses the snack bar. */ + dismiss(): void { + if (this.snackBarRef) { + this.snackBarRef.dismiss(); + } + } +} diff --git a/libs/ui/src/lib/ui/notification/state/notification.actions.ts b/libs/ui/src/lib/ui/notification/state/notification.actions.ts new file mode 100644 index 0000000..893f35d --- /dev/null +++ b/libs/ui/src/lib/ui/notification/state/notification.actions.ts @@ -0,0 +1,29 @@ +import { createAction, props } from '@ngrx/store'; + +export const displaySuccess = createAction( + '[CORE NOTIFICATION] Display Success', + props<{ + payload: string; + description?: string; + }>() +); + +export const displayError = createAction( + '[CORE NOTIFICATION] Display Error', + props<{ + payload: string; + description?: string; + }>() +); + +export const displayInfo = createAction( + '[CORE NOTIFICATION] Display Info', + props<{ + payload: string; + description?: string; + }>() +); + +export const triggerNotificationAction = createAction( + '[CORE NOTIFICATION] Trigger Notification Action' +); diff --git a/libs/ui/src/lib/ui/notification/state/notification.effects.ts b/libs/ui/src/lib/ui/notification/state/notification.effects.ts new file mode 100644 index 0000000..65e2378 --- /dev/null +++ b/libs/ui/src/lib/ui/notification/state/notification.effects.ts @@ -0,0 +1,59 @@ +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import * as NotificationActions from './notification.actions'; + +import { map, switchMap, tap } from 'rxjs/operators'; + +import { Injectable } from '@angular/core'; + +import { NotificationService } from '../infrastructure/notification.service'; + +@Injectable() +export class NotificationEffects { + constructor(private action$: Actions, private _notification: NotificationService) {} + + displaySuccess$ = createEffect( + () => { + return this.action$.pipe( + ofType(NotificationActions.displaySuccess), + map((action) => { + return action; + }), + tap((action) => { + this._notification.success(action.payload, action.description); + }) + ); + }, + { dispatch: false } + ); + + displayError$ = createEffect( + () => { + return this.action$.pipe( + ofType(NotificationActions.displayError), + map((action) => { + return action; + }), + tap((action) => { + this._notification.error(action.payload, action.description); + }) + ); + }, + { dispatch: false } + ); + + displayInfo$ = createEffect( + () => { + return this.action$.pipe( + ofType(NotificationActions.displayInfo), + map((action) => { + return action; + }), + tap((action) => { + this._notification.info(action.payload, action.description); + }) + ); + }, + { dispatch: false } + ); +} diff --git a/libs/ui/src/lib/ui/notification/utils.ts b/libs/ui/src/lib/ui/notification/utils.ts new file mode 100644 index 0000000..daa1c6d --- /dev/null +++ b/libs/ui/src/lib/ui/notification/utils.ts @@ -0,0 +1,15 @@ +import { MatSnackBarConfig } from '@angular/material/snack-bar'; + +import { NotificationConfigKeys, notificationConfigs } from './entities/notification.data'; +import { IconNotificationData, ButtonNotificationData } from './entities/notification.model'; + +// Notification config +export function getNotificationConfigs( + key: NotificationConfigKeys, + data?: IconNotificationData | ButtonNotificationData +): MatSnackBarConfig { + return { + ...notificationConfigs[key], + ...(data && { data }), + }; +} diff --git a/libs/ui/src/lib/ui/paginator/index.ts b/libs/ui/src/lib/ui/paginator/index.ts new file mode 100644 index 0000000..6a2eee9 --- /dev/null +++ b/libs/ui/src/lib/ui/paginator/index.ts @@ -0,0 +1,3 @@ +export * from './shared-ui-paginator.module'; + +export * from './paginator/paginator.component'; diff --git a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.html b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.html new file mode 100644 index 0000000..ec2d559 --- /dev/null +++ b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.html @@ -0,0 +1,68 @@ + + +Previous + +
+ +
+ + + Next + + + + + {{ page }} + + + +
+ + {{ page }} + +
+ + +
+ + {{ page }} + +
+
diff --git a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.scss b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.scss new file mode 100644 index 0000000..581d1e5 --- /dev/null +++ b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.scss @@ -0,0 +1,36 @@ +:host { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-top: var(--pi-spacing-small); + & > footer { + display: flex; + align-items: center; + gap: var(--pi-spacing-small); + } + span { + display: flex; + justify-content: center; + align-items: center; + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + border-radius: 0.625rem; + transition: var(--pi-animation); + cursor: pointer; + font: { + size: var(--pi-font-size-small); + } + &.is-active { + color: var(--pi-color-primary-900); + background-color: var(--pi-color-primary-50); + transition: var(--pi-animation); + font: { + weight: 500; + } + } + &:not(.is-active) { + color: var(--pi-color-grey-200); + } + } +} diff --git a/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts new file mode 100644 index 0000000..15c7df6 --- /dev/null +++ b/libs/ui/src/lib/ui/paginator/paginator/paginator.component.ts @@ -0,0 +1,118 @@ +import { + ChangeDetectionStrategy, + EventEmitter, + Component, + Output, + Input, + OnInit, + HostBinding, +} from '@angular/core'; + +@Component({ + standalone: false, + selector: 'pi-paginator', + templateUrl: './paginator.component.html', + styleUrls: ['./paginator.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiPaginatorComponent implements OnInit { + @HostBinding('class') className = 'pi-paginator'; + @Input() currentPage = 1; + @Input() pageSize!: number; + @Input() totalPages!: number; + @Input() isDisabled!: boolean; + + @Output() changePage = new EventEmitter(); + + firstPages!: number[]; + + firstPagesRange = 0; + + ngOnInit(): void { + this.initFirstPages(); + } + + get pages() { + return [...new Array(this.totalPages).keys()].map((el) => el + 1); + } + + // get firstPages() { + // return this._firstPages; + // } + + // New arr with last 3 (new empty array with the desired legnth), withg the .keys I got the + // + /** + * Returns the names of the enumerable string properties and methods of an object. + * @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object. + */ + // keys(o: object): string[]; + + get lastPages() { + return [...new Array(this.totalPages).keys()].slice(-3).map((el) => el + 1); + } + + protected initFirstPages(): void { + this.firstPages = [...new Array(this.totalPages).slice(0, 3).keys()].map((el) => el + 1); + } + + protected paginateBack(): void { + if (this.currentPage > 1) { + const updatedCurrentPage = this.currentPage - 1; + if (this.firstPages.indexOf(this.currentPage) === 0) { + this.firstPages = this.decrementPaginatorRangeBy(3); + this.firstPagesRange = this.firstPagesRange - 3; + } + this.currentPage = updatedCurrentPage; + this.changePage.emit(updatedCurrentPage); + } + } + + protected paginateNext(): void { + if (this.currentPage < this.totalPages) { + const updatedCurrentPage = this.currentPage + 1; + if (this.firstPages.indexOf(this.currentPage) >= 2) { + this.firstPagesRange = this.firstPagesRange + 3; + this.firstPages = this.incrementPaginatorRangeBy(3); + } + this.currentPage = updatedCurrentPage; + this.changePage.emit(updatedCurrentPage); + } + } + + protected paginateTo(selectedPage: number): void { + if (this.isDisabled && selectedPage > this.currentPage) { + return; + } else { + this.currentPage = selectedPage; + this.changePage.emit(this.currentPage); + } + } + + protected paginateToFirstPage(): void { + this.paginateTo(1); + this.initFirstPages(); + this.firstPagesRange = 0; + } + + protected movePages(): void { + if (!this.isDisabled) { + this.firstPagesRange = this.firstPagesRange + 3; + this.firstPages = this.incrementPaginatorRangeBy(3); + this.currentPage = this.firstPages[0]; + this.changePage.emit(this.currentPage); + } + } + + private incrementPaginatorRangeBy(range: number): Array { + return [...new Array(this.totalPages).keys()] + .slice(this.firstPagesRange, this.firstPagesRange + range) + .map((el) => el + 1); + } + + private decrementPaginatorRangeBy(range: number): Array { + return [...new Array(this.totalPages).keys()] + .slice(this.firstPagesRange - range, this.firstPagesRange) + .map((el) => el + 1); + } +} diff --git a/libs/ui/src/lib/ui/paginator/shared-ui-paginator.module.ts b/libs/ui/src/lib/ui/paginator/shared-ui-paginator.module.ts new file mode 100644 index 0000000..732ff06 --- /dev/null +++ b/libs/ui/src/lib/ui/paginator/shared-ui-paginator.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiPaginatorComponent } from './paginator/paginator.component'; + +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; + +@NgModule({ + imports: [CommonModule, PiSharedUIButtonModule, PiSharedUIIconModule], + declarations: [PiPaginatorComponent], + exports: [PiPaginatorComponent], +}) +export class PiSharedUIPaginatorModule {} diff --git a/libs/ui/src/lib/ui/panel/base-panel.component.ts b/libs/ui/src/lib/ui/panel/base-panel.component.ts new file mode 100644 index 0000000..0a36a58 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/base-panel.component.ts @@ -0,0 +1,122 @@ +// Angular +import { Directive, Input, OnInit } from '@angular/core'; +import { + alignment, + borderRadius, + styleType, + flexDirection, + justify, + padding, + gap, + opacity, +} from '../../styles'; +import { status } from '../../util/status'; + +@Directive() +export abstract class PiBasePanelComponent implements OnInit { + @Input() alignment: alignment = 'center'; + @Input() borderRadius: borderRadius = 'large'; + @Input() color: styleType = 'white'; + @Input() flexDirection: flexDirection = 'column'; + @Input() isStroked = false; + @Input() hasShadow = false; + @Input() justify: justify = 'center'; + @Input() width: 'quarter' | 'third' | 'half' | 'fifth' | 'sixth' | 'two-thirds' | undefined; + // TODO: remove on the advisor dashboard refactor + @Input() height!: 'small' | 'medium' | 'large' | 'x-large' | undefined; + @Input() outline!: styleType; + @Input() padding: padding = 'l'; + @Input() grid: 'half' | 'quarter' | 'reverse-quarter'; + @Input() gap: gap = 'l'; + @Input() mode: 'dark' | 'light' = 'dark'; + @Input() innerPadding!: padding; + @Input() opacity: opacity; + + @Input() status!: status | ''; + + ngOnInit(): void { + this.alignmentClass; + this.borderRadiusClass; + this.colorClass; + this.flexDirectionClass; + this.justifyClass; + this.widthClass; + this.outlineClass; + this.paddingClass; + this.gapClass; + this.statusClass; + this.isStrokedClass; + this.modeClass; + this.opacityClass; + } + + protected get widthClass(): string | undefined { + return this.width && `pi-panel__width--${this.width}`; + } + + protected get alignmentClass(): string { + return this.alignment && `pi-panel__align--${this.alignment}`; + } + + protected get justifyClass(): string { + return this.justify && `pi-panel__justify--${this.justify}`; + } + + protected get flexDirectionClass(): string { + return this.flexDirection && `pi-panel__flex-direction--${this.flexDirection}`; + } + + protected get borderRadiusClass(): string { + return this.borderRadius && `pi-panel__border-radius--${this.borderRadius}`; + } + + protected get colorClass(): string { + return `pi-color__${this.color}`; + } + + protected get hasShadowClass(): string | boolean { + return this.hasShadow && 'has-shadow'; + } + + protected get heightClass(): string | undefined { + return this.height && `pi-panel__height--${this.height}`; + } + + protected get outlineClass(): string { + return this.outline && `is-stroked pi-panel__border-color--${this.outline}`; + } + + protected get paddingClass(): string { + return this.padding ? `pi-panel__padding--${this.padding}` : ''; + } + + protected get gridClass(): string { + return this.grid && `pi-panel__grid--${this.grid}`; + } + + protected get isStrokedClass(): string { + return this.isStroked ? 'is-stroked' : ''; + } + + protected get gapClass(): string { + return this.gap && `pi-panel__gap--${this.gap}`; + } + + protected get statusClass(): string { + return this.status && `is-${this.status}`; + } + + protected get modeClass(): string { + return this.mode && `pi-color__mode--${this.mode}`; + } + + protected get innerPaddingClass(): string { + return this.innerPadding + ? `pi-panel__inner-padding pi-panel__inner-padding--${this.innerPadding}` + : ''; + } + + protected get opacityClass(): string { + return this.opacity ? `pi-color__opacity--${this.opacity}` : ''; + } +} diff --git a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.html b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.html new file mode 100644 index 0000000..6f0f321 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.html @@ -0,0 +1,63 @@ + +
+ +
+
+

{{ title }}

+

{{ subtitle }}

+
+ +
+ + + + + +
+ +
+
+
+ + + + +
+ + +
+ + + + + +
+ +
+
diff --git a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.scss b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.scss new file mode 100644 index 0000000..b5f1499 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.scss @@ -0,0 +1,145 @@ +:host { + display: block; + padding: var(--pi-spacing-x-large); + background-color: var(--pi-color-white); + border-radius: var(--pi-border-radius-large); + + // Form type + & > form { + & > header { + display: flex; + justify-content: space-between; + & > aside { + display: flex; + align-items: center; + gap: var(--pi-spacing-medium); + height: var(--pi-spacing-x-large); + & > div { + display: flex; + align-items: center; + } + } + } + + & > section { + display: flex; + flex-direction: column; + height: auto; + overflow: hidden; + transition: all 0.33s ease-out; + + &.is-collapsed { + height: auto; + flex: 1; + max-height: 0; + & > *:nth-child(n + 1) { + opacity: 0; + visibility: hidden; + } + } + &.is-expanded { + flex: 0; + height: 100%; + opacity: 1; + visibility: visible; + transition: all 0.33s ease; + } + } + } + + // Custom type + &.pi-panel__expansion--type-custom { + // Header + & > header { + display: flex; + justify-content: space-between; + gap: var(--pi-spacing-medium); + & > aside { + display: flex; + align-items: center; + gap: var(--pi-spacing-small); + .pi-icon { + transition: var(--pi-animation); + &:hover { + cursor: pointer; + } + &.is-collapsed { + transform: rotate(-90deg); + } + &:not(.is-collapsed) { + transform: rotate(0); + } + } + } + } + + // Expandable content + & > section { + display: flex; + flex-direction: column; + height: auto; + overflow: hidden; + transition: all 0.33s ease-out; + + // Collapsed + &.is-collapsed { + height: auto; + flex: 1; + max-height: 0; + margin-top: 0; + & > *:nth-child(n + 1) { + opacity: 0; + visibility: hidden; + } + } + // Expanded + &.is-expanded { + margin-top: var(--pi-spacing-xx-large); + flex: 0; + height: 100%; + opacity: 1; + visibility: visible; + transition: all 0.33s ease; + } + } + } +} + +html { + &.is-mobile { + :host { + & > form { + & > header { + flex-direction: column-reverse; + gap: var(--pi-spacing-medium); + & > aside { + justify-content: space-between; + .pi-divider__direction--vertical { + display: none; + } + } + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + :host { + & > form { + &.is-expanded { + max-height: 100%; + } + } + } + } + // Desktop + &.is-desktop { + :host { + & > form { + &.is-expanded { + max-height: 166.66vh; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts new file mode 100644 index 0000000..7a9fd39 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/expansion-panel/expansion-panel.component.ts @@ -0,0 +1,72 @@ +import { + ChangeDetectionStrategy, + Component, + ContentChild, + EventEmitter, + HostBinding, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + TemplateRef, +} from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBasePanelComponent } from '../base-panel.component'; + +@Component({ + standalone: false, + selector: 'pi-expansion-panel', + templateUrl: './expansion-panel.component.html', + styleUrls: ['./expansion-panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiExpansionPanelComponent extends PiBasePanelComponent implements OnInit, OnChanges { + @HostBinding('class') get classes(): string { + const typeClass = this.type && `pi-panel__expansion--type-${this.type}`; + return [ + 'pi-panel__expansion', + ComponentFamilyClass.Panel, + this.colorClass, + this.paddingClass, + this.statusClass, + typeClass, + ].join(' '); + } + + @Input() type: 'form' | 'custom' = 'form'; + @Input() isCollapsed = false; + @Input() title: string | undefined; + @Input() subtitle: string | undefined; + @Input() formGroup!: FormGroup; + @Output() edit: EventEmitter = new EventEmitter(); + + @ContentChild('topContent', { static: false }) + topContentRef: TemplateRef | undefined; + + @ContentChild('centerContent', { static: false }) + centerContentRef: TemplateRef | undefined; + + @ContentChild('expandableContent', { static: false }) + expandableContentRef: TemplateRef | undefined; + + override ngOnInit(): void { + if (this.status === 'disabled') this.isCollapsed = true; + } + + ngOnChanges({ status }: SimpleChanges): void { + if (status?.currentValue === 'disabled') this.isCollapsed = true; + } + + toggle(): void { + if (this.status !== 'disabled' && this.status !== 'info') this.isCollapsed = !this.isCollapsed; + } + + onEdit(): void { + if (this.status !== 'disabled') { + if (this.isCollapsed) this.toggle(); + this.edit.emit(); + } + } +} diff --git a/libs/ui/src/lib/ui/panel/index.ts b/libs/ui/src/lib/ui/panel/index.ts new file mode 100644 index 0000000..3846088 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/index.ts @@ -0,0 +1,8 @@ +export * from './expansion-panel/expansion-panel.component'; +export * from './panel/panel.component'; + +export * from './panel.directive'; + +export * from './base-panel.component'; + +export * from './ui-panel.module'; diff --git a/libs/ui/src/lib/ui/panel/panel.directive.ts b/libs/ui/src/lib/ui/panel/panel.directive.ts new file mode 100644 index 0000000..78f095b --- /dev/null +++ b/libs/ui/src/lib/ui/panel/panel.directive.ts @@ -0,0 +1,82 @@ +import { Directive, HostBinding, Input, OnInit } from '@angular/core'; +import { padding } from '../../styles'; + +@Directive() +export abstract class PiBasePanelContentDirective implements OnInit { + @HostBinding('class') get commonClasses(): string { + return ['pi-panel-content', this.innerPaddingClass].join(' '); + } + + @Input() innerPadding: padding; + + ngOnInit(): void { + this.innerPaddingClass; + } + + protected get innerPaddingClass(): string { + return this.innerPadding ? `pi-panel-content__inner-padding--${this.innerPadding}` : ''; + } +} + +// Horizontal direction +@Directive({ + standalone: false, + selector: 'pi-panel-right-content', +}) +export class PiPanelRightContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__right'; + } +} + +@Directive({ + standalone: false, + selector: 'pi-panel-left-content', +}) +export class PiPanelLeftContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__left'; + } +} + +// Vertical direction +@Directive({ + standalone: false, + selector: 'pi-panel-top-content', +}) +export class PiPanelTopContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__top'; + } +} + +@Directive({ + standalone: false, + selector: 'pi-panel-center-content', +}) +export class PiPanelCenterContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__center'; + } +} + +@Directive({ + standalone: false, + selector: 'pi-panel-expandable-content', +}) +export class PiPanelExpandableContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__expandable'; + } +} + +// Vertical direction +@Directive({ + standalone: false, + selector: 'pi-panel-bottom-content', +}) +export class PiPanelBottomContentDirective extends PiBasePanelContentDirective{ + @HostBinding('class') get classes(): string { + return 'pi-panel-content__bottom'; + } +} diff --git a/libs/ui/src/lib/ui/panel/panel/panel.component.html b/libs/ui/src/lib/ui/panel/panel/panel.component.html new file mode 100644 index 0000000..938ecc5 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/panel/panel.component.html @@ -0,0 +1,19 @@ +
+ +
+
+

{{ title }}

+

{{ subtitle }}

+
+ +
+ + + + + + + + + +
diff --git a/libs/ui/src/lib/ui/panel/panel/panel.component.scss b/libs/ui/src/lib/ui/panel/panel/panel.component.scss new file mode 100644 index 0000000..f16dca0 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/panel/panel.component.scss @@ -0,0 +1,336 @@ +@mixin panelPadding($padding) { + padding: var(--pi-spacing-#{$padding}); +} + +@mixin panelGap($gap) { + & > section { + gap: var(--pi-spacing-#{$gap}); + } +} + +@mixin panelColor($color) { + &.pi-color__mode--dark { + background-color: var(--pi-color-#{$color}-50); + } + &.pi-color__mode--light { + background-color: var(--pi-color-#{$color}-25); + } + &.is-stroked { + &.pi-color__mode--dark { + border-color: var(--pi-color-#{$color}-200); + } + &.pi-color__mode--light { + border-color: var(--pi-color-#{$color}-50); + } + } +} + +@mixin panelColorWhite { + // Light Translucent + &[class*='pi-color__opacity'] { + // Light Glass color + background-color: var(--pi-color-white-rgba-03); + &.is-clickable { + &:hover { + // Light Glass color hover + background-color: var(--pi-color-white-rgba-06); + } + } + } + // Light Opaque + &:not([class*='pi-color__opacity']) { + background-color: var(--pi-color-white); + } + // Light & Is stroked + &.is-stroked { + border-color: var(--pi-color-white-900); + } +} + +@mixin alignItems($align-items) { + & > section { + align-items: $align-items; + } +} + +@mixin flexDirection($flex-direction) { + & > section { + flex-direction: $flex-direction; + } +} + +@mixin justifyContent($justify-content) { + & > section { + justify-content: $justify-content; + } +} + +:host { + display: block; + + & > section { + display: flex; + & > header { + display: flex; + justify-content: space-between; + gap: var(--pi-spacing-medium); + } + } + + // Alignment + &.pi-panel__align--center { + @include alignItems(center); + } + &.pi-panel__align--flex-start { + @include alignItems(flex-start); + } + &.pi-panel__align--flex-end { + @include alignItems(flex-end); + } + // Flex direction + &.pi-panel__flex-direction--column { + @include flexDirection(column); + } + &.pi-panel__flex-direction--column-reverse { + @include flexDirection(column-reverse); + } + // Justify + &.pi-panel__justify--flex-start { + @include justifyContent(flex-start); + } + &.pi-panel__justify--flex-end { + @include justifyContent(flex-end); + } + &.pi-panel__justify--center { + @include justifyContent(center); + } + &.pi-panel__justify--space-between { + @include justifyContent(space-between); + } + &.pi-panel__justify--space-evenly { + @include justifyContent(space-evenly); + } + &.pi-panel__justify--space-around { + @include justifyContent(space-around); + } + + // Radius + &.pi-panel__border-radius--small { + border-radius: var(--pi-border-radius-small); + } + &.pi-panel__border-radius--medium { + border-radius: var(--pi-border-radius-medium); + } + &.pi-panel__border-radius--large { + border-radius: var(--pi-border-radius-large); + } + // Colors + + &.pi-color__primary { + @include panelColor(primary); + } + &.pi-color__accent { + @include panelColor(accent); + } + &.pi-color__highlighted { + @include panelColor(highlighted); + } + &.pi-color__success { + @include panelColor(success); + } + &.pi-color__grey { + @include panelColor(grey); + } + &.pi-color__white { + background-color: var(--pi-color-white); + @include panelColorWhite; + } + &.is-stroked { + border: 1px solid; + &.pi-color__white { + border-color: var(--pi-color-grey-25); + } + } + + // Has shadow + &.has-shadow { + box-shadow: var(--pi-box-shadow-panel); + } + + // Padding + &.pi-panel__padding--xxxs { + @include panelPadding(xxx-small); + } + &.pi-panel__padding--xxs { + @include panelPadding(xx-small); + } + &.pi-panel__padding--xs { + @include panelPadding(x-small); + } + + &.pi-panel__padding--s { + @include panelPadding(small); + } + + &.pi-panel__padding--m { + @include panelPadding(medium); + } + + &.pi-panel__padding--l { + @include panelPadding(large); + } + + &.pi-panel__padding--xl { + @include panelPadding(x-large); + } + + &.pi-panel__padding--xxl { + @include panelPadding(xx-large); + } + + // Gap + &.pi-panel__gap--xxxs { + @include panelGap(xxx-small); + } + + &.pi-panel__gap--xxs { + @include panelGap(xx-small); + } + + &.pi-panel__gap--xs { + @include panelGap(x-small); + } + + &.pi-panel__gap--s { + @include panelGap(small); + } + + &.pi-panel__gap--m { + @include panelGap(medium); + } + + &.pi-panel__gap--l { + @include panelGap(large); + } + + &.pi-panel__gap--xl { + @include panelGap(x-large); + } + + &.pi-panel__gap--xxl { + @include panelGap(xx-large); + } + + &.pi-panel__gap--xxxl { + @include panelGap(xxx-large); + } + + &.pi-panel__gap--xxxxl { + @include panelGap(xxxx-large); + } + + &.pi-panel__gap--none { + & > section { + gap: 0; + } + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + // Width + // 2 / 3 + &.pi-panel__width--two-thirds { + max-width: 75%; + } + // 1 / 2 + &.pi-panel__width--half { + max-width: 66.66%; + } + // 1 / 3 + &.pi-panel__width--third { + max-width: 50%; + } + // 1 / 4 + &.pi-panel__width--quarter { + max-width: 33.33%; + } + // 1 / 5 + &.pi-panel__width--fifth { + max-width: 25%; + } + // 1 / 6 + &.pi-panel__width--sixth { + max-width: 20%; + } + } + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // 2 / 3 + &.pi-panel__width--two-thirds { + width: 66.66%; + max-width: 40rem; + } + // 1 / 2 + &.pi-panel__width--half { + width: 50%; + max-width: 35rem; + } + // 1 / 3 + &.pi-panel__width--third { + width: 33.33%; + max-width: 30rem; + } + // 1 / 4 + &.pi-panel__width--quarter { + width: 25%; + max-width: 17.5rem; + } + // 1 / 5 + &.pi-panel__width--fifth { + width: 20%; + max-width: 15rem; + } + // 1 / 6 + &.pi-panel__width--sixth { + width: 16.66%; + max-width: 12rem; + } + } + } + + // Desktop + &.is-desktop { + // Small + &.pi-panel__height--small { + & > section { + min-height: 20vh; + } + } + // Medium + &.pi-panel__height--medium { + & > section { + min-height: 55vh; + } + } + // Large + &.pi-panel__height--large { + & > section { + min-height: 66.66vh; + } + } + // X Large + &.pi-panel__height--x-large { + & > section { + min-height: 75vh; + } + } + } +} diff --git a/libs/ui/src/lib/ui/panel/panel/panel.component.ts b/libs/ui/src/lib/ui/panel/panel/panel.component.ts new file mode 100644 index 0000000..0c06a40 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/panel/panel.component.ts @@ -0,0 +1,40 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; +import { PiBasePanelComponent } from '../base-panel.component'; + +@Component({ + standalone: false, + selector: 'pi-panel', + templateUrl: './panel.component.html', + styleUrls: ['./panel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiPanelComponent extends PiBasePanelComponent { + @HostBinding('class') get classes(): string { + return [ + 'pi-panel', + ComponentFamilyClass.Panel, + this.alignmentClass, + this.borderRadiusClass, + this.colorClass, + this.flexDirectionClass, + this.gapClass, + this.gridClass, + this.hasShadowClass, + this.heightClass, + this.innerPaddingClass, + this.justifyClass, + this.modeClass, + this.outlineClass, + this.paddingClass, + this.paddingClass, + this.statusClass, + this.widthClass, + this.isStrokedClass, + this.opacityClass, + ].join(' '); + } + + @Input() title: string | undefined; + @Input() subtitle: string | undefined; +} diff --git a/libs/ui/src/lib/ui/panel/ui-panel.module.ts b/libs/ui/src/lib/ui/panel/ui-panel.module.ts new file mode 100644 index 0000000..229e8a3 --- /dev/null +++ b/libs/ui/src/lib/ui/panel/ui-panel.module.ts @@ -0,0 +1,64 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +// Shared +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITextModule } from '../text'; + +// Components +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiExpansionPanelComponent } from './expansion-panel/expansion-panel.component'; +import { PiPanelComponent } from './panel/panel.component'; + +// Directives +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + PiPanelBottomContentDirective, + PiPanelCenterContentDirective, + PiPanelExpandableContentDirective, + PiPanelLeftContentDirective, + PiPanelRightContentDirective, + PiPanelTopContentDirective, +} from './panel.directive'; + +import { PiSharedUITooltipModule } from '../tooltip'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; +import { PiSharedUIDividerModule } from '../divider'; + +@NgModule({ + imports: [ + CommonModule, + TranslatePipe, TranslateDirective, + PiSharedUITextModule, + PiSharedUIButtonModule, + PiSharedUIIconModule, + PiSharedUITextModule, + PiSharedUtilColorModule, + PiSharedUITooltipModule, + PiSharedUIDividerModule, + ReactiveFormsModule, + FormsModule, + ], + declarations: [ + PiExpansionPanelComponent, + PiPanelComponent, + PiPanelLeftContentDirective, + PiPanelRightContentDirective, + PiPanelTopContentDirective, + PiPanelCenterContentDirective, + PiPanelExpandableContentDirective, + PiPanelBottomContentDirective, + ], + exports: [ + PiExpansionPanelComponent, + PiPanelComponent, + PiPanelLeftContentDirective, + PiPanelRightContentDirective, + PiPanelTopContentDirective, + PiPanelCenterContentDirective, + PiPanelExpandableContentDirective, + PiPanelBottomContentDirective, + ], +}) +export class PiSharedUIPanelModule {} diff --git a/libs/ui/src/lib/ui/progress/index.ts b/libs/ui/src/lib/ui/progress/index.ts new file mode 100644 index 0000000..34976d1 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/index.ts @@ -0,0 +1,5 @@ +export * from './ui-progress.module'; + +export * from './progress-bar/progress-bar.component'; + +export * from './progress-spinner/progress-spinner.component'; diff --git a/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.html b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.html new file mode 100644 index 0000000..c24c794 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.html @@ -0,0 +1,2 @@ +
{{ label }}
+ diff --git a/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.scss b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.scss new file mode 100644 index 0000000..7853f2d --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.scss @@ -0,0 +1,31 @@ +:host { + display: block; + width: -webkit-fill-available; + & > *:nth-child(n + 2) { + margin-top: var(--pi-spacing-xx-small); + } +} + +html { + // Mobile + &.is-mobile { + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + } + } + // Tablet + &.is-tablet { + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + } + } + // Desktop + &.is-desktop { + } +} diff --git a/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.ts b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.ts new file mode 100644 index 0000000..20df680 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-bar/progress-bar.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +@Component({ + standalone: false, + selector: 'pi-progress-bar', + templateUrl: './progress-bar.component.html', + styleUrls: ['./progress-bar.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiProgressBarComponent { + @HostBinding('class') className = 'pi-progress-bar'; + + @Input() label!: string; + @Input() value!: number; +} diff --git a/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.html b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.html new file mode 100644 index 0000000..f368d85 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.html @@ -0,0 +1,29 @@ + + + + + + + {{ value }}% + + + + + + {{ value }}% + + {{ label }} + diff --git a/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.scss b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.scss new file mode 100644 index 0000000..3091a07 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.scss @@ -0,0 +1,140 @@ +@mixin progressSpinnerSize( + $large-font-size: var(--pi-font-size-large), + $small-font-size: var(--pi-font-size-medium) +) { + :host { + &.pi-spinner__progress { + & > span { + & > span { + &:nth-child(1) { + font: { + size: $large-font-size; + } + } + &:nth-child(2) { + font: { + size: $small-font-size; + } + } + } + } + } + } +} + +@mixin strokeWidth($box-shadow) { + // Compact + &.pi-spinner__appearance--compact { + .mat-mdc-progress-spinner { + box-shadow: $box-shadow; + } + } +} + +@mixin fontSizesClass( + $value-font-size-small: 1.75rem, + $percentage-font-size-small: var(--pi-font-size-x-small), + $value-font-size-large: var(--pi-font-size-xxx-large), + $percentage-font-size-large: var(--pi-font-size-small) +) { + :host { + // Diameter class + // Small 60 - 70 + &.pi-spinner__diameter--small { + // Value + & > span { + font: { + size: $value-font-size-small; + } + // Percentage + & > span { + font: { + size: $percentage-font-size-small; + } + } + } + } + // Large 110 - 130 + &.pi-spinner__diameter--large { + // Value + & > span { + font: { + size: $value-font-size-large; + } + // Percentage + & > span { + font: { + size: $percentage-font-size-large; + } + } + } + } + } +} + +:host { + display: block; + + & > span { + position: relative; + display: flex; + justify-content: center; + align-items: baseline; + & > span { + color: var(--pi-color-primary); + &:nth-child(1) { + font: { + weight: 700; + } + } + } + } + + // Style Type + // Default + &:not(.pi-spinner__appearance--compact) { + } + // Compact + &.pi-spinner__appearance--compact { + .mat-mdc-progress-spinner { + border-radius: 50%; + } + } + // Stroke width + // 4 + &.pi-spinner__stroke--4 { + @include strokeWidth(0 0 0 var(--pi-spacing-xxx-small) var(--pi-color-grey-25) inset); + } + // 6 + &.pi-spinner__stroke--6 { + @include strokeWidth(0 0 0 0.375rem var(--pi-color-grey-25) inset); + } + // 8 + &.pi-spinner__stroke--8 { + @include strokeWidth(0 0 0 var(--pi-spacing-xx-small) var(--pi-color-grey-25) inset); + } +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + @include progressSpinnerSize; + @include fontSizesClass; + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + @include progressSpinnerSize(var(--pi-font-size-large), var(--pi-font-size-small)); + @include fontSizesClass( + var(--pi-font-size-xxx-large), + var(--pi-font-size-x-small), + var(--pi-font-size-xxxx-large), + var(--pi-font-size-small) + ); + } + } +} diff --git a/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.ts b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.ts new file mode 100644 index 0000000..4409dce --- /dev/null +++ b/libs/ui/src/lib/ui/progress/progress-spinner/progress-spinner.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +@Component({ + standalone: false, + selector: 'pi-progress-spinner', + templateUrl: './progress-spinner.component.html', + styleUrls: ['./progress-spinner.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiProgressSpinnerComponent { + @HostBinding('class') + className = 'pi-spinner pi-spinner__progress'; + + @HostBinding('class') get classes(): string { + const appearanceClass = + this.appearance !== 'default' && `pi-spinner__appearance--${this.appearance}`; + const strokeWidthClass = this.strokeWidth && `pi-spinner__stroke--${this.strokeWidth}`; + const smallDiameterClass = + this.diameter === 60 || (this.diameter === 70 && 'pi-spinner__diameter--small'); + const largeDiameterClass = + this.diameter === 110 || (this.diameter === 130 && 'pi-spinner__diameter--large'); + return [ + 'pi-spinner', + 'pi-spinner__progress', + appearanceClass, + strokeWidthClass, + smallDiameterClass, + largeDiameterClass, + ].join(' '); + } + + @Input() value!: number; + @Input() label!: string; + @Input() appearance: 'compact' | 'default' = 'default'; + @Input() strokeWidth: 2 | 3 | 4 | 5 | 6 | 7 | 8 = 6; + @Input() diameter!: number; +} diff --git a/libs/ui/src/lib/ui/progress/ui-progress.module.ts b/libs/ui/src/lib/ui/progress/ui-progress.module.ts new file mode 100644 index 0000000..c4debc3 --- /dev/null +++ b/libs/ui/src/lib/ui/progress/ui-progress.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiProgressBarComponent } from './progress-bar/progress-bar.component'; +import { PiProgressSpinnerComponent } from './progress-spinner/progress-spinner.component'; + +// MATERIAL ANGULAR +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +const MATERIAL_IMPORTS = [MatProgressBarModule, MatProgressSpinnerModule]; + +@NgModule({ + imports: [CommonModule, MATERIAL_IMPORTS], + declarations: [PiProgressBarComponent, PiProgressSpinnerComponent], + exports: [PiProgressBarComponent, PiProgressSpinnerComponent], +}) +export class PiSharedUIProgressModule {} diff --git a/libs/ui/src/lib/ui/select/base-select.component.ts b/libs/ui/src/lib/ui/select/base-select.component.ts new file mode 100644 index 0000000..a459d2a --- /dev/null +++ b/libs/ui/src/lib/ui/select/base-select.component.ts @@ -0,0 +1,432 @@ +import { + Component, + ContentChild, + ContentChildren, + ElementRef, + HostBinding, + Injector, + Input, + QueryList, + Renderer2, + TemplateRef, + ViewChildren, +} from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { PiFormControl } from '../../util/form-control'; +import { PiSelectOptionComponent } from './select-option/select-option.component'; +import { + SelectColor, + SelectOptionFlexDirection, + SelectOptionSize, + actionIcon, +} from './entities/select.model'; + +@Component({ + standalone: false, + template: '', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiBaseSelectComponent, + multi: true, + }, + ], +}) +export class PiBaseSelectComponent extends PiFormControl { + @HostBinding('class') get commonClasses(): string { + const isDisabledClass = this.disabled ? 'is-disabled' : ''; + return ['pi-select', isDisabledClass].join(' '); + } + + // ====================== + // FORM CONFIGURATION + // ====================== + + /** + * Name of the form control. + */ + @Input() formControlName!: string; + + /** + * Property used for comparisons. + */ + @Input() keyIdProperty!: string; + + // ====================== + // SKIN + // ====================== + + /** + * Specifies the layout for the menu. + */ + @Input() menuType: 'default' | 'flex-wrap' | 'custom-grid' = 'default'; + + /** + * Defines the type of the select component. + */ + @Input() type: 'default' | 'dropdown' = 'default'; + + /** + * The appearance style of the select component. + */ + + @Input() appearance: 'fill' | 'outline' | 'ghost' = 'fill'; + + /** + * The type of the input. + */ + @Input() pickedOptionType: 'button' | 'input' = 'input'; + + /** + * Defines the color of the select component and its child components. + */ + @Input() color: SelectColor = 'primary'; + + /** + * Defines the color of the select component and its child components. + */ + @Input() menuColor: 'white' | 'transparent' = 'white'; + + /** + * The flex direction property binded in the select component is used to bind + * the pre definded select option flex direction property. + */ + + @Input() flexDirection: SelectOptionFlexDirection = 'row'; + + /** + * The text align property defines if the picked option text in type dropdown cases and + * can extends the text alignment from the selec option when we use Back End data. + */ + + @Input() textAlign: 'left' | 'center' = 'left'; + /** + * An optional icon for the select. + */ + @Input() icon: string | undefined; + + /** + * An optional icon for the select. + */ + @Input() actionIcon: actionIcon = 'chevron-forward'; + + // ====================== + // CONTENT & DATA + // ====================== + + /** + * List of selectable options. + */ + @Input() options: Array = []; + + /** + * An optional label for the select. + */ + @Input() label: string | undefined; + + /** + * Placeholder for the select when no option is selected. + */ + @Input() placeholder: string | undefined; + + /** + * Defines the select an select option size in picked option type button cases + */ + @Input() size: 'small' | 'medium' = 'medium'; + + + /** + * Defines if the picked option is responsive or not (ie. if it disappears in small devices or not) + */ + @Input() isResponsive = true; + + // ====================== + // CONTENT PROJECTIONS + // ====================== + + /** + * Provides a reference to the custom template for the options if provided. + */ + @ContentChild('optionTemplate', { static: false }) + optionTemplateRef!: TemplateRef; + + /** + * Provides a reference to the custom template for the selected item if provided. + */ + @ContentChild('selectedTemplate') selectedTemplateRef!: TemplateRef; + + /** + * A query list of all the child option components that are content projected from an external consumer. + * These components are typically passed into this component's content slot from outside. + */ + @ContentChildren(PiSelectOptionComponent) childrenOptionsComponents: + | QueryList + | undefined; + + /** + * A query list of native element references for the child option components + * that are content projected from an external consumer. + * This provides direct access to their corresponding DOM elements. + */ + @ContentChildren(PiSelectOptionComponent, { read: ElementRef }) + childrenOptionsEl!: QueryList; + + /** + * A query list of all the child option components that are directly defined (view children) + * within this component's template. + * These are the default option components provided by the component itself. + */ + @ViewChildren(PiSelectOptionComponent) + viewChildOptionsComponents: QueryList; + + /** + * A query list of native element references for the child option components + * that are directly defined (view children) within this component's template. + * This provides direct access to their corresponding DOM elements. + */ + @ViewChildren(PiSelectOptionComponent, { read: ElementRef }) + viewChildOptionsEl: QueryList; + + constructor( + protected el: ElementRef, + protected override injector: Injector, + protected readonly _renderer: Renderer2 + ) { + super(injector); + } + + trackByFn(index: number): number | undefined { + return index; + } + + /** + * Returns the FormControl associated with the formControlName. + */ + get control(): FormControl { + return this.controlContainer.control?.get(this.formControlName) as FormControl; + } + + /** + * Retrieves the native element of the select. + */ + get selectElement(): HTMLElement { + return this.el.nativeElement; + } + + /** + * Determines if the control has an error and returns an appropriate class. + */ + get errorClass(): string { + return this.control?.errors ? 'has-error' : ''; + } + + /** + * Toggles the visibility of the dropdown menu. + */ + onToggleDropdown(): void { + if (this.disabled) return; + if (this.isMenuVisible()) { + this.setIsHidden(); + } else { + this.setIsVisible(); + } + } + + /** + * Sets the state of the component to visible. + */ + protected setIsVisible(): void { + this.selectElement && this._renderer.addClass(this.selectElement, 'is-expanded'); + } + + /** + * Sets the state of the component to hidden. + */ + protected setIsHidden(): void { + this.selectElement && this._renderer.removeClass(this.selectElement, 'is-expanded'); + } + + /** + * Checks if the menu (options) is visible. + * @returns True if the menu is visible, false otherwise. + */ + protected isMenuVisible(): boolean { + return this.selectElement.classList.contains('is-expanded'); + } + + /** + * Manage the Select Option size + * + * When the select option is not instanced in the template, for BE data + */ + + get selectOptionSize(): SelectOptionSize { + switch (true) { + case this.type === 'dropdown' && this.pickedOptionType === 'input': + return SelectOptionSize.Small; + case this.type === 'dropdown' && this.pickedOptionType === 'button' && this.size === 'medium': + return SelectOptionSize.Medium; + case this.type === 'dropdown' && this.pickedOptionType === 'button' && this.size === 'small': + return SelectOptionSize.Small; + case this.type === 'default' && this.menuType === 'flex-wrap': + return SelectOptionSize.Large; + case this.type === 'default' && this.menuType === 'default' && this.appearance === 'ghost': + return SelectOptionSize.Medium; + case this.type === 'default' && this.menuType === 'default' && this.appearance !== 'ghost': + return SelectOptionSize.Small; + default: + return SelectOptionSize.Medium; + // Default value if no one of the cases match + } + } + + /** + * Get SKIN scss Classes + */ + + /** + * Manage the Select & Select Multiple appearance property + */ + protected get appearanceClass(): string | undefined { + return ( + this.menuType !== 'custom-grid' && + this.appearance && + `pi-select__appearance--${this.appearance}` + ); + } + + /** + * Manage the Select & Select Multiple Theming Color Class property + * + * This property affectes to: + * + * A) the picked option in type !== 'dropdown'. + * + * B) the select options to, binding a class to manage the options. + * + * color depending on the user profile. + */ + protected get colorClass(): string | undefined { + return this.menuType !== 'custom-grid' && this.color && `pi-color__${this.color}`; + } + + /** + * Manage the Select & Select Multiple Menu Color Class property + * + * This property can be modified from the default one (white) specially when type !== 'dropdown'. + */ + protected get menuColorClass(): string | undefined { + return this.menuType !== 'custom-grid' && this.menuColor !== 'white' + ? `pi-select__menu-color--${this.menuColor}` + : ''; + } + + /** + * Manage the Select & Select Multiple Menu Type Class property + * + * This property defines if: + * + * A) the Menu Type ==> flex direction: column + * as default property + * + * B) the Menu Type ==> flex direction: row && flex-wrap: wrap + * to display the select options as a mosaic in the page + */ + protected get menuTypeClass(): string | undefined { + return this.menuType === 'default' + ? 'pi-select__menu-type--column' + : `pi-select__menu-type--${this.menuType}`; + } + + /** + * Manage the Select & Select Multiple Picked Option Type Class property + * This only applies to the dropdown + * + * This property defines if: + * + * A) the Picked Option Type === Input + * it means that the picked option emulate a Field or an Input. + * + * B) the Picked Option Type === Button + * it means that the picked option emulate a Button. + */ + protected get pickedOptionTypeClass(): string | undefined { + return ( + this.type === 'dropdown' && + this.menuType !== 'custom-grid' && + this.pickedOptionType && + `pi-select__picked-option-type--${this.pickedOptionType}` + ); + } + + /** + * Manage the Select & Select Multiple Type Class property + * This defines if: + * + * This property defines if: + * + * A) + * a.1) Large device: + * the select options are displayed in a Dropdown menu, + * with an absolut position (nearest ancestor). + * + * a.2) Small device: + * the select options are displayed in a Hub menu, + * with a fixed position (viewport). + * When the menu is visible, it appears over the navbar. + * + * B) The select options are displayed in a default menu, + * with menu type flex direction or column, + * in a static positioned menu. + */ + protected get typeClass(): string | undefined { + return this.type && `pi-select__type--${this.type}`; + } + + /** + * Manage the Select & Select Multiple Text Alignment property + * + * This property can only be used in two cases + * + * A) Select Type Default && + * + * This defines if the text alignment and justify content is center or left and flex start: + * + * This property defines: + * + * A) The picked option text alignment in Select Type === Dropdown cases. + * + * B) The select option text aligment and justify content. + * + */ + protected get textAlignClass(): string | boolean { + return (this.textAlign !== 'left' && this.pickedOptionType === 'button') || + (this.menuType === 'flex-wrap' && this.flexDirection === 'row') || + this.menuType === 'default' + ? `pi-select__text-align--${this.textAlign}` + : ''; + } + + /** + * Manage the Select & Select Multiple Picked Option and Select Options Sizes + * + * This property can only be used in one case + * + * When the Select Picked Option Type === Button + * + */ + protected get sizeClass(): string | boolean { + return this.pickedOptionType === 'button' && this.size ? `pi-select__${this.size}` : ''; + } + + /** + * Manage the Select & Select Multiple Picked Option Responsive Class + * + * This property can only be used in one case + * + * When the Select Picked Option Type === Button + */ + protected get isResponsiveClass(): string | boolean { + return this.pickedOptionType === 'button' && this.isResponsive ? 'is-responsive' : ''; +} +} diff --git a/libs/ui/src/lib/ui/select/entities/select.model.ts b/libs/ui/src/lib/ui/select/entities/select.model.ts new file mode 100644 index 0000000..9362b7c --- /dev/null +++ b/libs/ui/src/lib/ui/select/entities/select.model.ts @@ -0,0 +1,30 @@ +import { styleType } from '../../../styles'; +import { status } from '../../../util/status'; + +export interface SelectOption { + id: number; + name?: string; + value: any; + initials?: string; + description?: string; + disabled?: boolean; + icon?: string; +} + +export type actionIcon = 'chevron-forward' | 'filter' | 'sort'; + +export enum SelectOptionSize { + Small = 'small', + Medium = 'medium', + Large = 'large', +} + +export type SelectOptionFlexDirection = 'row' | 'column' | 'row-reverse'; + +export const StatusRadioColorMap = new Map([ + ['disabled', 'grey'], + ['info', 'white'], + ['success', 'primary'], +]); + +export type SelectColor = 'grey' | 'primary' | 'accent' | 'highlighted' | 'warn' | 'success'; \ No newline at end of file diff --git a/libs/ui/src/lib/ui/select/index.ts b/libs/ui/src/lib/ui/select/index.ts new file mode 100644 index 0000000..10254fc --- /dev/null +++ b/libs/ui/src/lib/ui/select/index.ts @@ -0,0 +1,12 @@ +export * from './ui-select.module'; + +export * from './select-multiple/select-multiple.component'; +export * from './select-option/select-option.component'; +export * from './select-period/select-period.component'; +export * from './select/select.component'; + +export * from './entities/select.model'; + +export * from './base-select.component'; + +export * from './select-carousel/select-carousel.component'; diff --git a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.html b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.html new file mode 100644 index 0000000..8655699 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.scss b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.scss new file mode 100644 index 0000000..b2b9967 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.scss @@ -0,0 +1,8 @@ +:host { + .pi-select-options-menu { + .pi-select__carousel--option { + display: flex; + justify-content: center; + } + } +} diff --git a/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts new file mode 100644 index 0000000..6aa9783 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-carousel/select-carousel.component.ts @@ -0,0 +1,207 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + Injector, + Input, + QueryList, + Renderer2, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import _ from 'lodash'; +import { merge } from 'rxjs'; +import { PiBaseSelectComponent } from '../base-select.component'; +import { PiSelectOptionComponent } from '../select-option/select-option.component'; +import { Device } from '../../../util/device-configs'; +import { justify } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-select-carousel', + templateUrl: './select-carousel.component.html', + styleUrls: ['./select-carousel.component.scss', '../styles/base-select.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSelectCarouselComponent, + multi: true, + }, + ], +}) +export class PiSelectCarouselComponent extends PiBaseSelectComponent implements AfterContentInit { + /** + * Binds a CSS class name to the host element. + * Represents the default class for the select component. + */ + @HostBinding('class') get classes(): string { + return [ + 'pi-select__carousel', + this.appearanceClass, + this.colorClass, + this.menuColorClass, + this.menuTypeClass, + this.pickedOptionTypeClass, + this.textAlignClass, + this.typeClass, + ].join(' '); + } + + //CAROUSEL PROPERTIES + @Input() device: Device; + + @Input() itemsDesktop!: number; + + @Input() itemsTablet: number; + + @Input() itemsMobile: number; + + @Input() itemWidth: number; + + @Input() movePerPage: boolean; + + @Input() hideSwipeButtons: boolean; + + @Input() isDraggable: boolean; + + @Input() justifyCarousel: justify; + + /** + * Component's constructor. Initializes the component with required services. + * @param translateService - Service used for translating option names. + * @param el - Element reference of the component. + * @param injector - Injector to get instances of dependencies. + * @param _renderer - Renderer to modify native elements. + */ + constructor( + private readonly translateService: TranslateService, + protected override el: ElementRef, + protected override injector: Injector, + protected override readonly _renderer: Renderer2, + private _cdr: ChangeDetectorRef + ) { + super(el, injector, _renderer); + } + + /** + * Getter for the value of the control. + * @returns The value of the control as a string. + */ + get controlValue(): any { + return this.control.value; + } + + /** + * Getter for the selected option based on the control value. + * @returns The selected option. + */ + get selectedOption(): any { + return this.options.find((option) => _.isEqual(this.controlValue, option)); + } + + /** + * Getter for the name of the selected option. + * Uses the `translateService` to get the translated name. + * @returns The translated name of the selected option. + */ + get selectedOptionsName(): Array { + return this.translateService.instant(this.selectedOption.name); + } + + /** + * Angular lifecycle hook that is called after Angular has fully initialized + * a component's content. This method sets up reactive subscriptions to listen + * for changes in either content children or view children. + * + * The method uses the `merge` RxJS operator to combine streams of changes + * from both `childrenOptionsComponents` (content children) and + * `viewChildOptionsComponents` (view children). This ensures that whenever + * any changes occur in either of the child lists, the visual state of the + * select options is updated appropriately. + * + * Using `Promise.resolve().then(...)`, the method ensures that the + * `updateChildrenOptionsSelectedState` method is executed in the next + * microtask, after any ongoing change detection cycles are completed. + * This approach ensures consistent and correct behavior in all scenarios. + * + * Lastly, it proactively calls `updateChildrenOptionsSelectedState` to + * set the initial state of the child options based on the current value of + * `selectedOption`. + */ + ngAfterContentInit(): void { + this.control.setValue(this.selectedOption); + merge( + this.childrenOptionsComponents?.changes || [], + this.viewChildOptionsComponents?.changes || [], + this.control.valueChanges + ).subscribe(() => { + Promise.resolve().then(() => { + this.updateChildrenOptionsSelectedState(this.selectedOption); + }); + }); + } + + /** + * Handles the logic for selecting an option. + * Sets the control value to the value of the selected option and updates child states. + * @param option - The option to be selected. + */ + onSelectOption(option: any) { + this.control.setValue(option); + this.updateChildrenOptionsSelectedState(option); + } + + onItemChanged(event: number) { + this.onSelectOption(this.options[event]); + } + + /** + * Updates the visual state of child options based on the selected option. + * + * This method first determines whether the child options are content projected + * from the parent or are internally defined within the component. Depending on + * the source, it then updates the visual state of each child option accordingly. + * + * @param selectedOption - The currently selected option. + */ + private updateChildrenOptionsSelectedState(selectedOption: any): void { + // Array of child option components to work with. + let childrenOptionsComponent: Array; + // Reference to the child option elements for DOM manipulation. + let childrenOptionsElementRef: QueryList; + + // Check if we have child options that are content projected from a parent component. + if (this.childrenOptionsComponents && this.childrenOptionsComponents.length > 0) { + childrenOptionsComponent = this.childrenOptionsComponents.toArray(); + childrenOptionsElementRef = this.childrenOptionsEl; + } else { + // If not, it means they're internally defined (default), so we fetch from viewChildren. + childrenOptionsComponent = this.viewChildOptionsComponents?.toArray(); + childrenOptionsElementRef = this.viewChildOptionsEl; + } + + // Update the visual state of each child option. + childrenOptionsComponent?.forEach((childOption, index) => { + const isSelected = selectedOption + ? selectedOption[this.keyIdProperty] === childOption['id'] + : false; + // If the current child option matches the selected option, highlight it. + if (isSelected) { + this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + childOption.isSelected = true; + } else { + // Otherwise, ensure it is not highlighted. + this._renderer.removeClass( + childrenOptionsElementRef.get(index)?.nativeElement, + 'is-selected' + ); + childOption.isSelected = false; + } + }); + this._cdr.markForCheck(); + } +} diff --git a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.html b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.html new file mode 100644 index 0000000..8e3bc3a --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.html @@ -0,0 +1,171 @@ + + + +
+ +
+ + {{ + (selectedOptionsNames?.join(', ') | truncate : 33) || (label | translate) + }} +
+ +
+
+ + + + +
+ + +
+ + + + {{ + selectedOptionsNames?.join(', ') | truncate : 33 || (placeholder | translate) + }} + + + + + + +
+
+
+ + + +
+ + +
+ + + {{ + selectedOptionsNames?.join(', ') | truncate : 33 || (placeholder | translate) + }} + + + + + + +
+
+
+
+
+
+ + + diff --git a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.scss b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.scss new file mode 100644 index 0000000..9173052 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.scss @@ -0,0 +1,44 @@ +:host { + // Select Multiple + &.pi-select__multiple { + // Options menu + .pi-select-options-menu { + // Options menu bottom action bar + & > footer { + padding-right: var(--pi-spacing-small); + & > span { + display: flex; + justify-content: center; + align-items: flex-end; + height: calc(var(--pi-spacing-xxx-large) + var(--pi-spacing-xx-small)); + border-top: 1px solid; + border-color: var(--pi-color-grey-75); + } + &::before { + content: ''; + border-color: var(--pi-color-grey-75); + } + } + } + + // Type + // Dropdown + &.pi-select__type--dropdown { + .pi-select-options-menu { + flex-direction: column; + } + } + &:not(.pi-select__type--dropdown) { + .pi-select-options-menu { + & > .pi-select-options-menu__select-all { + display: flex; + gap: var(--pi-spacing-x-small); + align-items: center; + } + & > .pi-select-options-menu__select-all + .pi-select-options-menu__options { + margin-top: var(--pi-spacing-x-large); + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts new file mode 100644 index 0000000..b93856c --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-multiple/select-multiple.component.ts @@ -0,0 +1,301 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + Injector, + Input, + QueryList, + Renderer2, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { merge } from 'rxjs'; +import { PiBaseSelectComponent } from '../base-select.component'; +import { PiSelectOptionComponent } from '../select-option/select-option.component'; +import _ from 'lodash'; +import { MatCheckboxChange } from '@angular/material/checkbox'; + +@Component({ + standalone: false, + selector: 'pi-select-multiple', + templateUrl: './select-multiple.component.html', + styleUrls: ['./select-multiple.component.scss', '../styles/base-select.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSelectMultipleComponent, + multi: true, + }, + ], +}) +export class PiSelectMultipleComponent extends PiBaseSelectComponent implements AfterContentInit { + /** + * Binds a CSS class name to the host element. + * Represents the default class for the select component. + */ + @HostBinding('class') get classes(): string { + return [ + 'pi-select__multiple', + this.appearanceClass, + this.colorClass, + this.menuColorClass, + this.menuTypeClass, + this.pickedOptionTypeClass, + this.sizeClass, + this.textAlignClass, + this.typeClass, + this.isResponsiveClass + ].join(' '); + } + + /** + * Specifies the maximum number of selectable options. + * Defaults to `Infinity` if not provided. + */ + @Input() maxLength: number | undefined = Infinity; + + /** + * Specifies whether the select component has an option to select all options at once. + * Defaults to `false` if not provided. + */ + @Input() hasSelectAllOptions = false; + + /** + * Provides the label to display next to the select all option. + */ + @Input() selectAllLabel: string; + + constructor( + private readonly translateService: TranslateService, + protected override el: ElementRef, + protected override injector: Injector, + protected override readonly _renderer: Renderer2, + private _cdr: ChangeDetectorRef + ) { + super(el, injector, _renderer); + } + + /** + * Retrieves the values of the selected options. + */ + get controlValue(): Array { + return this.control.value; + } + + /** + * Retrieves a list of selected options. + */ + @Input() get selectedOptions(): Array { + if (!this.controlValue) return []; + return this.options.filter((option) => _.some(this.controlValue, option)); + } + + /** + * Retrieves the names of the selected options. + */ + get selectedOptionsNames(): Array { + return this.selectedOptions.map((option) => this.translateService.instant(option.name)); + } + + get enabledOptions(): Array { + return this.options.filter((option) => !option.disabled); + } + + get areAllEnabledOptionsSelected(): boolean { + return this.enabledOptions.length === this.selectedOptions.length; + } + + get areAllOptionsSelected(): boolean { + return this.options.length === this.selectedOptions.length; + } + + /** + * Angular lifecycle hook that is called after Angular has fully initialized + * a component's content. This method sets up reactive subscriptions to listen + * for changes in either content children or view children. + * + * The method uses the `merge` RxJS operator to combine streams of changes + * from both `childrenOptionsComponents` (content children) and + * `viewChildOptionsComponents` (view children). This ensures that whenever + * any changes occur in either of the child lists, the visual state of the + * select options is updated appropriately. + * + * Using `Promise.resolve().then(...)`, the method ensures that the + * `updateChildrenOptionsSelectedState` method is executed in the next + * microtask, after any ongoing change detection cycles are completed. + * This approach ensures consistent and correct behavior in all scenarios. + * + * Lastly, it proactively calls `updateChildrenOptionsSelectedState` to + * set the initial state of the child options based on the current value of + * `selectedOptions`. + */ + ngAfterContentInit(): void { + this.control.setValue(this.selectedOptions); + merge( + this.childrenOptionsComponents?.changes || [], + this.viewChildOptionsComponents?.changes || [], + this.control.valueChanges + ).subscribe(() => { + Promise.resolve().then(() => { + this.updateChildrenOptionsSelectedState(this.selectedOptions); + this.updateChildrenOptionsDisabledState(this.selectedOptions); + }); + }); + } + + /** + * Resets the list of selected options. + */ + onClickReset(): void { + this.control.setValue([]); + } + + /** + * Hides the dropdown after saving the selected options. + */ + onClickSave(): void { + this.setIsHidden(); + } + + /** + * Handles the logic for selecting and deselecting options. + * + * @remarks + * - Adds the option to the list of selected options when selected. + * - Removes the option from the list when deselected. + * - Ensures that the number of selected options doesn't exceed `maxLength`. + * + * @param option - The option to be selected or deselected. + */ + onSelectOption(option: any): void { + if (option.disabled) return; + const selectedOptions: any[] = [...(this.selectedOptions || [])]; + const optionIndex = selectedOptions.findIndex( + (id) => id[this.keyIdProperty] === option[this.keyIdProperty] + ); + + if (optionIndex !== -1) { + selectedOptions.splice(optionIndex, 1); + } else if (selectedOptions.length < (this.maxLength || Infinity)) { + selectedOptions.push(option); + } else { + return; + } + + this.control.setValue(selectedOptions); + this.updateChildrenOptionsSelectedState(selectedOptions); + this.updateChildrenOptionsDisabledState(selectedOptions); + } + + selectAll(): void { + this.control.setValue(this.enabledOptions); + } + + onSelectAllOptions(event: MatCheckboxChange): void { + if (event.checked) { + this.selectAll(); + } else { + this.onClickReset(); + } + } + + /** + * Updates the visual state of child options based on the currently selected options. + * + * This method determines the source of the child options — whether they are content projected + * from an external component or are defined within this component. + * + * The visual state of each child option is then adjusted based on the provided list of selected options. + * If a child option is in the list of selected options, it is visually marked as selected. + * + * @param selectedOptions - The current list of selected options. + */ + private updateChildrenOptionsSelectedState(selectedOptions: any[]): void { + // Array of child option components to work with. + let childrenOptionsComponent: Array; + // Reference to the child option elements for DOM manipulation. + let childrenOptionsElementRef: QueryList; + + // Check if we have child options that are content projected from a parent component. + if (this.childrenOptionsComponents && this.childrenOptionsComponents.length > 0) { + childrenOptionsComponent = this.childrenOptionsComponents.toArray(); + childrenOptionsElementRef = this.childrenOptionsEl; + } else { + // If not, it means they're internally defined (default), so we fetch from viewChildren. + childrenOptionsComponent = this.viewChildOptionsComponents.toArray(); + childrenOptionsElementRef = this.viewChildOptionsEl; + } + + childrenOptionsComponent?.forEach((childOption, index) => { + const isSelected = selectedOptions.some( + (selectedOption) => selectedOption[this.keyIdProperty] === childOption['id'] + ); + if (isSelected) { + this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + childOption.isSelected = true; + } else { + this._renderer.removeClass( + childrenOptionsElementRef.get(index)?.nativeElement, + 'is-selected' + ); + childOption.isSelected = false; + } + }); + + this._cdr.markForCheck(); + } + + /** + * Updates the visual state of child options based on the currently selected options and the `maxLength` property. + * + * This method determines the source of the child options — whether they are content projected + * from an external component or are defined within this component. + * + * The visual state of each child option is adjusted based on the following: + * - If the `maxLength` property has been reached, and a child option isn't in the list of selected options, + * it is visually marked as disabled. + * - Otherwise, any disabled visual indication on the child option is removed. + * + * @param selectedOptions - The current list of selected options. + */ + private updateChildrenOptionsDisabledState(selectedOptions: any[]): void { + // Disable all options if the maximum number of selections has been reached. + const disableOthers = selectedOptions.length >= (this.maxLength || Infinity); + // Array of child option components to work with. + let childrenOptionsComponent: Array; + // Reference to the child option elements for DOM manipulation. + let childrenOptionsElementRef: QueryList; + + // Check if we have child options that are content projected from a parent component. + if (this.childrenOptionsComponents && this.childrenOptionsComponents.length > 0) { + childrenOptionsComponent = this.childrenOptionsComponents.toArray(); + childrenOptionsElementRef = this.childrenOptionsEl; + } else { + // If not, it means they're internally defined (default), so we fetch from viewChildren. + childrenOptionsComponent = this.viewChildOptionsComponents.toArray(); + childrenOptionsElementRef = this.viewChildOptionsEl; + } + + childrenOptionsComponent?.forEach((childOption, index) => { + const isSelected = selectedOptions.some( + (selectedOption) => selectedOption[this.keyIdProperty] === childOption['id'] + ); + + const isDisabled = childOption.isDisabled; + if ((!isSelected && disableOthers) || isDisabled) { + this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-disabled'); + } else { + this._renderer.removeClass( + childrenOptionsElementRef.get(index)?.nativeElement, + 'is-disabled' + ); + } + }); + + this._cdr.markForCheck(); + } +} diff --git a/libs/ui/src/lib/ui/select/select-option/_color.scss b/libs/ui/src/lib/ui/select/select-option/_color.scss new file mode 100644 index 0000000..1860cff --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/_color.scss @@ -0,0 +1,409 @@ +/*** +* Select Option single color properties +**/ + +/*** +* Current text color +**/ +@mixin selectOptionTextColor($text-color) { + .pi-select-option__label, + .pi-select-option__initials { + color: var(--pi-color-#{$text-color}); + } + &.has-icon { + .pi-select-option__icon { + color: var(--pi-color-#{$text-color}); + } + } + &.has-radio { + .pi-select-option__radio-button { + color: var(--pi-color-#{$text-color}); + } + } +} + +/*** +* Select Option Description Color +**/ +@mixin selectOptionDescriptionColor($desciption-color) { + &.has-description { + .pi-select-option__description { + color: var(--pi-color-#{$desciption-color}); + } + } +} + +@mixin selectOptionFillIconBackgroundColor($background-color) { + &.has-icon { + .pi-select-option__icon { + background-color: var(--pi-color-#{$background-color}); + } + } +} +@mixin selectOptionBorderColor($color) { + border-color: var(--pi-color-#{$color}); +} + +@mixin selectOptionBackgroundColor($color) { + background-color: var(--pi-color-#{$color}); +} + +/*** +* 1. Select Type: DEFAULT +* +* 1.1. Color Mixin for the following cases: +* +* Select Appearance: GHOST && +* Select Type: DEFAULT && +* Select Menu Type: COLUMN +* +* This combinations are composed by radio button and checkboxes gruops +* & can have or not a description, that will be always grey-300. +**/ +@mixin selectOptionAppearanceGhostMenuTypeDefaultColor($color) { + // Default (Column) + &.pi-select__appearance--ghost { + // Select Type: + // Default + &:not(.pi-select__type--dropdown) { + :host { + // Has Description + @include selectOptionDescriptionColor(grey-300); + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor(grey-900); + } + // Default && Active + &.is-selected { + @include selectOptionTextColor(#{$color}); + } + // Hover + &:hover { + // NOT Disabled && NOT Active + &:not(.is-disabled):not(.is-selected) { + @include selectOptionTextColor(#{$color}); + } + } + } + } + } +} + +/*** +* 2. Select Picked Option Type: BUTTON +* +* 2.1 Color Mixin for the following cases: +* +* Select Appearance: GHOST && +* Select Type: DROPDOWN && +* Select Menu Type: COLUMN && +* Select Picked Option Type: BUTTON +* +**/ +@mixin selectOptionAppearanceGhostPickedOptionTypeButtonColor($color) { + // Apperarance: + // Ghost + &.pi-select__appearance--ghost { + // Select Type: + + // Dropdown + &.pi-select__type--dropdown { + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + :host { + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor(grey); + } + // Active + &.is-selected { + @include selectOptionTextColor($color); + } + // Hover + &:hover { + &:not(.is-disabled):not(.is-selected) { + @include selectOptionTextColor(#{$color}-900); + @include selectOptionBackgroundColor(grey-10); + } + } + } + } + } + } +} + +/*** +* +* 2.2 Color Mixin for the following cases: +* +* Select Appearance: FILL && +* Select Type: DROPDOWN && +* Select Menu Type: COLUMN && +* Select Picked Option Type: BUTTON +* +**/ + +@mixin selectOptionAppearanceFillPickedOptionTypeButtonColor($color) { + // Apperarance: + // Fill + &.pi-select__appearance--fill { + // Select Type: + + // Dropdown + &.pi-select__type--dropdown { + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + :host { + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor($color); + @include selectOptionBackgroundColor(#{$color}-25); + } + // Active + &.is-selected { + @include selectOptionTextColor(#{$color}); + @include selectOptionBackgroundColor(#{$color}-50); + } + // Hover + &:hover { + &:not(.is-disabled) { + @include selectOptionTextColor(#{$color}); + @include selectOptionBackgroundColor(#{$color}-50); + } + } + } + } + } + } +} + +/** +* 2.3 Color Mixin for the following cases: +* +* Select Appearance: OUTLINE && +* Select Type: DROPDOWN && +* Select Menu Type: COLUMN && +* Select Picked Option Type: BUTTON +* +**/ + +@mixin selectOptionAppearanceOutlinePickedOptionTypeButtonColor($color) { + // Apperarance: + // Outline + &.pi-select__appearance--outline { + // Select Type: + + // Dropdown + &.pi-select__type--dropdown { + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + :host { + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor(#{$color}); + @include selectOptionBorderColor(#{$color}-50); + } + // Active + &.is-selected { + @include selectOptionTextColor($color); + @include selectOptionBorderColor(#{$color}-100); + } + // Hover + &:hover { + &:not(.is-disabled):not(.is-selected) { + @include selectOptionTextColor(#{$color}); + @include selectOptionBorderColor(#{$color}-100); + } + } + } + } + } + } +} + +/*** +* 3. Select Picked Option Type: INPUT || undefined +* +* 3.1 Color Mixin for the following cases: +* +* Select Appearance: FILL || OUTLINE && +* Select Type: DROPDOWN || DEFAULT (in context as inside an Accordion in Aside) && +* Select Menu Type: COLUMN && +* Select Picked Option Type: INPUT +* +**/ + +@mixin selectOptionAppearanceFillAndOutlineMenuTypeColumnColor($color) { + // Menu Type: + &.pi-select__menu-type--column { + // Select Appearance: + // Fill || Outline + &.pi-select__appearance--fill, + &.pi-select__appearance--outline { + // Default (Column) + &:not(.pi-select__picked-option-type--button) { + :host { + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor(#{$color}); + @include selectOptionBackgroundColor(#{$color}-10); + @include selectOptionBorderColor(#{$color}-50); + } + // Active + &.is-selected { + @include selectOptionTextColor($color); + @include selectOptionBackgroundColor(#{$color}-25); + @include selectOptionBorderColor(#{$color}-100); + } + // Hover + &:hover { + &:not(.is-disabled) { + @include selectOptionTextColor(#{$color}); + @include selectOptionBackgroundColor(#{$color}-50); + @include selectOptionBorderColor(#{$color}-100); + } + } + } + } + } + } +} + +/*** +* 4. Select Menu Type: FLEX WRAP +* +* 4.1 Color Mixin for the following cases: +* +* Select Menu Type: FLEX WRAP && +* +**/ + +@mixin selectOptionMenuTypeFlexWrapColor($color) { + // Menu Type: + // Flex Wrap + &.pi-select__menu-type--flex-wrap { + :host { + // Default + &:not(.is-selected):not(:hover), + &.is-disabled { + @include selectOptionTextColor(grey); + @include selectOptionBorderColor(grey-50); + @include selectOptionFillIconBackgroundColor(grey-50); + } + // Active + &.is-selected { + @include selectOptionTextColor($color); + @include selectOptionBackgroundColor(#{$color}-25); + @include selectOptionBorderColor(#{$color}-75); + @include selectOptionFillIconBackgroundColor(#{$color}-50); + } + // Hover + &:hover { + &:not(.is-disabled) { + @include selectOptionTextColor(#{$color}); + @include selectOptionBackgroundColor(#{$color}-25); + @include selectOptionFillIconBackgroundColor(#{$color}-75); + &.is-selected { + @include selectOptionBorderColor(#{$color}-200); + } + &:not(.is-selected) { + @include selectOptionBorderColor(#{$color}-100); + } + } + } + } + } +} +.pi-select { + // Theming Color: + // Primary + &.pi-color__primary { + // 1. Type: Default && Menu Type: Column && Appearance: Ghost + @include selectOptionAppearanceGhostMenuTypeDefaultColor(primary); + // 2. Type: Dropdown && Menu Type: Column && Picked Option Type: Button + // 2.1. Appearance: Ghost + @include selectOptionAppearanceGhostPickedOptionTypeButtonColor(primary); + // 2.2. Appearance: Fill + @include selectOptionAppearanceFillPickedOptionTypeButtonColor(primary); + // 2.3. Appearance: Outline + @include selectOptionAppearanceOutlinePickedOptionTypeButtonColor(primary); + // 3. Appearance: Fill || Outline && Menu Type: Column + @include selectOptionAppearanceFillAndOutlineMenuTypeColumnColor(primary); + // 4. Menu Type: Flex Wrap + @include selectOptionMenuTypeFlexWrapColor(primary); + } + // Accent + &.pi-color__accent { + // 1. Type: Default && Menu Type: Column && Appearance: Ghost + @include selectOptionAppearanceGhostMenuTypeDefaultColor(accent); + // 2. Type: Dropdown && Menu Type: Column && Picked Option Type: Button + // 2.1. Appearance: Ghost + @include selectOptionAppearanceGhostPickedOptionTypeButtonColor(accent); + // 2.2. Appearance: Fill + @include selectOptionAppearanceFillPickedOptionTypeButtonColor(accent); + // 2.3. Appearance: Outline + @include selectOptionAppearanceOutlinePickedOptionTypeButtonColor(accent); + // 3. Appearance: Fill || Outline && Menu Type: Column + @include selectOptionAppearanceFillAndOutlineMenuTypeColumnColor(accent); + // 4. Menu Type: Flex Wrap + @include selectOptionMenuTypeFlexWrapColor(accent); + } + // Highlighted + &.pi-color__highlighted { + // 1. Type: Default && Menu Type: Column && Appearance: Ghost + @include selectOptionAppearanceGhostMenuTypeDefaultColor(highlighted); + // 2. Type: Dropdown && Menu Type: Column && Picked Option Type: Button + // 2.1. Appearance: Ghost + @include selectOptionAppearanceGhostPickedOptionTypeButtonColor(highlighted); + // 2.2. Appearance: Fill + @include selectOptionAppearanceFillPickedOptionTypeButtonColor(highlighted); + // 2.3. Appearance: Outline + @include selectOptionAppearanceOutlinePickedOptionTypeButtonColor(highlighted); + // 3. Appearance: Fill || Outline && Menu Type: Column + @include selectOptionAppearanceFillAndOutlineMenuTypeColumnColor(highlighted); + // 4. Menu Type: Flex Wrap + @include selectOptionMenuTypeFlexWrapColor(highlighted); + } + + // Status colors + // Success + &.pi-color__success { + // 1. Type: Default && Menu Type: Column && Appearance: Ghost + @include selectOptionAppearanceGhostMenuTypeDefaultColor(success); + // 2. Type: Dropdown && Menu Type: Column && Picked Option Type: Button + // 2.1. Appearance: Ghost + @include selectOptionAppearanceGhostPickedOptionTypeButtonColor(success); + // 2.2. Appearance: Fill + @include selectOptionAppearanceFillPickedOptionTypeButtonColor(success); + // 2.3. Appearance: Outline + @include selectOptionAppearanceOutlinePickedOptionTypeButtonColor(success); + // 3. Appearance: Fill || Outline && Menu Type: Column + @include selectOptionAppearanceFillAndOutlineMenuTypeColumnColor(success); + // 4. Menu Type: Flex Wrap + @include selectOptionMenuTypeFlexWrapColor(success); + } + // Error + &.pi-color__warn { + // 1. Type: Default && Menu Type: Column && Appearance: Ghost + @include selectOptionAppearanceGhostMenuTypeDefaultColor(warn); + // 2. Type: Dropdown && Menu Type: Column && Picked Option Type: Button + // 2.1. Appearance: Ghost + @include selectOptionAppearanceGhostPickedOptionTypeButtonColor(warn); + // 2.2. Appearance: Fill + @include selectOptionAppearanceFillPickedOptionTypeButtonColor(warn); + // 2.3. Appearance: Outline + @include selectOptionAppearanceOutlinePickedOptionTypeButtonColor(warn); + // 3. Appearance: Fill || Outline && Menu Type: Column + @include selectOptionAppearanceFillAndOutlineMenuTypeColumnColor(warn); + // 4. Menu Type: Flex Wrap + @include selectOptionMenuTypeFlexWrapColor(warn); + } +} diff --git a/libs/ui/src/lib/ui/select/select-option/_size.scss b/libs/ui/src/lib/ui/select/select-option/_size.scss new file mode 100644 index 0000000..ffe87f8 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/_size.scss @@ -0,0 +1,392 @@ +// SELECT OPTION SIZE + +/* +* Select Option Height +*/ +@mixin selectOptionHeight($height) { + height: $height; +} + +/* +* Select Option Width +*/ +@mixin selectOptionWidth($width) { + width: $width; +} + +/* +* Select Option Side Spacings +*/ +@mixin selectOptionSpacings($spacing-large, $spacing-small) { + // Side spacings depending on type property: + // Single + &.pi-select-option__type--single { + padding-left: $spacing-large; + padding-right: $spacing-large; + } + // Multiple + &.pi-select-option__type--multiple { + padding-left: $spacing-small; + padding-right: $spacing-large; + } + // Flex wrap + &.pi-select-option__type--flex-wrap { + &.has-icon { + padding-left: $spacing-small; + padding-right: $spacing-large; + } + &:not(.has-icon) { + padding-left: $spacing-large; + padding-right: $spacing-large; + } + } +} +/* +* Select Option Border Radius +*/ +@mixin selectOptionBorderRadius($border-radius) { + border-radius: $border-radius; + -webkit-border-radius: $border-radius; + -moz-border-radius: $border-radius; +} + +/* +* Select Option Font Sizes +* +* Label +* Description +* Initials +*/ + +@mixin selectOptionFontSizes($label-font-size, $description-font-size, $initials-font-size) { + .pi-select-option__label { + font: { + size: var(--pi-font-size-#{$label-font-size}); + } + } + .pi-select-option__description { + font: { + size: var(--pi-font-size-#{$description-font-size}); + } + } + .pi-select-option__initials { + font: { + size: var(--pi-font-size-#{$initials-font-size}); + } + } +} + +/* +* Select Option Appearance Ghost Has Description Sizes +*/ +@mixin selectOptionAppearanceGhostHasDescriptionSizes( + $side-gap, + $has-description-vertical-gap, + $min-height +) { + // Has radio || Has checkbox + &.has-radio, + &.has-checkbox { + & > div { + gap: $side-gap; + } + } + + // Has decription + &.has-description { + min-height: $min-height; + & > div { + & > span { + gap: $has-description-vertical-gap; + margin-top: var(--pi-spacing-xxxx-small); + } + } + } +} + +/* +* Select Option Radio Button Size +*/ +@mixin selectOptionRadioButtonSize($radio-size) { + .pi-select-option__radio-button { + height: $radio-size; + width: $radio-size; + min-width: $radio-size; + max-width: $radio-size; + } +} + +/* +* Select Option Max Width ch +*/ +@mixin selectOptionMaxCharacters($max-width) { + overflow: hidden; + & > div { + overflow: hidden; + & > span { + overflow: hidden; + text-overflow: ellipsis; + } + } + .pi-select-option__label { + white-space: nowrap; + overflow: hidden; + max-width: $max-width; + } +} + +/* +* Select Option for Select Appearance Ghost Sizes +*/ +@mixin selectOptionAppearanceGhostSizes { + .pi-select { + // Default (Column) & Ghost appearance + &.pi-select__appearance--ghost { + // NOT type button + &:not(.pi-select__picked-option-type--button) { + :host { + // Size: + // Medium + /* + * side-gap, has-description-vertical-gap, label-font-size, description-font-size + */ + &.pi-select-option__medium { + @include selectOptionAppearanceGhostHasDescriptionSizes( + var(--pi-spacing-x-small), + var(--pi-spacing-xxxx-small), + 3.125rem + ); + @include selectOptionFontSizes(small, x-small, small); + @include selectOptionRadioButtonSize(var(--pi-spacing-large)); + } + // // Large + &.pi-select-option__large { + @include selectOptionAppearanceGhostHasDescriptionSizes( + var(--pi-spacing-small), + var(--pi-spacing-xxx-small), + 3.875rem + ); + @include selectOptionFontSizes(medium, small, medium); + @include selectOptionRadioButtonSize(var(--pi-spacing-large)); + } + // NOT Has description + &:not(.has-description) { + // Medium + &.pi-select-option__medium { + @include selectOptionHeight(var(--pi-spacing-xxx-large)); + @include selectOptionFontSizes(small, x-small, small); + } + // Large + &.pi-select-option__large { + @include selectOptionHeight(var(--pi-spacing-xxxx-large)); + @include selectOptionFontSizes(medium, small, medium); + } + } + } + } + // Select Type: Dropown || Menu Type: Default (Column) + &.pi-select__type--dropdown, + &.pi-select__menu-type--column { + /* + ** Picked Option Type Contitions the Select Option Sizes + ** Size SMALL does not exists in the Piked Option Type Button cases + ** + */ + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + :host { + // Size: + + // Size: + // Small + &.pi-select-option__small { + @include selectOptionFontSizes(x-small, x-small, x-small); + @include selectOptionMaxCharacters(30ch); + } + + // Small Medium & Large + &.pi-select-option__small, + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionHeight(var(--pi-spacing-xxx-large)); + @include selectOptionSpacings(var(--pi-spacing-small), var(--pi-spacing-x-small)); + @include selectOptionBorderRadius(0.875rem); + } + + // Medium & Large + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionFontSizes(small, small, small); + @include selectOptionMaxCharacters(40ch); + } + } + } + } + } + } +} + +/* +* Select Option for Select Appearance Fill and Outline Sizes +*/ +@mixin selectOptionFillAndOutlineSizes { + .pi-select { + // Default (Column) + &.pi-select__appearance--fill, + &.pi-select__appearance--outline { + // Select Type: Dropown || Menu Type: Default (Column) + &.pi-select__type--dropdown, + &.pi-select__menu-type--column { + /* + ** Picked Option Type Contitions the Select Option Sizes + ** Size SMALL does not exists in the Piked Option Type Button cases + ** + */ + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + :host { + // Size: + // Small + &.pi-select-option__small { + @include selectOptionFontSizes(x-small, x-small, x-small); + @include selectOptionMaxCharacters(30ch); + } + + // Small, Medium & Large + &.pi-select-option__small, + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionHeight(var(--pi-spacing-xxx-large)); + @include selectOptionSpacings(var(--pi-spacing-medium), var(--pi-spacing-x-small)); + @include selectOptionBorderRadius(0.875rem); + } + // Medium & Large + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionFontSizes(small, small, small); + } + } + } + // Input || (Default && (Select Appearance === Fill || Outline)) + &:not(.pi-select__picked-option-type--button) { + :host { + // Flex Direction: + // Row + &.pi-select-option__flex-direction--row { + &:not(.pi-select-option__type--flex-wrap) { + // Size: + // Small + &.pi-select-option__small, + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionHeight(var(--pi-spacing-xx-large)); + @include selectOptionSpacings( + var(--pi-spacing-x-small), + var(--pi-spacing-xxx-small) + ); + @include selectOptionBorderRadius(var(--pi-border-radius-small)); + @include selectOptionFontSizes(x-small, x-small, x-small); + } + } + } + } + } + } + + // Select Type: Default + &:not(.pi-select__type--dropdown) { + // && Menu Type: Flex Wrap + &.pi-select__menu-type--flex-wrap { + :host { + &.pi-select-option__type--flex-wrap { + // Radio button size in Select Otions Type: Flex-Wrap + @include selectOptionRadioButtonSize(0.875rem); + @include selectOptionBorderRadius(0.875rem); + + // Row + &.pi-select-option__flex-direction--row { + &.pi-select-option__small, + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionHeight(var(--pi-spacing-xxxx-large)); + @include selectOptionSpacings( + var(--pi-spacing-medium), + var(--pi-spacing-xx-small) + ); + @include selectOptionFontSizes(x-small, x-small, x-small); + } + } + // Column + &.pi-select-option__flex-direction--column { + &.pi-select-option__small, + &.pi-select-option__medium, + &.pi-select-option__large { + @include selectOptionHeight(6.375rem); + @include selectOptionWidth(8.75rem); + @include selectOptionSpacings(var(--pi-spacing-medium), var(--pi-spacing-medium)); + @include selectOptionFontSizes(x-small, x-small, small); + } + } + } + } + } + } + } + } +} + +/* +* Select Option Flex Direction Column for Select Appearance Fill and Outline Sizes +*/ +@mixin selectOptionFillAndOutlineColumnSize { +} + +// SIDE SPACINGS + +/* +* Select Option for Select Appearance Ghost Spacings +*/ +@mixin selectOptionAppearanceGhostSpacings { +} + +/* +* Select Option Flex Direction Row for Select Appearance Fill and Outline Spacings +*/ +@mixin selectOptionFillAndOutlineRowSpacings { +} + +/* +* Select Option Flex Direction Column for Select Appearance Fill and Outline Spacings +*/ +@mixin selectOptionFillAndOutlineColumnSpacings { +} + +/* +* Select Option In Menu Type Flex Wrap Width +*/ + +@mixin selectOptionFlexWrapWidth($width: 100%) { + :host { + &.pi-select-option__type--flex-wrap { + &.pi-select-option__flex-direction--row { + width: $width; + } + } + } +} + +html { + @include selectOptionAppearanceGhostSizes; + @include selectOptionFillAndOutlineSizes; + + // Mobile + &.is-mobile { + @include selectOptionFlexWrapWidth; + } + // Tablet & Desktop + &:not(.is-mobile) { + @include selectOptionFlexWrapWidth(12rem); + } +} diff --git a/libs/ui/src/lib/ui/select/select-option/_text-align.scss b/libs/ui/src/lib/ui/select/select-option/_text-align.scss new file mode 100644 index 0000000..eef2b4a --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/_text-align.scss @@ -0,0 +1,94 @@ +/** +* Text Align Center can only be applied when +* +* A) +* +* The Menu Type === Flex Wrap. +* && The Select Option does not have an Icon +*/ + +@mixin selectOptionForMenuTypeFlexWrapTextAlign { + :host { + // Center + &.pi-select-option__text-align--center { + &.pi-select-option__type--flex-wrap { + &:not(.has-icon) { + & > span { + text-align: center; + width: -webkit-fill-available; + } + } + } + } + } +} + +/** +* B) +* +* The Select Component === Single. +* && The Menu Type === Column. +* && The Picked Option Type === Button. +*/ +@mixin selectOptionForPickedOptionTypeButtonTextAlign { + .pi-select { + // Center + &.pi-select__single { + &.pi-select__menu-type--column { + &.pi-select__picked-option-type--button { + &.pi-select__text-align--center { + :host { + &.pi-select-option__type--single { + & > div { + justify-content: center; + width: -webkit-fill-available; + } + &.has-icon { + & > div { + transform: translateX(0.6rem); + } + } + } + } + } + } + } + } + } +} +/** +* C) +* +* The Select Component === Single. +* && The Menu Type === Column. +*/ +@mixin selectOptionMenuTypeColumnTextAlign { + .pi-select { + // Center + &.pi-select__single { + &.pi-select__menu-type--column { + &.pi-select__text-align--center { + :host { + &.pi-select-option__type--single { + & > div { + justify-content: center; + width: -webkit-fill-available; + } + &.has-icon { + & > div { + transform: translateX(0.6rem); + } + } + } + } + } + } + } + } +} + +html { + @include selectOptionForMenuTypeFlexWrapTextAlign; + @include selectOptionForPickedOptionTypeButtonTextAlign; + @include selectOptionMenuTypeColumnTextAlign; +} diff --git a/libs/ui/src/lib/ui/select/select-option/_type.scss b/libs/ui/src/lib/ui/select/select-option/_type.scss new file mode 100644 index 0000000..104effc --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/_type.scss @@ -0,0 +1,262 @@ +// TYPE +/** +* The Select Option Type depends on the select, select multiple and date-picker components Appearance +*/ + +/** +* Select Option Single & Multiple for Select Appearance Ghost cases +* +*/ +@mixin selectOptionForSelectAppearanceGhost { + // Ghost + &.pi-select__appearance--ghost { + :host { + display: flex; + // Select option: + // Single || Multiple + &.pi-select-option__type--single, + &.pi-select-option__type--multiple { + & > div { + display: flex; + } + // Is Selected + &.is-selected { + &:not(.is-disabled) { + .pi-select-option__label { + font: { + weight: 600; + } + } + } + } + // Has description + &.has-description { + & > div { + align-items: flex-start; + & > span { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + } + } + + .pi-select-option__radio-button { + transform: translateY(0.1875rem); + } + + // Font Styles: + // Label + .pi-select-option__label { + font: { + weight: 600; + } + } + } + // NOT Has description + &:not(.has-description) { + justify-content: space-between; + align-items: center; + & > div { + align-items: center; + } + } + } + } + } +} + +/** +* Select Option Single & Multiple for Select Appearance Ghost cases +* +*/ +@mixin selectOptionForSelectAppearanceFillAndOutline { + &.pi-select__appearance--fill, + &.pi-select__appearance--outline { + :host { + // Select option type: + // Single || Multiple + &.pi-select-option__type--single, + &.pi-select-option__type--multiple { + // Structure + display: flex; + justify-content: space-between; + align-items: center; + + & > div { + display: flex; + align-items: center; + gap: var(--pi-spacing-xx-small); + width: 100%; + text-wrap: nowrap; + & > span { + display: flex; + align-items: center; + line-height: 1; + } + } + + // State + // Is selected + &.is-selected { + &:not(.is-disabled) { + .pi-select-option__label { + font: { + weight: 600; + } + } + } + } + &:hover { + &:not(.is-disabled) { + .pi-select-option__label { + font: { + weight: 400; + } + } + } + } + .pi-select-option__initials { + font: { + weight: 600; + } + } + } + } + &.pi-select__menu-type--column { + :host { + border: 1px solid; + } + } + } + &.pi-select__appearance--outline { + &.pi-select__picked-option-type--button { + :host { + border: 1px solid; + } + } + } +} + +/** +* Select Option Single +* +*/ +@mixin selectOptionTypeSingle { + .pi-select { + // Select appearance: + // Ghost + @include selectOptionForSelectAppearanceGhost; + // Fill & Outline + // Default (Column) + @include selectOptionForSelectAppearanceFillAndOutline; + } +} + +/** +* Select Option Multiple +*/ +@mixin selectOptionTypeMultiple { + .pi-select { + // Select appearance: + // Ghost + @include selectOptionForSelectAppearanceGhost; + // Fill & Outline + // Default (Column) + @include selectOptionForSelectAppearanceFillAndOutline; + } +} + +/** +* Select Option Type Flex Wrap +* Flex Direction: Row +*/ +@mixin selectOptionTypeFlexWrapFlexDirectionRow { + display: flex; + align-items: center; + gap: var(--pi-spacing-xx-small); + white-space: pre-line; + text-align: left; + + // State: + // Hover +} + +/** +* Select Option Type Flex Wrap +* Flex Direction: Column +*/ +@mixin selectOptionTypeFlexWrapFlexDirectionColumn { + display: grid; + padding: var(--pi-spacing-small); + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr; + grid-template-areas: + 'icon radio' + 'label label'; + + .pi-select-option__label { + grid-area: label; + } + .pi-select-option__radio-button { + grid-area: radio; + justify-self: end; + } + + .pi-select-option__icon { + grid-area: icon; + } +} + +/** +* Select Option Flex Wrap +*/ +@mixin selectOptionTypeFlexWrap { + .pi-select { + &.pi-select__menu-type--flex-wrap { + :host { + // Select option type: + // Flex Wrap + &.pi-select-option__type--flex-wrap { + border: 1px solid; + .pi-select-option__icon { + height: var(--pi-spacing-xx-large); + width: var(--pi-spacing-xx-large); + min-width: var(--pi-spacing-xx-large); + padding: var(--pi-spacing-xxx-small); + border-radius: 0.6rem; + } + // Is selected + &.is-selected, + &:hover { + &:not(.is-disabled) { + .pi-select-option__label { + font: { + weight: 400; + } + } + } + } + // Select option flex direction: + // Row + &.pi-select-option__flex-direction--row { + @include selectOptionTypeFlexWrapFlexDirectionRow; + } + &.pi-select-option__flex-direction--column { + // Column + @include selectOptionTypeFlexWrapFlexDirectionColumn; + } + } + } + } + } +} + +html { + // Single + @include selectOptionTypeSingle; + // Multiple + @include selectOptionTypeMultiple; + // Flex wrap + @include selectOptionTypeFlexWrap; +} diff --git a/libs/ui/src/lib/ui/select/select-option/select-option.component.html b/libs/ui/src/lib/ui/select/select-option/select-option.component.html new file mode 100644 index 0000000..4f9948b --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/select-option.component.html @@ -0,0 +1,65 @@ + + + +
+ + + {{ label | translate }} + {{ + description | translate + }} + +
+ + + {{ initials | translate }} + +
+ + + +
+ + {{ label | translate }} +
+ {{ initials | translate }} + +
+ + + + + {{ label | translate }} + {{ initials | translate }} + + +
diff --git a/libs/ui/src/lib/ui/select/select-option/select-option.component.scss b/libs/ui/src/lib/ui/select/select-option/select-option.component.scss new file mode 100644 index 0000000..871c61d --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/select-option.component.scss @@ -0,0 +1,55 @@ +// Properties +@use 'color'; +@use 'text-align'; +@use 'size'; +@use 'type'; + +:host { + transition: var(--pi-animation); + // States: + // Hover + &:hover { + transition: var(--pi-animation); + // NOT Is Selected && + &:not(.is-disabled) { + cursor: pointer; + transition: var(--pi-animation); + .mat-icon { + transform: scale(1.025); + transition: var(--pi-animation); + } + } + &.is-disabled { + cursor: default; + } + } + &:not(:hover) { + transition: var(--pi-animation); + } + + &.is-disabled { + opacity: 0.33; + } + + .mat-icon { + display: flex; + } + + // Menu Type: + // Default (Column) + &.pi-select__menu-type--column { + :host { + .pi-select-option__label { + line-height: 1; + } + } + } + // Flex wrap + &.pi-select__menu-type--flex-wrap { + :host { + .pi-select-option__label { + line-height: 1.25; + } + } + } +} diff --git a/libs/ui/src/lib/ui/select/select-option/select-option.component.ts b/libs/ui/src/lib/ui/select/select-option/select-option.component.ts new file mode 100644 index 0000000..8db1803 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-option/select-option.component.ts @@ -0,0 +1,93 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + HostBinding, + Input, +} from '@angular/core'; +import { SelectColor, SelectOptionFlexDirection, actionIcon } from '../entities/select.model'; + +@Component({ + standalone: false, + selector: 'pi-select-option', + templateUrl: './select-option.component.html', + styleUrls: ['./select-option.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiSelectOptionComponent { + @HostBinding('class') get classes(): string { + const colorClass = this.color && `pi-color__${this.color}`; + const flexDirectionClass = + this.flexDirection && `pi-select-option__flex-direction--${this.flexDirection}`; + const hasDescriptionClass = this.description ? 'has-description' : ''; + const hasCheckboxClass = this.type === 'multiple' ? 'has-checkbox' : ''; + const hasIconClass = this.icon ? 'has-icon' : ''; + const hasRadioClass = this.hasRadio ? 'has-radio' : ''; + const justifyClass = this.textAlign && `pi-select-option__text-align--${this.textAlign}`; + const sizeClass = this.size && `pi-select-option__${this.size}`; + const typeClass = this.type && `pi-select-option__type--${this.type}`; + + return [ + 'pi-select-option', + colorClass, + flexDirectionClass, + hasCheckboxClass, + hasDescriptionClass, + hasIconClass, + hasRadioClass, + justifyClass, + sizeClass, + typeClass, + ].join(' '); + } + + // Content + @Input() id: string | undefined; + @Input() label: string | undefined; + @Input() description: string | undefined; + @Input() initials: string | undefined; + @Input() icon: string | undefined; + @Input() actionIcon: actionIcon = 'chevron-forward'; + @Input() hasRadio = false; + + // Skin + @Input() color: SelectColor = 'primary'; + @Input() flexDirection: SelectOptionFlexDirection = 'row'; + @Input() textAlign: 'left' | 'center' = 'left'; + + /** + * Small: for menuType === column && Select Appearance Fill and Outline + * Medium: for menuType === column && Select Appearance Button + * Large: for menuType === flex-wrap && Appearance Fill and Outline + */ + @Input() size: 'small' | 'medium' | 'large' = 'medium'; + + /** + * Type 'flex-wrap' is used if parent select type === flex-wrap + * Type 'single' is used if parent select type === default (column) and is a simple select + * Type 'multiple' is used if parent select type === default (column) and is a multiple select + */ + @Input() type: 'single' | 'multiple' | 'flex-wrap' = 'single'; + + private _isSelected = false; + + @Input() set isSelected(newValue: boolean) { + if (this._isSelected !== newValue) { + this._isSelected = newValue; + this.onValueChanged(); + } + } + get isSelected() { + return this._isSelected; + } + + @Input() isDisabled!: boolean; + + constructor(private readonly _cdr: ChangeDetectorRef) {} + + onValueChanged() { + // In order to update the value displayed in the component we still + // need to manually request change detection. + this._cdr.markForCheck(); + } +} diff --git a/libs/ui/src/lib/ui/select/select-period/select-period.component.html b/libs/ui/src/lib/ui/select/select-period/select-period.component.html new file mode 100644 index 0000000..ea2e430 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-period/select-period.component.html @@ -0,0 +1,167 @@ + + + + + +
+ +
+ {{ + selectedOption?.name || label | translate + }} +
+ +
+
+ + + + +
+ + +
+ + + + {{ + selectedOption?.name || placeholder | translate + }} + + + + + + +
+
+
+ + + +
+ + +
+ + + {{ + selectedOption?.name || placeholder | translate + }} + + + + + + +
+
+
+
+
+
+ + + diff --git a/libs/ui/src/lib/ui/select/select-period/select-period.component.scss b/libs/ui/src/lib/ui/select/select-period/select-period.component.scss new file mode 100644 index 0000000..359b039 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-period/select-period.component.scss @@ -0,0 +1,5 @@ +:host { + // Select Period + &.pi-select__period { + } +} diff --git a/libs/ui/src/lib/ui/select/select-period/select-period.component.ts b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts new file mode 100644 index 0000000..343fa8f --- /dev/null +++ b/libs/ui/src/lib/ui/select/select-period/select-period.component.ts @@ -0,0 +1,211 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + HostListener, + Injector, + Input, + QueryList, + Renderer2, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { merge } from 'rxjs'; +import { PiBaseSelectComponent } from '../base-select.component'; +import { PiSelectOptionComponent } from '../select-option/select-option.component'; +import _ from 'lodash'; + +@Component({ + standalone: false, + selector: 'pi-select-period', + templateUrl: './select-period.component.html', + styleUrls: ['./select-period.component.scss', '../styles/base-select.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSelectPeriodComponent, + multi: true, + }, + ], +}) +export class PiSelectPeriodComponent extends PiBaseSelectComponent implements AfterContentInit { + /** + * Binds a CSS class name to the host element. + * Represents the default class for the select component. + */ + @HostBinding('class') get classes(): string { + return [ + 'pi-select__period', + this.appearanceClass, + this.colorClass, + this.menuColorClass, + this.menuTypeClass, + this.pickedOptionTypeClass, + this.textAlignClass, + this.typeClass, + ].join(' '); + } + + /** + * Host listener for the click event on the component. + * Toggles the visibility of the select options. + */ + @HostListener('click') + onClick() { + if (!this.isMenuVisible() && !this.disabled) { + this.setIsVisible(); + } else { + this.setIsHidden(); + } + } + + /** + * Host listener for the document click event. + * Ensures that the select options are hidden if clicked outside the component. + * @param event - The mouse event object. + */ + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + if (!this.el.nativeElement.contains(event.target)) { + this.setIsHidden(); + } + } + + @Input() override type: 'dropdown'; + @Input() override menuType: 'flex-wrap'; + + /** + * Component's constructor. Initializes the component with required services. + * @param translateService - Service used for translating option names. + * @param el - Element reference of the component. + * @param injector - Injector to get instances of dependencies. + * @param _renderer - Renderer to modify native elements. + */ + constructor( + private readonly translateService: TranslateService, + protected override el: ElementRef, + protected override injector: Injector, + protected override readonly _renderer: Renderer2, + private _cdr: ChangeDetectorRef + ) { + super(el, injector, _renderer); + } + + /** + * Getter for the value of the control. + * @returns The value of the control as a string. + */ + get controlValue(): any { + return this.control.value; + } + + /** + * Getter for the selected option based on the control value. + * @returns The selected option. + */ + get selectedOption(): any { + return this.options.find((option) => _.isEqual(this.controlValue, option)); + } + + /** + * Getter for the name of the selected option. + * Uses the `translateService` to get the translated name. + * @returns The translated name of the selected option. + */ + get selectedOptionsName(): Array { + return this.translateService.instant(this.selectedOption.name); + } + + /** + * Angular lifecycle hook that is called after Angular has fully initialized + * a component's content. This method sets up reactive subscriptions to listen + * for changes in either content children or view children. + * + * The method uses the `merge` RxJS operator to combine streams of changes + * from both `childrenOptionsComponents` (content children) and + * `viewChildOptionsComponents` (view children). This ensures that whenever + * any changes occur in either of the child lists, the visual state of the + * select options is updated appropriately. + * + * Using `Promise.resolve().then(...)`, the method ensures that the + * `updateChildrenOptionsSelectedState` method is executed in the next + * microtask, after any ongoing change detection cycles are completed. + * This approach ensures consistent and correct behavior in all scenarios. + * + * Lastly, it proactively calls `updateChildrenOptionsSelectedState` to + * set the initial state of the child options based on the current value of + * `selectedOption`. + */ + ngAfterContentInit(): void { + this.control.setValue(this.selectedOption); + merge( + this.childrenOptionsComponents?.changes || [], + this.viewChildOptionsComponents?.changes || [], + this.control.valueChanges + ).subscribe(() => { + Promise.resolve().then(() => { + this.updateChildrenOptionsSelectedState(this.selectedOption); + }); + }); + } + + /** + * Handles the logic for selecting an option. + * Sets the control value to the value of the selected option and updates child states. + * @param option - The option to be selected. + */ + onSelectOption(option: any) { + this.control.setValue(option); + this.updateChildrenOptionsSelectedState(option); + } + + /** + * Updates the visual state of child options based on the selected option. + * + * This method first determines whether the child options are content projected + * from the parent or are internally defined within the component. Depending on + * the source, it then updates the visual state of each child option accordingly. + * + * @param selectedOption - The currently selected option. + */ + private updateChildrenOptionsSelectedState(selectedOption: any): void { + // Array of child option components to work with. + let childrenOptionsComponent: Array; + // Reference to the child option elements for DOM manipulation. + let childrenOptionsElementRef: QueryList; + + // Check if we have child options that are content projected from a parent component. + if (this.childrenOptionsComponents && this.childrenOptionsComponents.length > 0) { + childrenOptionsComponent = this.childrenOptionsComponents.toArray(); + childrenOptionsElementRef = this.childrenOptionsEl; + } else { + // If not, it means they're internally defined (default), so we fetch from viewChildren. + childrenOptionsComponent = this.viewChildOptionsComponents?.toArray(); + childrenOptionsElementRef = this.viewChildOptionsEl; + } + + // Update the visual state of each child option. + childrenOptionsComponent?.forEach((childOption, index) => { + const isSelected = selectedOption + ? selectedOption[this.keyIdProperty] === childOption['id'] + : false; + // If the current child option matches the selected option, highlight it. + if (isSelected) { + this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + childOption.isSelected = true; + } else { + // Otherwise, ensure it is not highlighted. + this._renderer.removeClass( + childrenOptionsElementRef.get(index)?.nativeElement, + 'is-selected' + ); + childOption.isSelected = false; + } + }); + this._cdr.markForCheck(); + } +} diff --git a/libs/ui/src/lib/ui/select/select/select.component.html b/libs/ui/src/lib/ui/select/select/select.component.html new file mode 100644 index 0000000..259393e --- /dev/null +++ b/libs/ui/src/lib/ui/select/select/select.component.html @@ -0,0 +1,141 @@ + + + + + +
+ +
+ {{ + selectedOption?.name || label | translate + }} +
+ +
+
+ + + + +
+ + +
+ + + + {{ + selectedOption?.name || placeholder | translate + }} + + + + + + +
+
+
+ + + +
+ + +
+ + + {{ + selectedOption?.name || placeholder | translate + }} + + + + + + +
+
+
+
+
+
+ + + diff --git a/libs/ui/src/lib/ui/select/select/select.component.ts b/libs/ui/src/lib/ui/select/select/select.component.ts new file mode 100644 index 0000000..f46ca34 --- /dev/null +++ b/libs/ui/src/lib/ui/select/select/select.component.ts @@ -0,0 +1,216 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + HostBinding, + HostListener, + Injector, + Input, + QueryList, + Renderer2, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TranslateService } from '@ngx-translate/core'; +import { merge } from 'rxjs'; +import { PiBaseSelectComponent } from '../base-select.component'; +import { PiSelectOptionComponent } from '../select-option/select-option.component'; +import _ from 'lodash'; + +@Component({ + standalone: false, + selector: 'pi-select', + templateUrl: './select.component.html', + styleUrls: ['../styles/base-select.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSelectComponent, + multi: true, + }, + ], +}) +export class PiSelectComponent extends PiBaseSelectComponent implements AfterContentInit { + /** + * Binds a CSS class name to the host element. + * Represents the default class for the select component. + */ + @HostBinding('class') get classes(): string { + return [ + 'pi-select__single', + this.hasRadioClass, + this.appearanceClass, + this.colorClass, + this.menuColorClass, + this.menuTypeClass, + this.pickedOptionTypeClass, + this.sizeClass, + this.textAlignClass, + this.typeClass, + this.isResponsiveClass + ].join(' '); + } + + /** + * Host listener for the click event on the component. + * Toggles the visibility of the select options. + */ + @HostListener('click') + onClick() { + if (!this.isMenuVisible() && !this.disabled) { + this.setIsVisible(); + } else { + this.setIsHidden(); + } + } + + /** + * Host listener for the document click event. + * Ensures that the select options are hidden if clicked outside the component. + * @param event - The mouse event object. + */ + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + if (!this.el.nativeElement.contains(event.target)) { + this.setIsHidden(); + } + } + + @Input() hasRadio = false; + + /** + * Component's constructor. Initializes the component with required services. + * @param translateService - Service used for translating option names. + * @param el - Element reference of the component. + * @param injector - Injector to get instances of dependencies. + * @param _renderer - Renderer to modify native elements. + */ + constructor( + private readonly translateService: TranslateService, + protected override el: ElementRef, + protected override injector: Injector, + protected override readonly _renderer: Renderer2, + private _cdr: ChangeDetectorRef + ) { + super(el, injector, _renderer); + } + + /** + * Getter for the value of the control. + * @returns The value of the control as a string. + */ + get controlValue(): any { + return this.control.value; + } + + /** + * Getter for the selected option based on the control value. + * @returns The selected option. + */ + get selectedOption(): any { + return this.options.find((option) => _.isEqual(this.controlValue, option)); + } + + /** + * Getter for the name of the selected option. + * Uses the `translateService` to get the translated name. + * @returns The translated name of the selected option. + */ + get selectedOptionsName(): Array { + return this.translateService.instant(this.selectedOption.name); + } + + /** + * Angular lifecycle hook that is called after Angular has fully initialized + * a component's content. This method sets up reactive subscriptions to listen + * for changes in either content children or view children. + * + * The method uses the `merge` RxJS operator to combine streams of changes + * from both `childrenOptionsComponents` (content children) and + * `viewChildOptionsComponents` (view children). This ensures that whenever + * any changes occur in either of the child lists, the visual state of the + * select options is updated appropriately. + * + * Using `Promise.resolve().then(...)`, the method ensures that the + * `updateChildrenOptionsSelectedState` method is executed in the next + * microtask, after any ongoing change detection cycles are completed. + * This approach ensures consistent and correct behavior in all scenarios. + * + * Lastly, it proactively calls `updateChildrenOptionsSelectedState` to + * set the initial state of the child options based on the current value of + * `selectedOption`. + */ + ngAfterContentInit(): void { + this.control.setValue(this.selectedOption); + merge( + this.childrenOptionsComponents?.changes || [], + this.viewChildOptionsComponents?.changes || [], + this.control.valueChanges + ).subscribe(() => { + Promise.resolve().then(() => { + this.updateChildrenOptionsSelectedState(this.selectedOption); + }); + }); + } + + /** + * Handles the logic for selecting an option. + * Sets the control value to the value of the selected option and updates child states. + * @param option - The option to be selected. + */ + onSelectOption(option: any) { + this.control.setValue(option); + this.updateChildrenOptionsSelectedState(option); + } + + /** + * Updates the visual state of child options based on the selected option. + * + * This method first determines whether the child options are content projected + * from the parent or are internally defined within the component. Depending on + * the source, it then updates the visual state of each child option accordingly. + * + * @param selectedOption - The currently selected option. + */ + private updateChildrenOptionsSelectedState(selectedOption: any): void { + // Array of child option components to work with. + let childrenOptionsComponent: Array; + // Reference to the child option elements for DOM manipulation. + let childrenOptionsElementRef: QueryList; + + // Check if we have child options that are content projected from a parent component. + if (this.childrenOptionsComponents && this.childrenOptionsComponents.length > 0) { + childrenOptionsComponent = this.childrenOptionsComponents.toArray(); + childrenOptionsElementRef = this.childrenOptionsEl; + } else { + // If not, it means they're internally defined (default), so we fetch from viewChildren. + childrenOptionsComponent = this.viewChildOptionsComponents?.toArray(); + childrenOptionsElementRef = this.viewChildOptionsEl; + } + + // Update the visual state of each child option. + childrenOptionsComponent?.forEach((childOption, index) => { + const isSelected = selectedOption + ? selectedOption[this.keyIdProperty] === childOption['id'] + : false; + // If the current child option matches the selected option, highlight it. + if (isSelected) { + this._renderer.addClass(childrenOptionsElementRef.get(index)?.nativeElement, 'is-selected'); + childOption.isSelected = true; + } else { // Otherwise, ensure it is not highlighted. + this._renderer.removeClass( + childrenOptionsElementRef.get(index)?.nativeElement, + 'is-selected' + ); + childOption.isSelected = false; + } + }); + this._cdr.markForCheck(); + } + + get hasRadioClass(): string | undefined { + return this.hasRadio ? 'has-radio' : ''; + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_appearance.scss b/libs/ui/src/lib/ui/select/styles/_appearance.scss new file mode 100644 index 0000000..0b99ce7 --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_appearance.scss @@ -0,0 +1,91 @@ +// Button +@mixin selectPickedOptionButton { + :host { + &.pi-select__picked-option-type--button { + .pi-select-options-menu { + border-radius: var(--pi-border-radius-medium); + & > div { + & > span { + & > div { + height: var(--pi-height-button-large); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--pi-spacing-small) 0 var(--pi-spacing-x-large); + border-radius: var(--pi-border-radius-medium); + transition: var(--pi-animation); + } + &.is-active { + & > div { + border: 1px solid; + } + } + } + } + } + } + } +} + +@mixin selectPickedOptionButtonMobile { + :host { + &.pi-select__picked-option-type--button { + .pi-select-options-menu { + position: absolute; + z-index: 99; + padding: var(--pi-spacing-x-large) var(--pi-spacing-xxxx-small) var(--pi-spacing-medium) + var(--pi-spacing-large); + } + &:not(.pi-select__multiple) { + .pi-select-options-menu { + margin-top: var(--pi-spacing-xxx-large); + } + } + &.pi-select__multiple { + .pi-select-options-menu { + margin-top: calc( + var(--pi-spacing-xxxx-large) + var(--pi-spacing-xx-small) + var(--pi-spacing-medium) + ); + } + } + } + } +} + +html { + // Button + @include selectPickedOptionButton; + + // Mobile + &.is-mobile { + // Button mobile + @include selectPickedOptionButtonMobile; + :host { + .pi-select-options-menu { + width: 100%; + } + // Appearance + // Fill & Outline + &.pi-select__appearance--fill, + &.pi-select__appearance--outline { + .pi-select-options-menu { + margin-top: var(--pi-spacing-small); + } + } + } + } + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + :host { + &.pi-select__picked-option-type--button { + .pi-select-options-menu { + min-width: max-content; + right: 0; + } + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_color.scss b/libs/ui/src/lib/ui/select/styles/_color.scss new file mode 100644 index 0000000..d0a75c9 --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_color.scss @@ -0,0 +1,251 @@ +/*** +* 1. Select Picked Option Type: Input +* +* 1.1. Color Mixin for the following cases: +* +* Select Appearance: FILL && +* Select Type: DROPDOWN && +* Select Picked Option Type: INPUT +* +**/ + +@mixin pickedOptionTypeInputAppearanceFillColor($theming-color) { + &.pi-select { + &.pi-select__picked-option-type--input { + &.pi-select__appearance--fill { + .pi-select-picked-option { + & > div { + background-color: var(--pi-color-white); + } + } + &:not(:hover) { + &:not(.is-expanded) { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-grey-100); + } + } + .pi-select-picked-option__label { + color: var(--pi-color-grey-300); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-grey-300); + } + } + } + + // Is Expanded + &.is-expanded { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-#{$theming-color}); + } + } + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon { + color: var(--pi-color-#{$theming-color}); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-#{$theming-color}); + } + } + + // Hover + &:hover { + &:not(.is-expanded):not(.is-disabled) { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-grey-200); + } + } + .pi-select-picked-option__label { + color: var(--pi-color-grey); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-grey-300); + } + } + } + } + } + } +} + +/*** +* 1. Select Picked Option Type: Input +* +* 1.2. Color Mixin for the following cases: +* +* Select Appearance: OUTLINE && +* Select Type: DROPDOWN && +* Select Picked Option Type: INPUT +* +**/ + +@mixin pickedOptionTypeInputAppearanceOutlineColor($theming-color) { + &.pi-select { + &.pi-select__picked-option-type--input { + &.pi-select__appearance--outline { + .pi-select-picked-option { + & > div { + background-color: var(--pi-color-white); + } + } + // Default + &:not(:hover) { + &:not(.is-expanded) { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-grey-200); + } + } + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-grey-200); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-grey-200); + } + } + } + + // Is Expanded + &.is-expanded { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-#{$theming-color}); + } + } + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-#{$theming-color}); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-#{$theming-color}); + } + } + + // Hover + &:hover { + &:not(.is-expanded):not(.is-disabled) { + .pi-select-picked-option { + & > div { + border-color: var(--pi-color-grey-300); + } + } + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-grey-300); + } + .pi-select-picked-option__placeholder { + color: var(--pi-color-grey-300); + } + } + } + } + } + } +} + +/*** +* 2. Select Picked Option Type: Button +* +* 2.1. Color Mixin for the following cases: +* +* Select Appearance: FILL || OUTLINE || GHOST && +* Select Type: DROPDOWN && +* Select Picked Option Type: BUTTON +* +**/ + +@mixin pickedOptionTypeButtonColor($theming-color) { + &.pi-select { + &.pi-select__picked-option-type--button { + // Select Appearance: + // Fill & Ghost + &.pi-select__appearance--fill, + &.pi-select__appearance--ghost { + .pi-select-picked-option { + border-color: transparent; + } + } + // Default + &:not(:hover) { + .pi-select-picked-option { + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-#{$theming-color}); + } + } + + // Outline + &.pi-select__appearance--outline { + .pi-select-picked-option { + border-color: var(--pi-color-#{$theming-color}-200); + } + } + } + // Is Expanded + &.is-expanded { + .pi-select-picked-option { + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-#{$theming-color}); + } + } + + // Outline + &.pi-select__appearance--outline { + .pi-select-picked-option { + border-color: var(--pi-color-#{$theming-color}); + } + } + } + // Hover + &:hover { + &:not(.is-expanded):not(.is-disabled) { + .pi-select-picked-option { + .pi-select-picked-option__label, + .pi-select-picked-option__action-icon, + .pi-select-picked-option__custom-icon { + color: var(--pi-color-#{$theming-color}); + } + } + + // Outline + &.pi-select__appearance--outline { + .pi-select-picked-option { + border-color: var(--pi-color-#{$theming-color}); + } + } + } + } + } + } +} + +:host { + // Primary + &.pi-color__primary { + @include pickedOptionTypeInputAppearanceFillColor(primary); + @include pickedOptionTypeInputAppearanceOutlineColor(primary); + @include pickedOptionTypeButtonColor(primary); + } + // Accent + &.pi-color__accent { + @include pickedOptionTypeInputAppearanceFillColor(accent); + @include pickedOptionTypeInputAppearanceOutlineColor(accent); + @include pickedOptionTypeButtonColor(accent); + } + // Highlighted + &.pi-color__highlighted { + @include pickedOptionTypeInputAppearanceFillColor(highlighted); + @include pickedOptionTypeInputAppearanceOutlineColor(highlighted); + @include pickedOptionTypeButtonColor(highlighted); + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_menu-color.scss b/libs/ui/src/lib/ui/select/styles/_menu-color.scss new file mode 100644 index 0000000..4a7bfba --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_menu-color.scss @@ -0,0 +1,22 @@ +// MENU COLOR + +@mixin selectMenuColor { + :host { + // Default (White) + &:not(.pi-select__menu-color--transparent) { + .pi-select-options-menu { + background-color: var(--pi-color-white); + } + } + // Transparent + &.pi-select__menu-color--transparent { + .pi-select-options-menu { + background-color: transparent; + } + } + } +} + +html { + @include selectMenuColor; +} diff --git a/libs/ui/src/lib/ui/select/styles/_menu-type.scss b/libs/ui/src/lib/ui/select/styles/_menu-type.scss new file mode 100644 index 0000000..a066762 --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_menu-type.scss @@ -0,0 +1,110 @@ +// MENU TYPE + +// Default (COLUMN) +/** +* Display: flex + flex-direction: column. +* +* Default (Select options displayed in flex column direction menu is the expected common select menu layout) +*/ +@mixin selectMenuTypeDefault { + :host { + &:not(.pi-select__menu-type--flex-wrap):not(.pi-select__menu-type--custom-grid) { + // Options menu + .pi-select-options-menu { + // Options + & > div { + gap: var(--pi-spacing-xx-small); + } + } + + // Options menu + .pi-select-options-menu { + // Options + & > div { + flex-direction: column; + } + } + + // Ghost + &.pi-select__appearance--ghost { + // NOT Type: dropdown. Just in default select cases. + &:not(.pi-select__type--dropdown) { + .pi-select-options-menu { + padding: var(--pi-spacing-small); + } + } + } + } + } +} + +// Flex Wrap (ROW + WRAP) +/** +* Display: flex + flex-direction: row + flex-wrap: wrap. +* +* Default (Select options displayed in flex column direction is the expected common select menu layout) +* +* Use cases +* +* On Type Default: +* +* The menu type flex wrap is used: +* +* A) when the select option emulates a button with a max width property in screens type private + form in large devices usually for multiple selections with quite a few options. + It's always be used in selects type default (non emergent). +* +* B) In the date-picker with monthly frequency in order to allow to see + all the months in the options menu without scrolling. + It's always be used in date-pickers type dropdown or hub. +*/ + +@mixin selectMenuTypeFlexWrap($flex-direction: column, $flex-wrap: nowrap) { + :host { + &.pi-select__menu-type--flex-wrap { + // Options menu + .pi-select-options-menu { + // Options + & > div { + flex-direction: $flex-direction; + flex-wrap: $flex-wrap; + gap: var(--pi-spacing-x-small); + } + } + } + } +} +// Custom Grid +@mixin seletcMenuTypeCustomGrid { + :host { + &:not(.pi-select__menu-type--custom-grid) { + // Options menu + .pi-select-options-menu { + // Options + & > div { + display: flex; + } + } + } + } +} + +html { + // Default + @include selectMenuTypeDefault; + + // Custom Grid + @include seletcMenuTypeCustomGrid; + + // Mobile + &.is-mobile { + // Flex wrap + @include selectMenuTypeFlexWrap; + } + + // Tablet & Desktop + &:not(.is-mobile) { + // Flex wrap + @include selectMenuTypeFlexWrap(row, wrap); + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_picked-option-type.scss b/libs/ui/src/lib/ui/select/styles/_picked-option-type.scss new file mode 100644 index 0000000..0d8702e --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_picked-option-type.scss @@ -0,0 +1,287 @@ +/** +* Picked Option Type: Button +*/ +@mixin pickedOptionTypeButton { + &.pi-select__picked-option-type--button { + .pi-select-picked-option { + padding-left: var(--pi-spacing-medium); + padding-right: var(--pi-spacing-medium); + border: 1px solid; + border-radius: var(--pi-border-radius-large); + + & > div { + align-items: center; + gap: var(--pi-font-size-xx-small); + } + + transition: var(--pi-animation-fast); + & > * { + transition: var(--pi-animation-fast); + & > * { + transition: var(--pi-animation-fast); + } + } + } + // Options Menu + .pi-select-options-menu { + display: flex; + flex-direction: column; + } + // Options Menu Picked Option + .pi-select-options-menu__picked-option { + min-height: var(--pi-spacing-xxx-large); + padding-left: calc(var(--pi-spacing-xxx-small) + var(--pi-spacing-xxxxx-small)); + padding-right: var(--pi-spacing-medium); + } + // Options Menu Picked Option Placeholder + .pi-select-options-menu-picked-option__placeholder { + display: flex; + align-items: center; + line-height: 1; + color: var(--pi-color-grey); + } + // State: + // Not Hover + &:not(:hover) { + .pi-select-picked-option { + border: 1px solid; + border-color: var(--pi-color-white); + } + } + + // Hover || Expanded + &:hover, + &.is-expanded { + .pi-select-picked-option__custom-icon { + transform: scale(1.1); + } + } + // Hover + &:hover { + .pi-select-picked-option__action-icon { + transform: translateX(var(--pi-spacing-xxxx-small)) rotate(90deg); + } + } + + // Text Align: + // Center + // Select Single + &.pi-select__single { + &.pi-select__text-align--center { + // Options Menu Picked Option + .pi-select-options-menu__picked-option { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--pi-spacing-xx-small); + } + } + } + + // Select Multiple + &.pi-select__multiple, + &:not(.pi-select__text-align--center) { + // Options Menu Picked Option + .pi-select-options-menu__picked-option { + display: grid; + grid-template-columns: auto auto 1fr; + align-content: center; + grid-column-gap: var(--pi-spacing-xx-small); + + & > *:last-child { + justify-self: end; + } + } + } + + .pi-select-options-menu-picked-option__action-icon { + transition: var(--pi-animation-fast); + &:hover { + cursor: pointer; + } + &.pi-select-options-menu-picked-option__action-icon--chevron { + transform: rotate(-90deg); + + &:hover { + transform: rotate(-90deg) translateX(var(--pi-spacing-xxxx-small)) scale(1.2); + } + } + &:not(.pi-select-options-menu-picked-option__action-icon--chevron) { + &:hover { + transform: scale(1.2); + } + } + } + // Appearance: + // Fill & Outline Appearance + @include pickedOptionTypeButtonAppearanceFillAndOutline; + // Ghost Appearance + @include pickedOptionTypeButtonAppearanceGhost; + } +} + +/** +* Appearance: +* Fill & Outline Appearance +*/ +@mixin pickedOptionTypeButtonAppearanceFillAndOutline { + &.pi-select__appearance--fill, + &.pi-select__appearance--outline { + .pi-select-picked-option { + & > div { + height: var(--pi-spacing-xxxx-large); + } + } + } +} +/** +* Appearance: +* Ghost Appearance +*/ +@mixin pickedOptionTypeButtonAppearanceGhost { + &.pi-select__appearance--ghost { + .pi-select-picked-option { + display: flex; + justify-content: flex-end; + gap: var(--pi-spacing-xxx-small); + & > div { + width: fit-content; + } + } + } +} + +/** +* Picked Option Type: Button +* +* Devices: Mobile & Tabled S +*/ +@mixin pickedOptionTypeButtonSmallDevice { + :host { + &.pi-select__picked-option-type--button { + &.is-responsive { + .pi-select-picked-option__label { + display: none; + } + } + } + } +} + +/** +* Picked Option Type: Input +*/ +@mixin pickedOptionTypeInput { + &.pi-select__picked-option-type--input { + .pi-select-picked-option { + justify-content: space-between; + font: { + size: var(--pi-font-size-small); + } + & > div { + justify-content: space-between; + } + + .pi-select-picked-option__label { + font: { + size: var(--pi-font-size-x-small); + } + } + + .pi-select-picked-option__placeholder { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + // Position + // Expanded + &.is-expanded { + .pi-select-picked-option__action-icon { + transform: rotate(90deg); + } + } + + // Status + // Disabled + &.is-disabled { + .pi-select-picked-option__action-icon { + display: none; + } + } + + // Appearance + // Outline + &.pi-select__appearance--outline { + .pi-select-picked-option { + flex-direction: column; + align-items: flex-start; + .pi-select-picked-option__label { + margin-bottom: var(--pi-spacing-xxx-small); + padding-left: var(--pi-spacing-x-small); + padding-right: var(--pi-spacing-x-small); + font: { + size: var(--pi-font-size-x-small); + } + } + + & > div { + padding: var(--pi-spacing-x-small); + border: 1px solid; + border-radius: var(--pi-border-radius-small); + & > span { + align-items: center; + } + } + } + } + // Fill + &.pi-select__appearance--fill { + .pi-select-picked-option { + align-items: flex-end; + gap: var(--pi-spacing-medium); + & > div { + border-bottom-style: solid; + border-bottom-width: 1px; + & > span { + display: flex; + flex-direction: column; + gap: 0.275rem; + height: calc(var(--pi-spacing-xxxx-large) + 0.375rem); + justify-content: flex-end; + transform: translateY(-0.125rem); + } + } + } + } + } +} + +:host { + // Picked option + .pi-select-picked-option { + display: flex; + & > div { + display: flex; + align-items: center; + width: -webkit-fill-available; + transition: var(--pi-animation); + + & > .pi-icon { + transition: var(--pi-animation); + } + } + } + // Picked Option Type: + // Button + @include pickedOptionTypeButton; + // Input + @include pickedOptionTypeInput; +} + +html { + // Mobile & Tablet + &:not(.is-desktop) { + @include pickedOptionTypeButtonSmallDevice; + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_size.scss b/libs/ui/src/lib/ui/select/styles/_size.scss new file mode 100644 index 0000000..95c057b --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_size.scss @@ -0,0 +1,67 @@ +/* +* Select Font Sizes +*/ +@mixin selectSize($min-width) { + min-width: $min-width; +} + +/* +* Select Font Sizes +*/ +@mixin selectFontSize($font-size) { + .pi-select-picked-option__label, + .pi-select-options-menu-picked-option__placeholder { + font: { + size: var(--pi-font-size-#{$font-size}); + } + } +} + +/* +* Select Option Max Width ch +*/ +@mixin selectMaxCharacters($max-width) { + .pi-select-picked-option__label, + .pi-select-options-menu-picked-option__placeholder { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: $max-width; + } +} + +:host { + // The selec size property only applies to the Picked Option Type Button + &.pi-select__picked-option-type--button { + // Small + &.pi-select__small { + @include selectFontSize(x-small); + @include selectMaxCharacters(30ch); + } + + // Medium + &.pi-select__medium { + @include selectMaxCharacters(40ch); + } + } +} + +html { + &.is-mobile { + :host { + &.pi-select__picked-option-type--button { + &:not(.is-responsive) { + @include selectSize(16rem); + } + } + } + } + + &:not(.is-mobile) { + :host { + &.pi-select__picked-option-type--button { + @include selectSize(16rem); + } + } + } +} diff --git a/libs/ui/src/lib/ui/select/styles/_type.scss b/libs/ui/src/lib/ui/select/styles/_type.scss new file mode 100644 index 0000000..1c46671 --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/_type.scss @@ -0,0 +1,235 @@ +// TYPE +/** +* 1. Default +* 2. Dropdown (Large device only) +* Below the main mixins, there are also all style properties to display the options menu in the right +* distance and position respect to the picked option in any Select Appearance and Picked Option Type combination. +* +* 3. Hub (Small device only) +*/ + +// 1. DEFAULT +/** +* Position static: +* Options menu positioned according to the normal flow of the page. +*/ +@mixin selectTypeDefault { + :host { + &:not(.pi-select__type--dropdown) { + .pi-select-options-menu__options { + width: -webkit-fill-available; + } + } + } +} + +// 2. DROPDOWN (Large device) +/** +* Position absolute: +* Options menu positioned relative to the nearest positioned ancestor +* (instead of positioned relative to the viewport, like fixed) +* >> In this case, nearest to the Picked Option. +*/ +@mixin selectTypeDropdown { + :host { + &.pi-select__type--dropdown { + // State + // Collapsed + &:not(.is-expanded), + &.is-disabled { + .pi-select-options-menu { + display: none; + } + } + // Expanded + &.is-expanded { + .pi-select-options-menu { + display: flex; + } + } + // Options menu + .pi-select-options-menu { + position: absolute; + z-index: 99; + top: var(--pi-spacing-xxxx-small); + padding: var(--pi-spacing-medium) var(--pi-spacing-xxxx-small) var(--pi-spacing-medium) + var(--pi-spacing-medium); + border: 1px solid; + border-color: var(--pi-color-white-900); + box-shadow: var(--pi-box-shadow-panel); + + // Options Menu > Options + .pi-select-options-menu__options { + gap: var(--pi-spacing-xx-small); + overflow-y: scroll; + overflow-x: hidden; + + &::-webkit-scrollbar { + width: 0.1875rem; + } + &::-webkit-scrollbar-thumb { + background-color: var(--pi-color-grey-25); + + &:hover { + background-color: var(--pi-color-grey-75); + } + } + } + } + + // Picked Option Type: + // Input + &.pi-select__picked-option-type--input { + // Options Menu > Options + .pi-select-options-menu__options { + width: calc(100% - var(--pi-spacing-xxxxx-small)); + } + // Appearance: + // Fill + &.pi-select__appearance--fill { + @include optionsMenuDropdownOuterSizesAndSpacings( + 14.5rem, + calc(100% - var(--pi-spacing-xx-large)), + calc(var(--pi-spacing-xxxx-large) + var(--pi-spacing-xx-small)), + var(--pi-spacing-xxxx-large) + ); + @include optionsMenuDropdownInnerSizesAndSpacings( + var(--pi-spacing-xx-small), + var(--pi-border-radius-medium) + ); + } + // Outline || Ghost + &.pi-select__appearance--outline, + &.pi-select__appearance--ghost { + @include optionsMenuDropdownOuterSizesAndSpacings( + 16.75rem, + 100%, + calc( + var(--pi-spacing-xxxx-large) + var(--pi-spacing-small) + var(--pi-spacing-xxxxx-small) + ), + unset + ); + @include optionsMenuDropdownInnerSizesAndSpacings( + var(--pi-spacing-xx-small), + var(--pi-border-radius-small) + ); + } + } + + // Picked Option Type: + // Button + &.pi-select__picked-option-type--button { + @include optionsMenuDropdownOuterSizesAndSpacings(19.5rem, 100%, -0.5rem, unset); + @include optionsMenuDropdownInnerSizesAndSpacings( + var(--pi-spacing-x-small), + var(--pi-border-radius-large) + ); + + // Options Menu > Options + .pi-select-options-menu__options { + width: calc(100% - var(--pi-spacing-xxxxx-small)); + } + } + } + } +} +// 3. HUB (Small device) +/** +* Position fixed: +* Options menu positioned relative to the viewport, +* which means it always stays in the same place even if the page is scrolled. +* >> In this case, placed Fixed covering the navbar. +*/ + +@mixin selectTypeHub { + :host { + &.pi-select__type--dropdown { + } + } + /** + * TODO: + * Add the hub styles when the scss classes is-hub-visible / is-hub-hidden + * are going to be binded to the DOM tree through a hub state + */ +} + +// SPECIFIC PROPERTIES MIXINS +/** +* Select Type Dropdown Sizes & Outer Spacings +* +* This mixin manages the options menu: +* +* 1. Max height of the options menu box. +* Notice that the max height increases 50px when the select is multiple, +* because we're adding the extra options menu actions bar height. +* 2. Width of the options menu box. +* 3. Margin Top between the picked option and options menu box. +*/ +@mixin optionsMenuDropdownOuterSizesAndSpacings($max-height, $width, $margin-top, $margin-left) { + .pi-select-options-menu { + width: $width; + margin-top: $margin-top; + margin-left: $margin-left; + } + // Select Single + &:not(.pi-select__multiple) { + .pi-select-options-menu { + max-height: $max-height; + } + } + // Select Multiple + &.pi-select__multiple { + .pi-select-options-menu { + max-height: calc(#{$max-height} + 3.125rem); + } + } +} + +/** +* Select Type Dropdown Inner Sizes and Spacings +* +* This mixin manages the options menu: +* +* 1. Inner Padding Right from the scroll. +* 2. Options menu border radius. +*/ +@mixin optionsMenuDropdownInnerSizesAndSpacings($inner-padding-right, $border-radius) { + .pi-select-options-menu { + border-radius: $border-radius; + .pi-select-options-menu__options { + padding-right: $inner-padding-right; + } + } +} + +html { + // Type + // Default + @include selectTypeDefault; + + // MEDIA QUERIES + // Mobile & Tablet + &:not(.is-desktop) { + // Small device + @media screen and (max-width: 62rem) { + // Type + // Dropdown + /** + * TODO: + * Replace the selectTypeDropdown mixin to the selectTypeHub one + * when the scss classes is-hub-visible / is-hub-hidden + * are going to be binded to the DOM tree through a hub state + */ + @include selectTypeDropdown; + } + } + // Tablet & Desktop + &:not(.is-mobile) { + // Large device + @media screen and (min-width: 62rem) { + // Type + // Dropdown + @include selectTypeDropdown; + } + } +} diff --git a/libs/ui/src/lib/ui/select/styles/base-select.scss b/libs/ui/src/lib/ui/select/styles/base-select.scss new file mode 100644 index 0000000..8abbda9 --- /dev/null +++ b/libs/ui/src/lib/ui/select/styles/base-select.scss @@ -0,0 +1,30 @@ +// Properties +@use 'appearance'; +@use 'color'; +@use 'menu-color'; +@use 'menu-type'; +@use 'picked-option-type'; +@use 'size'; +@use 'type'; + +:host { + &.pi-select { + position: relative; + display: flex; + flex-direction: column; + width: -webkit-fill-available; + // States: + // Disabled + &.is-disabled { + // Button & Fill + &:not(.pi-select__appearance--fill) { + opacity: 0.33; + } + } + } + .pi-select-picked-option { + &:hover { + cursor: pointer; + } + } +} diff --git a/libs/ui/src/lib/ui/select/ui-select.module.ts b/libs/ui/src/lib/ui/select/ui-select.module.ts new file mode 100644 index 0000000..b7f5328 --- /dev/null +++ b/libs/ui/src/lib/ui/select/ui-select.module.ts @@ -0,0 +1,71 @@ +// Angular +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +// ngrx +import { NgxCurrencyDirective } from 'ngx-currency'; + +// Translate +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +// Shared +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUICarouselModule } from '../carousel'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITextModule } from '../text'; +import { PiSharedUITooltipModule } from '../tooltip'; +import { PiSharedUIWrapperModule } from '../wrapper'; +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiSharedUtilPipesModule } from '../../util/pipes'; +import { PiSharedUtilStatusModule } from '../../util/status'; + +//Components +import { PiSelectCarouselComponent } from './select-carousel/select-carousel.component'; +import { PiSelectComponent } from './select/select.component'; +import { PiSelectMultipleComponent } from './select-multiple/select-multiple.component'; +import { PiSelectOptionComponent } from './select-option/select-option.component'; + +// Material +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { PiSelectPeriodComponent } from './select-period/select-period.component'; + +const MATERIAL_IMPORTS = [MatIconModule, MatCheckboxModule]; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIIconModule, + PiSharedUITooltipModule, + PiSharedUIWrapperModule, + PiSharedUtilColorModule, + PiSharedUtilPipesModule, + PiSharedUtilStatusModule, + PiSharedUITextModule, + PiSharedUICarouselModule, + FormsModule, + MATERIAL_IMPORTS, + NgxCurrencyDirective, + OverlayModule, + ReactiveFormsModule, + TranslatePipe, TranslateDirective, + ], + declarations: [ + PiSelectComponent, + PiSelectMultipleComponent, + PiSelectOptionComponent, + PiSelectCarouselComponent, + PiSelectPeriodComponent, + ], + exports: [ + PiSelectComponent, + PiSelectMultipleComponent, + PiSelectOptionComponent, + PiSelectCarouselComponent, + PiSelectPeriodComponent, + ], +}) +export class PiSharedUISelectModule {} diff --git a/libs/ui/src/lib/ui/slide-toggle/index.ts b/libs/ui/src/lib/ui/slide-toggle/index.ts new file mode 100644 index 0000000..b3e827b --- /dev/null +++ b/libs/ui/src/lib/ui/slide-toggle/index.ts @@ -0,0 +1,3 @@ +export * from './ui-slide-toggle.module'; + +export * from './slide-toggle/slide-toggle.component'; diff --git a/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.html b/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.html new file mode 100644 index 0000000..d7072a2 --- /dev/null +++ b/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.html @@ -0,0 +1 @@ +

slide-toggle works!

diff --git a/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.scss b/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.ts b/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.ts new file mode 100644 index 0000000..437b86d --- /dev/null +++ b/libs/ui/src/lib/ui/slide-toggle/slide-toggle/slide-toggle.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { PiFormControl } from '../../../util/form-control'; + +@Component({ + standalone: false, + selector: 'pi-slide-toggle', + templateUrl: './slide-toggle.component.html', + styleUrls: ['./slide-toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiSlideToggleComponent extends PiFormControl {} diff --git a/libs/ui/src/lib/ui/slide-toggle/ui-slide-toggle.module.ts b/libs/ui/src/lib/ui/slide-toggle/ui-slide-toggle.module.ts new file mode 100644 index 0000000..93089d4 --- /dev/null +++ b/libs/ui/src/lib/ui/slide-toggle/ui-slide-toggle.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiSlideToggleComponent } from './slide-toggle/slide-toggle.component'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiSlideToggleComponent], + exports: [PiSlideToggleComponent], +}) +export class PiSharedUISlideToggleModule {} diff --git a/libs/ui/src/lib/ui/slider/base-slider.component.ts b/libs/ui/src/lib/ui/slider/base-slider.component.ts new file mode 100644 index 0000000..35ba50a --- /dev/null +++ b/libs/ui/src/lib/ui/slider/base-slider.component.ts @@ -0,0 +1,62 @@ +// Angular +import { + Directive, + HostBinding, + Input, + ViewChild +} from '@angular/core'; +import { FormControlDirective } from '@angular/forms'; + +// Models +import { unit } from '../../util/models'; + +@Directive() +export abstract class PiBaseSliderComponent { + @ViewChild(FormControlDirective, { static: true }) + formControlDirective: FormControlDirective; + + @HostBinding('class') + get classes(): string { + const isDisabledClass = this.isDisabled ? 'is-disabled' : ''; + const flexDirectionClass = this.flexDirection !== 'column' && `pi-slider__flex-direction--${this.flexDirection}`; + return ['pi-slider', isDisabledClass, flexDirectionClass].join(' '); + } + + // Content + @Input() label: string; + @Input() thumbLabel: boolean = true; + @Input() minValue: number = 0; + @Input() maxValue: number= 100; + @Input() unit!: unit; + @Input() step: number = 1; + + // Skin + @Input() flexDirection: 'column' | 'column-reverse' = 'column'; + + // Behaviour + @Input() isDisabled: boolean; + + + formatLabel = (value: number) => { + return `${value} ${this.unit}`; + }; + + registerOnTouched(fn: any): void { + this.formControlDirective?.valueAccessor?.registerOnTouched(fn); + } + + registerOnChange(fn: any): void { + this.formControlDirective?.valueAccessor?.registerOnChange(fn); + } + + writeValue(obj: any): void { + this.formControlDirective?.valueAccessor?.writeValue(obj); + } + + setDisabledState(isDisabled: boolean): void { + this.formControlDirective?.valueAccessor?.setDisabledState ? isDisabled : null; + } +} + + + diff --git a/libs/ui/src/lib/ui/slider/index.ts b/libs/ui/src/lib/ui/slider/index.ts new file mode 100644 index 0000000..996c79c --- /dev/null +++ b/libs/ui/src/lib/ui/slider/index.ts @@ -0,0 +1,5 @@ +export * from './ui-slider.module'; + +export * from './range-slider/range-slider.component'; + +export * from './slider/slider.component'; diff --git a/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.html b/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.html new file mode 100644 index 0000000..6247975 --- /dev/null +++ b/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.html @@ -0,0 +1,13 @@ +
+ + + + +
diff --git a/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.ts b/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.ts new file mode 100644 index 0000000..a7b1a51 --- /dev/null +++ b/libs/ui/src/lib/ui/slider/range-slider/range-slider.component.ts @@ -0,0 +1,49 @@ +// Angular +import { + ChangeDetectionStrategy, + Component, + HostBinding, + Input, + OnInit +} from '@angular/core'; +import { + FormGroup, + FormGroupDirective, + NG_VALUE_ACCESSOR +} from '@angular/forms'; + +// Component +import { PiBaseSliderComponent } from '../base-slider.component'; + +@Component({ + standalone: false, + selector: 'pi-range-slider', + templateUrl: './range-slider.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiRangeSliderComponent, + multi: true, + }, + ], +}) +export class PiRangeSliderComponent extends PiBaseSliderComponent implements OnInit { + @HostBinding('class') className = 'pi-slider__range'; + + @Input() formGroupName!: string; + + constructor(private rootFormGroup: FormGroupDirective) { + super() + } + + get form() { + return this.rootFormGroup.control.get(this.formGroupName) as FormGroup; + } + + ngOnInit(): void { + if (this.isDisabled) { + this.form.disable(); + } + } +} diff --git a/libs/ui/src/lib/ui/slider/slider/slider.component.html b/libs/ui/src/lib/ui/slider/slider/slider.component.html new file mode 100644 index 0000000..fc8b895 --- /dev/null +++ b/libs/ui/src/lib/ui/slider/slider/slider.component.html @@ -0,0 +1,14 @@ + + + {{ control?.value }} {{ unit }} + + + + diff --git a/libs/ui/src/lib/ui/slider/slider/slider.component.scss b/libs/ui/src/lib/ui/slider/slider/slider.component.scss new file mode 100644 index 0000000..4861530 --- /dev/null +++ b/libs/ui/src/lib/ui/slider/slider/slider.component.scss @@ -0,0 +1,18 @@ +:host { + & > span { + display: flex; + align-items: center; + justify-content: space-between; + + & > label { + font: { + size: var(--pi-font-size-small); + } + } + & > span { + font: { + size: var(pi-font-size-small); + } + } + } +} diff --git a/libs/ui/src/lib/ui/slider/slider/slider.component.ts b/libs/ui/src/lib/ui/slider/slider/slider.component.ts new file mode 100644 index 0000000..2e5c28b --- /dev/null +++ b/libs/ui/src/lib/ui/slider/slider/slider.component.ts @@ -0,0 +1,52 @@ +// Angular +import { + ChangeDetectionStrategy, + Component, + Injector, + Input, + OnInit +} from '@angular/core'; +import { + ControlContainer, + NG_VALUE_ACCESSOR +} from '@angular/forms'; + +// Component +import { PiBaseSliderComponent } from '../base-slider.component'; + +@Component({ + standalone: false, + selector: 'pi-slider', + templateUrl: './slider.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + styleUrls: ['./slider.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: PiSliderComponent, + multi: true, + }, + ], +}) +export class PiSliderComponent extends PiBaseSliderComponent implements OnInit { + + @Input() formControlName: string | undefined; + + constructor(private injector: Injector) { + super() + } + + get control(): any { + return this.controlContainer.control?.get(this.formControlName); + } + + get controlContainer(): any { + return this.injector.get(ControlContainer); + } + + ngOnInit(): void { + if (this.isDisabled) { + this.control.disable(); + } + } +} diff --git a/libs/ui/src/lib/ui/slider/ui-slider.module.ts b/libs/ui/src/lib/ui/slider/ui-slider.module.ts new file mode 100644 index 0000000..1930a94 --- /dev/null +++ b/libs/ui/src/lib/ui/slider/ui-slider.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatSliderModule } from '@angular/material/slider'; + +// Component +import { PiRangeSliderComponent } from './range-slider/range-slider.component'; +import { PiSliderComponent } from './slider/slider.component'; +import { PiSharedUIWrapperModule } from '../wrapper'; + +@NgModule({ + imports: [CommonModule, ReactiveFormsModule, FormsModule, MatSliderModule, PiSharedUIWrapperModule], + declarations: [PiRangeSliderComponent, PiSliderComponent], + exports: [PiRangeSliderComponent, PiSliderComponent], +}) +export class PiSharedUISliderModule {} diff --git a/libs/ui/src/lib/ui/spinner/index.ts b/libs/ui/src/lib/ui/spinner/index.ts new file mode 100644 index 0000000..413253f --- /dev/null +++ b/libs/ui/src/lib/ui/spinner/index.ts @@ -0,0 +1,3 @@ +export * from './ui-loader.module'; + +export * from './spinner/spinner.component'; diff --git a/libs/ui/src/lib/ui/spinner/spinner/spinner.component.html b/libs/ui/src/lib/ui/spinner/spinner/spinner.component.html new file mode 100644 index 0000000..7622782 --- /dev/null +++ b/libs/ui/src/lib/ui/spinner/spinner/spinner.component.html @@ -0,0 +1,6 @@ + diff --git a/libs/ui/src/lib/ui/spinner/spinner/spinner.component.ts b/libs/ui/src/lib/ui/spinner/spinner/spinner.component.ts new file mode 100644 index 0000000..d424c4b --- /dev/null +++ b/libs/ui/src/lib/ui/spinner/spinner/spinner.component.ts @@ -0,0 +1,24 @@ +// Angular +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { ComponentFamilyClass } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-spinner', + templateUrl: './spinner.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiSpinnerComponent { + @HostBinding('class') get classes(): string { + const typeClass = `${ComponentFamilyClass.DomainComponent} pi-spinner pi-spinner__loader`; + const isFullScreenClass = this.isFullScreen && 'pi-spinner__loader--fullscreen'; + const colorClass = `pi-spinner__loader--${this.color}`; + return [typeClass, isFullScreenClass, colorClass].join(' '); + } + + @Input() isLoading: boolean; + @Input() diameter = 50; + @Input() strokeWidth = 5; + @Input() isFullScreen = true; + @Input() color: 'primary' | 'accent' = 'accent'; +} diff --git a/libs/ui/src/lib/ui/spinner/ui-loader.module.ts b/libs/ui/src/lib/ui/spinner/ui-loader.module.ts new file mode 100644 index 0000000..4f81217 --- /dev/null +++ b/libs/ui/src/lib/ui/spinner/ui-loader.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// Material +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +// Components +import { PiSpinnerComponent } from './spinner/spinner.component'; + +@NgModule({ + imports: [CommonModule, MatProgressSpinnerModule], + declarations: [PiSpinnerComponent], + exports: [PiSpinnerComponent], +}) +export class PiSharedUILoaderModule {} diff --git a/libs/ui/src/lib/ui/tab/entities/tab.model.ts b/libs/ui/src/lib/ui/tab/entities/tab.model.ts new file mode 100644 index 0000000..11a6a68 --- /dev/null +++ b/libs/ui/src/lib/ui/tab/entities/tab.model.ts @@ -0,0 +1,5 @@ +export type TabButton = { + label: string; + isDisabled?: boolean; + icon?: string; +} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/tab/index.ts b/libs/ui/src/lib/ui/tab/index.ts new file mode 100644 index 0000000..bbd47e7 --- /dev/null +++ b/libs/ui/src/lib/ui/tab/index.ts @@ -0,0 +1,7 @@ +export * from './ui-tab.module'; + +export * from './tab/tab.component'; + +export * from './tab.directive'; + +export * from './entities/tab.model'; diff --git a/libs/ui/src/lib/ui/tab/tab.directive.ts b/libs/ui/src/lib/ui/tab/tab.directive.ts new file mode 100644 index 0000000..c9f73df --- /dev/null +++ b/libs/ui/src/lib/ui/tab/tab.directive.ts @@ -0,0 +1,17 @@ +import { Directive, TemplateRef } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-tab-content]', +}) +export class PiTabContentDirective { + constructor(public templateRef: TemplateRef) {} +} + +@Directive({ + standalone: false, + selector: '[pi-tab-element]', +}) +export class PiTabElementDirective { + constructor(public templateRef: TemplateRef) {} +} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/tab/tab/tab.component.html b/libs/ui/src/lib/ui/tab/tab/tab.component.html new file mode 100644 index 0000000..4781264 --- /dev/null +++ b/libs/ui/src/lib/ui/tab/tab/tab.component.html @@ -0,0 +1,59 @@ +
+ +
+ + +
+ + + {{ tab.label | translate }} + +
+
+
+
+ + +
+ +
+
+
+ +
+
+ +
+ +
+
+
diff --git a/libs/ui/src/lib/ui/tab/tab/tab.component.scss b/libs/ui/src/lib/ui/tab/tab/tab.component.scss new file mode 100644 index 0000000..4af650d --- /dev/null +++ b/libs/ui/src/lib/ui/tab/tab/tab.component.scss @@ -0,0 +1,280 @@ +@mixin tabHeader( + $bg-color: var(--pi-color-highlighted-25), + $border-color: var(--pi-color-primary-75), + $padding: var(--pi-spacing-xx-small) +) { + gap: var(--pi-spacing-large); + background-color: $bg-color; + border: 1px solid $border-color; + padding: $padding; + .pi-tab__element { + width: 100%; + padding: var(--pi-spacing-x-small) 0; + border-radius: 1rem; + transition: var(--pi-animation); + &:not(.is-disabled):not(.is-active) { + &:hover { + color: var(--pi-color-primary); + background-color: var(--pi-color-primary-75); + } + } + &.is-active { + color: var(--pi-color-highlighted); + background-color: var(--pi-color-primary-75); + } + } + & > section { + &.is-overflowing { + .pi-tab__element { + flex: 1 1 auto; + } + } + } +} + +@mixin tabElementSize($word-spacing: 100vh) { + :host { + // Fill + &.pi-tab__appearance--fill { + // Header + & > *:nth-child(1) { + .pi-tab__element { + & > span { + display: table-caption; + word-spacing: $word-spacing; + transition: var(--pi-animation); + } + } + } + } + } +} + +@mixin tabJustifyContent($justify-content) { + .pi-tab__element { + justify-content: $justify-content; + } +} + +:host { + width: 100%; + display: block; + + &.hide-tabs { + // Header + & > *:nth-child(1) { + display: none; + } + } + + &:not(.hide-tabs) { + // Header + & > *:nth-child(1) { + display: flex; + align-items: center; + // Tab wrapper + & > section { + width: 100%; + display: flex; + overflow: scroll; + &::-webkit-scrollbar { + display: none; + } + &:not(.is-overflowing) { + justify-content: space-between; + } + .pi-tab__element { + display: flex; + align-items: center; + &:not(.is-disabled):not(.is-active) { + &:hover { + cursor: pointer; + } + } + & > span { + font: { + size: var(--pi-font-size-x-small); + } + } + } + } + } + + // Content + & > *:nth-child(2) { + margin-top: var(--pi-spacing-x-large); + } + + // Tab justify content elements + // Flex start + &.pi-tab__justify-content--flex-start { + @include tabJustifyContent(flex-start); + } + // Center + &.pi-tab__justify-content--center { + @include tabJustifyContent(center); + } + // Flex end + &.pi-tab__justify-content--flex-end { + @include tabJustifyContent(flex-end); + } + + // Tab appearance + // Default + &.pi-tab__appearance--default { + // Header + & > *:nth-child(1) { + @include tabHeader; + border-radius: 1rem; + } + } + + // Fill + &.pi-tab__appearance--fill { + // Header + & > *:nth-child(1) { + @include tabHeader( + var(--pi-color-primary-25), + var(--pi-color-primary-75), + var(--pi-spacing-xx-small) + ); + border-radius: 1.5rem; + .pi-tab__element { + gap: var(--pi-spacing-xx-small); + box-sizing: border-box; + border-radius: 1rem; + } + } + } + // Outline + &.pi-tab__appearance--outline { + // Header + & > *:nth-child(1) { + padding: var(--pi-spacing-xx-small); + & > section { + gap: 0; + .pi-tab__element { + width: 100%; + flex: 1 1 auto; + padding: var(--pi-spacing-small) var(--pi-spacing-xx-small); + span { + font: { + size: var(--pi-font-size-small); + } + } + &:not(.is-disabled):not(.is-active) { + border-bottom: 1px solid; + border-bottom-color: var(--pi-color-grey-200); + span { + color: var(--pi-color-grey-300); + } + } + &.is-active { + border-bottom: 1px solid; + border-bottom-color: var(--pi-color-grey-900); + span { + color: var(--pi-color-grey-900); + font: { + weight: 400; + } + } + } + } + } + } + } + } + + // Content + & > *:nth-child(2) { + display: flex; + overflow: hidden; + // Tab content + & > * { + width: 100%; + flex: 0 0 auto; + transition: var(--pi-animation); + overflow: hidden; + } + } +} + +html { + // Mobile + &.is-mobile { + :host { + // Header + & > *:nth-child(1) { + // Tab wrapper + & > section { + gap: var(--pi-spacing-x-small); + } + } + // Tab Type + // Fill + &.pi-tab__appearance--fill { + // Header + & > *:nth-child(1) { + // Tab wrapper + & > section { + padding: var(--pi-spacing-medium) var(--pi-spacing-x-large); + .pi-tab__element { + width: 5rem; + min-width: 5rem; + max-width: 5rem; + height: var(--pi-spacing-xxx-large); + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-xx-small); + } + } + } + } + } + } + + // Mobile & Tablet + &:not(.is-desktop) { + @include tabElementSize; + } + + // Tablet & Desktop + &:not(.is-mobile) { + :host { + height: 100%; + // Header + & > *:nth-child(1) { + // Tab wrapper + & > section { + gap: var(--pi-spacing-x-large); + } + } + // Tab type + // Fill + &.pi-tab__appearance--fill { + // Header + & > *:nth-child(1) { + padding: var(--pi-spacing-x-small) var(--pi-spacing-x-large); + // Tab wrapper + & > section { + .pi-tab__element { + width: 5.75rem; + height: var(--pi-spacing-xxxx-large); + padding: var(--pi-spacing-xxxx-small) var(--pi-spacing-x-small); + } + } + } + } + } + } + + &.is-desktop { + // Medium & Large + &:not(.is-desktop__small) { + @include tabElementSize; + } + + // Large + &.is-desktop__large { + @include tabElementSize(unset); + } + } +} diff --git a/libs/ui/src/lib/ui/tab/tab/tab.component.ts b/libs/ui/src/lib/ui/tab/tab/tab.component.ts new file mode 100644 index 0000000..53df72c --- /dev/null +++ b/libs/ui/src/lib/ui/tab/tab/tab.component.ts @@ -0,0 +1,193 @@ +import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + EventEmitter, + HostBinding, + Input, + OnDestroy, + Output, + QueryList, + Renderer2, + ViewChild, +} from '@angular/core'; +import { TabButton } from '../entities/tab.model'; +import { PiTabContentDirective, PiTabElementDirective } from '../tab.directive'; + +@Component({ + standalone: false, + selector: 'pi-tab', + templateUrl: './tab.component.html', + styleUrls: ['./tab.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiTabComponent implements AfterViewInit, AfterViewChecked, OnDestroy { + @HostBinding('class') get classes(): string { + const appearanceClass = `pi-tab__appearance--${this.appearance}`; + const hideTabsClass = this.hideTabs ? 'hide-tabs' : ''; + const justifyClass = this.justify ? `pi-tab__justify-content--${this.justify}` : ''; + return ['pi-tab', hideTabsClass, appearanceClass, justifyClass].join(' '); + } + + @Input() tabButtons: Array; + private _defaultSelectedTab: number = 0; + @Input() + get defaultSelectedTab(): number { + return this._defaultSelectedTab; + } + set defaultSelectedTab(value: number) { + this._defaultSelectedTab = value; + this.onTabChange(value); + } + + @Input() appearance: 'default' | 'fill' | 'outline' = 'default'; + + @Input() isSmallDevice!: boolean; + + @Input() disableContentTranslation: boolean = false; + + @Input() hideTabs: boolean = false; + + // Justify content to the tab elments + @Input() justify: 'flex-start' | 'flex-end' | 'center' = 'center'; + + @Output() tabChange: EventEmitter = new EventEmitter(); + + @ContentChildren(PiTabContentDirective, { descendants: true }) + tabContentItems!: QueryList; + + @ContentChildren(PiTabElementDirective, { descendants: true }) + tabElementItems!: QueryList; + + @ViewChild('tabWrapper', { read: ElementRef, static: false }) + tabWrapperElementRef!: ElementRef; + + currentContent: number; + currentTabElement: number = 0; + + isTabWrapperScrolledToStart: boolean = true; + isTabWrapperScrolledToEnd: boolean = false; + + constructor( + private cdr: ChangeDetectorRef, + private el: ElementRef, + private renderer: Renderer2 + ) {} + + get tabContentNativeElements(): HTMLElement[] { + return Array.from(this.el.nativeElement.querySelectorAll('.pi-tab__content')); + } + + get tabElementsNativeElements(): HTMLElement[] { + return Array.from(this.el.nativeElement.querySelectorAll('.pi-tab__element')); + } + + get tabWrapperNativeElement(): HTMLElement { + return this.tabWrapperElementRef?.nativeElement; + } + + get isTabWrapperOverflowing(): boolean { + const isTabWrapperOverflowing = + this.tabWrapperNativeElement?.scrollWidth > this.tabWrapperNativeElement?.clientWidth; + if (isTabWrapperOverflowing) + this.renderer.addClass(this.tabWrapperNativeElement, 'is-overflowing'); + return isTabWrapperOverflowing; + } + + ngAfterViewInit(): void { + this.tabContentItems.changes.subscribe(() => { + this.cdr.markForCheck(); + }); + + this.onTabChange(this.defaultSelectedTab); + } + + ngAfterViewChecked(): void { + this.cdr.detectChanges(); + this.tabWrapperNativeElement?.addEventListener('scroll', this.updateTabWrapperScrollState); + } + + ngOnDestroy(): void { + this.tabWrapperNativeElement?.removeEventListener('scroll', this.updateTabWrapperScrollState); + } + + onTabChange(index: number): void { + this.tabChange.emit(index); + this.currentContent = index; + if (this.isSmallDevice) { + this.currentTabElement = index; + this.handleTabWrapperScrollOnMobile(); + } + if (!this.disableContentTranslation) this.handleTabContentDisplay(); + } + + getTabContentClass(index: number): string { + let contentClass: string = 'pi-tab pi-tab__content'; + if (index === this.currentContent) contentClass += ' ' + 'is-active'; + return contentClass; + } + + getTabElementClass(index: number, tabButton?: TabButton): string { + let contentClass: string = 'pi-tab pi-tab__element'; + if (index === this.currentContent) contentClass += ' ' + 'is-active'; + if (tabButton && tabButton.icon) contentClass += ' ' + 'has-icon'; + return contentClass; + } + + scrollTabWrapper(direction: 'next' | 'prev'): void { + if (direction === 'next') { + this.currentTabElement = this.currentTabElement + 1; + } else { + this.currentTabElement = this.currentTabElement - 1; + } + this.translateTabWrapper(); + } + + private handleTabContentDisplay(): void { + const transform: string = `translateX(-${this.currentContent * 100}%)`; + this.tabContentNativeElements.forEach((item) => { + this.renderer.setStyle(item, 'transform', `${transform}`); + }); + } + + private handleTabWrapperScrollOnMobile(): void { + if (!this.isTabWrapperOverflowing) return; + const target = this.tabElementsNativeElements[this.currentTabElement]; + const remainingSpace = this.tabWrapperNativeElement?.clientWidth - target.offsetWidth; + const spaceLeftAndRight = remainingSpace / 2; + this.tabWrapperNativeElement?.scrollTo({ + left: target.offsetLeft - spaceLeftAndRight, + behavior: 'smooth', + }); + } + + private translateTabWrapper(): void { + this.tabWrapperNativeElement?.scrollTo({ + left: this.currentTabElement * 0.75 * this.tabWrapperNativeElement?.offsetWidth, + behavior: 'smooth', + }); + } + + private updateIsTabWrapperScrolledToStart(): void { + this.isTabWrapperScrolledToStart = this.tabWrapperElementRef?.nativeElement?.scrollLeft === 0; + } + + private updateIsTabWrapperScrolledToEnd(): void { + this.isTabWrapperScrolledToEnd = + this.tabWrapperElementRef?.nativeElement?.scrollWidth - + this.tabWrapperElementRef?.nativeElement?.scrollLeft === + this.tabWrapperElementRef?.nativeElement?.clientWidth; + } + + private updateTabWrapperScrollState = (): void => { + this.updateIsTabWrapperScrolledToStart(); + + this.updateIsTabWrapperScrolledToEnd(); + + this.cdr.detectChanges(); + }; +} diff --git a/libs/ui/src/lib/ui/tab/ui-tab.module.ts b/libs/ui/src/lib/ui/tab/ui-tab.module.ts new file mode 100644 index 0000000..f4ec950 --- /dev/null +++ b/libs/ui/src/lib/ui/tab/ui-tab.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiTabComponent } from './tab/tab.component'; +import { PiTabContentDirective, PiTabElementDirective } from './tab.directive'; + +// Shared +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUtilColorModule } from '../../util/color'; +import { PiSharedUtilStatusModule } from '../../util/status'; +import { PiSharedUITextModule } from '../text'; + +// Translate +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +@NgModule({ + imports: [ + CommonModule, + PiSharedUIButtonModule, + PiSharedUIIconModule, + PiSharedUtilColorModule, + PiSharedUtilStatusModule, + PiSharedUITextModule, + TranslatePipe, TranslateDirective + ], + declarations: [ + PiTabComponent, + PiTabContentDirective, + PiTabElementDirective + ], + exports: [ + PiTabComponent, + PiTabContentDirective, + PiTabElementDirective + ], +}) +export class PiSharedUITabModule {} \ No newline at end of file diff --git a/libs/ui/src/lib/ui/text/base-text-viewer.component.ts b/libs/ui/src/lib/ui/text/base-text-viewer.component.ts new file mode 100644 index 0000000..b0c3c47 --- /dev/null +++ b/libs/ui/src/lib/ui/text/base-text-viewer.component.ts @@ -0,0 +1,137 @@ +// Angular +import { Directive, HostBinding, Input, OnInit } from '@angular/core'; + +// Style definitions +import { + alignment, + borderType, + textColor, + flexDirection, + justify, + fontWeight, + styleType, + fontStyle, + iconSize, + fontSize, + ComponentFamilyClass, +} from '../../styles'; + +@Directive() +export abstract class PiBaseTextViewerComponent implements OnInit { + @HostBinding('class') get classes(): string { + return [ + ComponentFamilyClass.Text, + this.alignmentClass, + this.borderTypeClass, + this.colorClass, + this.flexDirectionClass, + this.fontSizeClass, + this.justifyClass, + this.fontStyleClass, + this.fontWeightClass, + this.iconSizeClass, + this.isDisabledClass, + this.iconAppearanceClass, + this.iconColorClass, + this.hasIconClass, + this.hasLabelClass, + this.hasValueClass, + ].join(' '); + } + @Input() alignment: alignment = 'center'; + @Input() borderType: borderType; + @Input() color: textColor; + @Input() flexDirection: flexDirection = 'row'; + @Input() fontSize: fontSize = 's'; + @Input() justify: justify = 'flex-start'; + @Input() fontWeight: fontWeight = '300'; + @Input() fontStyle: fontStyle = 'normal'; + @Input() icon: string; + @Input() iconSize: iconSize = 'medium'; + @Input() iconColor: styleType; + @Input() iconAppearance: 'stroke' | 'fill-stroke' | 'fill' | 'default' | 'custom' = 'default'; + @Input() isDisabled = false; + + // Content + @Input() label!: string | unknown; + @Input() value!: string; + + ngOnInit(): void { + this.alignmentClass; + this.borderTypeClass; + this.colorClass; + this.flexDirectionClass; + this.fontSizeClass; + this.fontStyleClass; + this.fontWeightClass; + this.justifyClass; + this.iconSizeClass; + this.iconColorClass; + this.isDisabledClass; + this.hasLabelClass; + this.hasValueClass; + this.iconAppearanceClass; + } + + protected get alignmentClass(): string { + return this.alignment && `pi-text-viewer__align--${this.alignment}`; + } + + protected get borderTypeClass(): string | undefined { + return this.borderType && `pi-text-viewer__border--${this.borderType}`; + } + + protected get colorClass(): string | undefined { + return this.color && `pi-color__${this.color}`; + } + + protected get flexDirectionClass(): string | undefined { + return this.flexDirection && `pi-text-viewer__flex-direction--${this.flexDirection}`; + } + + protected get fontSizeClass(): string | undefined { + return this.fontSize && `pi-text-viewer__font-size--${this.fontSize}`; + } + + protected get fontWeightClass(): string | undefined { + return this.fontWeight && `pi-text-viewer__font-weight--${this.fontWeight}`; + } + + protected get justifyClass(): string | undefined { + return this.justify && `pi-text-viewer__justify--${this.justify}`; + } + + protected get fontStyleClass(): string | undefined { + return this.fontStyle && `pi-text-viewer__font-style--${this.fontStyle}`; + } + + protected get hasIconClass(): string | boolean { + return this.icon ? 'has-icon' : ''; + } + + protected get hasLabelClass(): string | boolean { + return this.label ? 'has-label' : ''; + } + + protected get hasValueClass(): string | boolean { + return this.value ? 'has-value' : ''; + } + + protected get iconSizeClass(): string | undefined { + return this.iconSize && `pi-text-viewer__icon-size--${this.iconSize}`; + } + + protected get iconColorClass(): string | undefined { + return this.iconColor + ? `pi-text-viewer__icon-color--${this.iconColor}` + : 'pi-text-viewer__icon-color--black'; + } + + protected get iconAppearanceClass(): string | undefined { + return this.iconAppearance && `pi-text-viewer__icon-appearance--${this.iconAppearance}`; + } + + protected get isDisabledClass(): string | boolean { + return this.isDisabled ? 'is-disabled' : ''; + } +} diff --git a/libs/ui/src/lib/ui/text/directives/text-color.directive.ts b/libs/ui/src/lib/ui/text/directives/text-color.directive.ts new file mode 100644 index 0000000..2c2f7b0 --- /dev/null +++ b/libs/ui/src/lib/ui/text/directives/text-color.directive.ts @@ -0,0 +1,26 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { styleType } from '../../../styles'; + +// Condition color +@Directive({ + standalone: false, + selector: '[pi-color]', +}) +export class PiTextColorDirective { + // Blue + @HostBinding('class.pi-color__primary') get isPrimary(): boolean { + return this.color === 'primary'; + } + + // Yellow + @HostBinding('class.pi-color__accent') get isAccent(): boolean { + return this.color === 'accent'; + } + + // Red + @HostBinding('class.pi-color__warn') get isWarn(): boolean { + return this.color === 'warn'; + } + + @Input() color: styleType; +} diff --git a/libs/ui/src/lib/ui/text/directives/text-group-indicator.directive.ts b/libs/ui/src/lib/ui/text/directives/text-group-indicator.directive.ts new file mode 100644 index 0000000..5bd1622 --- /dev/null +++ b/libs/ui/src/lib/ui/text/directives/text-group-indicator.directive.ts @@ -0,0 +1,11 @@ +import { Directive, HostBinding } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-text-group-indicator]', +}) +export class PiTextGroupIndicatorDirective { + @HostBinding('class') get classes(): string { + return 'pi-text-group pi-text-group__value-indicator'; + } +} diff --git a/libs/ui/src/lib/ui/text/directives/text-sizes.directive.ts b/libs/ui/src/lib/ui/text/directives/text-sizes.directive.ts new file mode 100644 index 0000000..470ce3a --- /dev/null +++ b/libs/ui/src/lib/ui/text/directives/text-sizes.directive.ts @@ -0,0 +1,113 @@ +import { Directive, HostBinding, Input } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-text-size]', +}) +export class PiTextSizeDirective { + @HostBinding('class') get classes(): string { + return this.sizeClass; + } + + @Input() size!: + | 'xx-small' + | 'x-small' + | 'small' + | 'medium' + | 'large' + | 'x-large' + | 'xx-large' + | 'xxx-large'; + + protected get sizeClass(): string { + return !!this.size && `pi-text pi-text__size--${this.size}`; + } +} + +// 0.625rem - 10px +@Directive({ + standalone: false, + selector: '[xxs]', +}) +export class PiTextXXSmallDirective { + @HostBinding('class') get classes(): string { + return 'pi-text pi-text__size--xx-small'; + } +} + +// 0.75rem - 12px +@Directive({ + standalone: false, + selector: '[xs]', +}) +export class PiTextXSmallDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--x-small'; + } +} + +// 0.9375rem - 15px +@Directive({ + standalone: false, + selector: '[s]', +}) +export class PiTextSmallDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--small'; + } +} + +// 1.125rem - 18px +@Directive({ + standalone: false, + selector: '[m]', +}) +export class PiTextMediumDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--medium'; + } +} + +// 1.375rem - 22px +@Directive({ + standalone: false, + selector: '[l]', +}) +export class PiTextLargeDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--large'; + } +} + +// 1.6875rem - 27px +@Directive({ + standalone: false, + selector: '[xl]', +}) +export class PiTextXLargeDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--x-large'; + } +} + +// 2.375rem - 38px +@Directive({ + standalone: false, + selector: '[xxl]', +}) +export class PiTextXXLargeDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--xx-large'; + } +} + +// 2.875rem - 46px +@Directive({ + standalone: false, + selector: '[xxxl]', +}) +export class PiTextXXXLargeDirective { + @HostBinding('class') get fontSize(): string { + return 'pi-text pi-text__size--xxx-large'; + } +} diff --git a/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts b/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts new file mode 100644 index 0000000..ce6f9e9 --- /dev/null +++ b/libs/ui/src/lib/ui/text/directives/text-styles.directive.ts @@ -0,0 +1,59 @@ +import { Directive, HostBinding, Input, OnChanges, SimpleChanges } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-text-centered]', +}) +export class PiTextCenteredDirective { + @HostBinding('class') get classes(): string { + return 'pi-text pi-text__centered'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-text-group]', +}) +export class PiTextGroupDirective { + @HostBinding('class') get classes(): string { + return 'pi-text pi-text__group'; + } +} +@Directive({ + standalone: false, + selector: '[pi-disabled]', +}) +export class PiDisabledDirective implements OnChanges { + @HostBinding('style.opacity') get opacity(): number { + return this.isDisabled ? 0.2 : 1; + } + @Input() isDisabled: boolean; + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { + this.isDisabled = changes['isDisabled'].currentValue; + } + } +} + +@Directive({ + standalone: false, + selector: '[pi-covered]', +}) +export class PiTextCoveredDirective { + @HostBinding('class') get classes(): string { + return this.isCovered ? 'pi-text pi-text__is-covered' : ''; + } + + @Input() isCovered: boolean; +} + +@Directive({ + standalone: false, + selector: '[pi-letter-spacing]', +}) +export class PiTextLetterSpacingDirective { + @HostBinding('class') get classes(): string { + return 'pi-text pi-text__letter-spacing'; + } +} diff --git a/libs/ui/src/lib/ui/text/directives/text-weights.directive.ts b/libs/ui/src/lib/ui/text/directives/text-weights.directive.ts new file mode 100644 index 0000000..b0d21d0 --- /dev/null +++ b/libs/ui/src/lib/ui/text/directives/text-weights.directive.ts @@ -0,0 +1,56 @@ +import { Directive, HostBinding } from '@angular/core'; + +// 300 +@Directive({ + standalone: false, + selector: '[light]', +}) +export class PiTextWeightLightDirective { + @HostBinding('style.font-weight') get fontWeight(): string { + return '300'; + } +} + +// 400 +@Directive({ + standalone: false, + selector: '[normal]', +}) +export class PiTextWeightNormalDirective { + @HostBinding('style.font-weight') get fontWeight(): string { + return '400'; + } +} + +// 600 +@Directive({ + standalone: false, + selector: '[medium]', +}) +export class PiTextWeightMediumDirective { + @HostBinding('style.font-weight') get fontWeight(): string { + return '600'; + } +} + +// 700 +@Directive({ + standalone: false, + selector: '[bold]', +}) +export class PiTextWeightBoldDirective { + @HostBinding('style.font-weight') get fontWeight(): string { + return '700'; + } +} + +// 800 +@Directive({ + standalone: false, + selector: '[black]', +}) +export class PiTextWeightBlackDirective { + @HostBinding('style.font-weight') get fontWeight(): string { + return '800'; + } +} diff --git a/libs/ui/src/lib/ui/text/index.ts b/libs/ui/src/lib/ui/text/index.ts new file mode 100644 index 0000000..5165467 --- /dev/null +++ b/libs/ui/src/lib/ui/text/index.ts @@ -0,0 +1,13 @@ +export * from './directives/text-color.directive'; +export * from './directives/text-group-indicator.directive'; +export * from './directives/text-sizes.directive'; +export * from './directives/text-styles.directive'; +export * from './directives/text-weights.directive'; + +export * from './text-area-expansion/text-area-expansion.component'; +export * from './text-viewer/text-viewer.component'; +export * from './text-viewer-link/text-viewer-link.component'; + +export * from './base-text-viewer.component'; + +export * from './ui-text.module'; diff --git a/libs/ui/src/lib/ui/text/styles/_alignment.scss b/libs/ui/src/lib/ui/text/styles/_alignment.scss new file mode 100644 index 0000000..453d24d --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_alignment.scss @@ -0,0 +1,18 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Alignment + &.pi-text-viewer__align--center { + align-items: center; + } + &.pi-text-viewer__align--flex-start { + align-items: flex-start; + } + &.pi-text-viewer__align--flex-end { + align-items: flex-end; + & > article { + text-align: right; + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_border-type.scss b/libs/ui/src/lib/ui/text/styles/_border-type.scss new file mode 100644 index 0000000..1d7a075 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_border-type.scss @@ -0,0 +1,66 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Border type + &.pi-text-viewer__border--top { + & > article { + padding-top: var(--pi-spacing-xxx-small); + border-top: 1px solid var(--pi-color-grey-25); + } + } + &.pi-text-viewer__border--middle { + &.pi-text-viewer__flex-direction--row { + & > article { + label { + width: 50%; + padding-right: var(--pi-spacing-xxx-small); + border-right: 1px solid var(--pi-color-grey-25); + } + span { + padding-left: var(--pi-spacing-xxx-small); + } + } + } + &.pi-text-viewer__flex-direction--row-reverse { + & > article { + label { + padding-left: var(--pi-spacing-xxx-small); + } + span { + width: 50%; + padding-right: var(--pi-spacing-xxx-small); + border-right: 1px solid var(--pi-color-grey-25); + } + } + } + &.pi-text-viewer__flex-direction--column { + & > article { + label { + padding-bottom: var(--pi-spacing-xxx-small); + border-bottom: 1px solid var(--pi-color-grey-25); + } + span { + padding-top: var(--pi-spacing-xxx-small); + } + } + } + &.pi-text-viewer__flex-direction--column-reverse { + & > article { + label { + padding-top: var(--pi-spacing-xxx-small); + } + span { + padding: var(--pi-spacing-xxx-small) var(--pi-spacing-medium); + border-bottom: 1px solid var(--pi-color-grey-25); + } + } + } + } + &.pi-text-viewer__border--bottom { + & > article { + padding-bottom: var(--pi-spacing-xxx-small); + border-bottom: 1px solid var(--pi-color-grey-25); + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_flex-direction.scss b/libs/ui/src/lib/ui/text/styles/_flex-direction.scss new file mode 100644 index 0000000..beef262 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_flex-direction.scss @@ -0,0 +1,86 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + &.has-icon { + & > article { + display: flex; + } + } + + &.pi-text-viewer__flex-direction--row, + &.pi-text-viewer__flex-direction--row-reverse { + & > article { + gap: var(--pi-spacing-xxx-small); + align-items: baseline; + } + // &.has-icon { + // &.has-label { + // &.has-value { + // & > article { + // display: flow; + // } + // } + // } + // } + } + + &.pi-text-viewer__flex-direction--column, + &.pi-text-viewer__flex-direction--column-reverse { + & > article { + display: flex; + } + } + + &.pi-text-viewer__flex-direction--column { + & > article { + flex-direction: column; + } + } + &.pi-text-viewer__flex-direction--column-reverse { + & > article { + flex-direction: column-reverse; + } + } + + &.pi-text-viewer__flex-direction--row-reverse { + flex-direction: row-reverse; + & > article { + gap: var(--pi-spacing-xxxx-small); + } + // Row reverse without icon + &:not(.has-icon) { + & > article { + align-items: center; + } + } + // Row reverse with icon + &.has-icon { + &.has-label { + &.has-value { + & > article { + flex-direction: column; + } + } + } + } + + &.pi-text-viewer__justify--space-around, + &.pi-text-viewer__justify--space-betweeen { + & > article { + align-items: flex-start; + } + } + } + // All flex direction row without justify content space... + &.pi-text-viewer__flex-direction--row-reverse { + &:not(.pi-text-viewer__justify--space-between):not( + .pi-text-viewer__justify--space-around + ):not(.pi-text-viewer__justify--center) { + max-width: max-content; + } + &.pi-text-viewer__justify--center { + max-width: -webkit-fill-available; + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_font-color.scss b/libs/ui/src/lib/ui/text/styles/_font-color.scss new file mode 100644 index 0000000..e0cf57c --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_font-color.scss @@ -0,0 +1,88 @@ +// Equal color +@mixin textViewerEqualFontColor($color) { + label, + span { + color: var(--pi-color-#{$color}); + } +} + +// Uniqual colors +@mixin textViewerUnequalFontColor($label-color, $value-color) { + label { + color: var(--pi-color-#{$label-color}); + } + span { + color: var(--pi-color-#{$value-color}); + } +} + +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Colors + // Equal combinations + &.pi-color__primary { + @include textViewerEqualFontColor(primary); + } + + &.pi-color__accent { + @include textViewerEqualFontColor(accent); + } + + &.pi-color__warn { + @include textViewerEqualFontColor(warn); + } + + &.pi-color__dark { + @include textViewerEqualFontColor(grey); + } + + &.pi-color__black { + @include textViewerEqualFontColor(grey-900); + } + + &.pi-color__white { + @include textViewerEqualFontColor(white); + } + + &.pi-color__grey { + @include textViewerEqualFontColor(grey-300); + } + + &.pi-color__highlighted { + @include textViewerEqualFontColor(highlighted); + } + + // Unequal combinations + &.pi-color__black-primary { + @include textViewerUnequalFontColor(grey-900, primary); + } + &.pi-color__primary-black { + @include textViewerUnequalFontColor(primary, grey-900); + } + &.pi-color__black-accent { + @include textViewerUnequalFontColor(grey-900, accent); + } + &.pi-color__accent-black { + @include textViewerUnequalFontColor(accent, grey-900); + } + &.pi-color__black-grey { + @include textViewerUnequalFontColor(grey-900, grey-300); + } + &.pi-color__grey-black { + @include textViewerUnequalFontColor(grey-300, grey-900); + } + &.pi-color__black-dark { + @include textViewerUnequalFontColor(grey-900, grey-400); + } + &.pi-color__dark-black { + @include textViewerUnequalFontColor(grey-400, grey-900); + } + &.pi-color__highlighted-grey { + @include textViewerUnequalFontColor(highlighted, grey); + } + &.pi-color__grey-highlighted { + @include textViewerUnequalFontColor(grey, highlighted); + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_font-size.scss b/libs/ui/src/lib/ui/text/styles/_font-size.scss new file mode 100644 index 0000000..628b679 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_font-size.scss @@ -0,0 +1,223 @@ +// Equal sizes +@mixin textViewerEqualFontSize($font-size, $gap) { + label, + a, + span { + font: { + size: var(--pi-font-size-#{$font-size}); + } + } + &[class*='pi-text-viewer__flex-direction--column'] { + & > article { + gap: var(--pi-spacing-#{$gap}); + } + } +} + +// Uniqual sizes +@mixin textViewerUnequalFontSize($large-font-size, $small-font-size, $gap) { + label { + font: { + size: var(--pi-font-size-#{$large-font-size}); + } + } + span, + a { + font: { + size: var(--pi-font-size-#{$small-font-size}); + } + } + &[class*='pi-text-viewer__flex-direction--column'] { + & > article { + gap: var(--pi-spacing-#{$gap}); + } + } +} +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Font size + // Equal sizes + &.pi-text-viewer__font-size--xxs { + @include textViewerEqualFontSize(xx-small, xxxx-small); + } + &.pi-text-viewer__font-size--xs { + @include textViewerEqualFontSize(x-small, xxxx-small); + } + &.pi-text-viewer__font-size--s { + @include textViewerEqualFontSize(small, xxxx-small); + } + &.pi-text-viewer__font-size--m { + @include textViewerEqualFontSize(medium, xxx-small); + } + &.pi-text-viewer__font-size--l { + @include textViewerEqualFontSize(large, xx-small); + } + &.pi-text-viewer__font-size--xl { + @include textViewerEqualFontSize(x-large, x-small); + } + &.pi-text-viewer__font-size--xxl { + @include textViewerEqualFontSize(xx-large, small); + } + &.pi-text-viewer__font-size--xxxl { + @include textViewerEqualFontSize(xxx-large, medium); + } + + // Unequal sizes + // xxs-xs + &.pi-text-viewer__font-size--xxs-xs { + @include textViewerUnequalFontSize(xx-small, x-small, xxxx-small); + } + // xxs-s + &.pi-text-viewer__font-size--xxs-s { + @include textViewerUnequalFontSize(xx-small, small, xxxx-small); + } + // xs-xxs + &.pi-text-viewer__font-size--xs-xxs { + @include textViewerUnequalFontSize(x-small, xx-small, xxxx-small); + } + // s-xs + &.pi-text-viewer__font-size--s-xs { + @include textViewerUnequalFontSize(small, x-small, xxxx-small); + } + // s-xxs + &.pi-text-viewer__font-size--s-xxs { + @include textViewerUnequalFontSize(small, xx-small, xxxx-small); + } + // xs-s + &.pi-text-viewer__font-size--xs-s { + @include textViewerUnequalFontSize(x-small, small, xxxx-small); + } + // xs-m + &.pi-text-viewer__font-size--xs-m { + @include textViewerUnequalFontSize(x-small, medium, xxxx-small); + } + // xs-m + &.pi-text-viewer__font-size--xs-l { + @include textViewerUnequalFontSize(x-small, large, xxxx-small); + } + // xl-m + &.pi-text-viewer__font-size--xl-m { + @include textViewerUnequalFontSize(x-large, medium, xxx-small); + } + // m-xs + &.pi-text-viewer__font-size--m-xs { + @include textViewerUnequalFontSize(medium, x-small, xxxx-small); + } + // m-xl + &.pi-text-viewer__font-size--m-xl { + @include textViewerUnequalFontSize(medium, x-large, xx-small); + } + // m-xxl + &.pi-text-viewer__font-size--m-xxl { + @include textViewerUnequalFontSize(medium, xx-large, x-small); + } + // s-l + &.pi-text-viewer__font-size--s-l { + @include textViewerUnequalFontSize(small, large, xxxx-small); + } + // l-s + &.pi-text-viewer__font-size--l-xs { + @include textViewerUnequalFontSize(large, x-small, xxxx-small); + } + // l-s + &.pi-text-viewer__font-size--l-s { + @include textViewerUnequalFontSize(large, small, xxxx-small); + } + // xl-s + &.pi-text-viewer__font-size--xl-s { + @include textViewerUnequalFontSize(x-large, small, xxxx-small); + } + // xxl-s + &.pi-text-viewer__font-size--xxl-s { + @include textViewerUnequalFontSize(xx-large, small, xxx-small); + } + + // xxl-xs + &.pi-text-viewer__font-size--xxl-xs { + @include textViewerUnequalFontSize(xx-large, x-small, xx-small); + } + // s-xl + &.pi-text-viewer__font-size--s-xl { + @include textViewerUnequalFontSize(small, x-large, xxx-small); + } + // s-xxl + &.pi-text-viewer__font-size--s-xxl { + @include textViewerUnequalFontSize(small, xx-large, x-small); + } + // m-xxl + &.pi-text-viewer__font-size--m-xxl { + @include textViewerUnequalFontSize(medium, xx-large, small); + } + // xl-xs + &.pi-text-viewer__font-size--xl-xs { + @include textViewerUnequalFontSize(x-large, x-small, xxx-small); + } + // xs-xl + &.pi-text-viewer__font-size--xs-xl { + @include textViewerUnequalFontSize(x-small, x-large, xxx-small); + } + + // m-xxl + &.pi-text-viewer__font-size--m-xxl { + @include textViewerUnequalFontSize(medium, xx-large, medium); + } + // xxl-m + &.pi-text-viewer__font-size--xxl-m { + @include textViewerUnequalFontSize(xx-large, medium, medium); + } + + // xs-xxxl + &.pi-text-viewer__font-size--xs-xxxl { + @include textViewerUnequalFontSize(x-small, xxx-large, medium); + } + // xxxl-xs + &.pi-text-viewer__font-size--xxxl-xs { + @include textViewerUnequalFontSize(xxx-large, x-small, medium); + } + + // s-xxxl + &.pi-text-viewer__font-size--s-xxxl { + @include textViewerUnequalFontSize(small, xxx-large, medium); + } + // xxxl-s + &.pi-text-viewer__font-size--xxxl-s { + @include textViewerUnequalFontSize(xxx-large, small, medium); + } + + // m-xxxl + &.pi-text-viewer__font-size--m-xxxl { + @include textViewerUnequalFontSize(medium, xxx-large, medium); + } + // xxxl-m + &.pi-text-viewer__font-size--xxxl-m { + @include textViewerUnequalFontSize(xxx-large, medium, medium); + } + + // l-xxxl + &.pi-text-viewer__font-size--l-xxxl { + @include textViewerUnequalFontSize(large, xxx-large, medium); + } + // xxxl-l + &.pi-text-viewer__font-size--xxxl-l { + @include textViewerUnequalFontSize(xxx-large, large, medium); + } + // xl-xxxl + &.pi-text-viewer__font-size--xl-xxxl { + @include textViewerUnequalFontSize(x-large, xxx-large, medium); + } + // xxxl-xl + &.pi-text-viewer__font-size--xxxl-xl { + @include textViewerUnequalFontSize(xxx-large, x-large, medium); + } + + // xxl-xxxl + &.pi-text-viewer__font-size--xxl-xxxl { + @include textViewerUnequalFontSize(xx-large, xxx-large, medium); + } + // xxxl-xxl + &.pi-text-viewer__font-size--xxxl-xxl { + @include textViewerUnequalFontSize(xxx-large, xx-large, medium); + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_font-style.scss b/libs/ui/src/lib/ui/text/styles/_font-style.scss new file mode 100644 index 0000000..5aac458 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_font-style.scss @@ -0,0 +1,32 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Font style + &.pi-text-viewer__font-style--italic { + label, + span, + a { + font: { + style: italic; + } + } + } + + &.pi-text-viewer__font-style--normal-italic { + span, + a { + font: { + style: italic; + } + } + } + + &.pi-text-viewer__font-style--italic-normal { + label { + font: { + style: italic; + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_font-weight.scss b/libs/ui/src/lib/ui/text/styles/_font-weight.scss new file mode 100644 index 0000000..185ad22 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_font-weight.scss @@ -0,0 +1,103 @@ +// Equal weight +@mixin textViewerEqualFontWeight($weight) { + label, + span, + a { + font: { + weight: $weight; + } + } +} +// Unequal weights +@mixin textViewerUnequalFontWeight($label-weight, $value-weight) { + label { + font: { + weight: $label-weight; + } + } + span, + a { + font: { + weight: $value-weight; + } + } +} + +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Equal weights + // Light 300 + &.pi-text-viewer__font-weight--300 { + @include textViewerEqualFontWeight(300); + } + // Normal 400 + &.pi-text-viewer__font-weight--400 { + @include textViewerEqualFontWeight(400); + } + // Medium 600 + &.pi-text-viewer__font-weight--600 { + @include textViewerEqualFontWeight(600); + } + // Bold 700 + &.pi-text-viewer__font-weight--700 { + @include textViewerEqualFontWeight(700); + } + // Black 800 + &.pi-text-viewer__font-weight--800 { + @include textViewerEqualFontWeight(800); + } + + // Unequal weights + // Light-Normal 300-400 + &.pi-text-viewer__font-weight--300-400 { + @include textViewerUnequalFontWeight(300, 400); + } + // Light-Medium 300-600 + &.pi-text-viewer__font-weight--300-600 { + @include textViewerUnequalFontWeight(300, 600); + } + // Light-Medium 600-300 + &.pi-text-viewer__font-weight--600-300 { + @include textViewerUnequalFontWeight(600, 300); + } + // Normal-Light 400-300 + &.pi-text-viewer__font-weight--400-300 { + @include textViewerUnequalFontWeight(400, 300); + } + + // Light-Bold 300-700 + &.pi-text-viewer__font-weight--300-700 { + @include textViewerUnequalFontWeight(300, 700); + } + // Bold Light 700-300 + &.pi-text-viewer__font-weight--700-300 { + @include textViewerUnequalFontWeight(700, 300); + } + // Light-Black 300-800 + &.pi-text-viewer__font-weight--300-800 { + @include textViewerUnequalFontWeight(300, 800); + } + // Black Light 800-300 + &.pi-text-viewer__font-weight--800-300 { + @include textViewerUnequalFontWeight(800, 300); + } + // Normal-Bold 400-700 + &.pi-text-viewer__font-weight--400-700 { + @include textViewerUnequalFontWeight(400, 700); + } + // Bold-Normal 700-400 + &.pi-text-viewer__font-weight--700-400 { + @include textViewerUnequalFontWeight(700, 400); + } + + // Normal-Black 400-800 + &.pi-text-viewer__font-weight--400-800 { + @include textViewerUnequalFontWeight(400, 800); + } + // Black-Normal 800-400 + &.pi-text-viewer__font-weight--800-400 { + @include textViewerUnequalFontWeight(800, 400); + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_icon-appearance.scss b/libs/ui/src/lib/ui/text/styles/_icon-appearance.scss new file mode 100644 index 0000000..c9bb042 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_icon-appearance.scss @@ -0,0 +1,12 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Icon style + &.pi-text-viewer__icon-appearance--fill-stroke, + &.pi-text-viewer__icon-appearance--stroke { + .mat-icon { + border: 1px solid; + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_icon-color.scss b/libs/ui/src/lib/ui/text/styles/_icon-color.scss new file mode 100644 index 0000000..0c311b5 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_icon-color.scss @@ -0,0 +1,100 @@ +// Default icon +@mixin textViewerDefaultIconColor($color) { + & > .mat-icon { + color: var(--pi-color-#{$color}); + } +} + +// Stroke icon +@mixin textViewerFillStrokeIconColor($color) { + &.pi-text-viewer__icon-appearance--fill-stroke { + .mat-icon { + color: var(--pi-color-#{$color}); + background-color: var(--pi-color-#{$color}-25); + border-color: var(--pi-color-#{$color}-75); + } + } +} + +@mixin textViewerDefaultStrokeIconColor($color) { + &.pi-text-viewer__icon-appearance--stroke { + .mat-icon { + color: var(--pi-color-#{$color}); + border-color: var(--pi-color-#{$color}-300); + } + } +} + +// Fill icon +@mixin textViewerFillIconColor($color) { + &.pi-text-viewer__icon-appearance--fill { + .mat-icon { + color: var(--pi-color-#{$color}); + background-color: var(--pi-color-#{$color}-50); + } + } +} + +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Primary + &.pi-text-viewer__icon-color--primary { + @include textViewerDefaultIconColor(primary); + @include textViewerDefaultStrokeIconColor(primary); + @include textViewerFillStrokeIconColor(primary); + @include textViewerFillIconColor(primary); + } + // Accent + &.pi-text-viewer__icon-color--accent { + @include textViewerDefaultIconColor(accent); + @include textViewerDefaultStrokeIconColor(accent); + @include textViewerFillStrokeIconColor(accent); + @include textViewerFillIconColor(accent); + } + // Warn + &.pi-text-viewer__icon-color--warn { + @include textViewerDefaultIconColor(warn); + @include textViewerDefaultStrokeIconColor(warn); + @include textViewerFillStrokeIconColor(warn); + @include textViewerFillIconColor(warn); + } + // Highlighted + &.pi-text-viewer__icon-color--highlighted { + @include textViewerDefaultIconColor(highlighted); + @include textViewerDefaultStrokeIconColor(highlighted); + @include textViewerFillStrokeIconColor(highlighted); + @include textViewerFillIconColor(highlighted); + } + // success + &.pi-text-viewer__icon-color--success { + @include textViewerDefaultIconColor(success); + @include textViewerDefaultStrokeIconColor(success); + @include textViewerFillStrokeIconColor(success); + @include textViewerFillIconColor(success); + } + // grey + &.pi-text-viewer__icon-color--grey { + @include textViewerDefaultIconColor(grey-300); + @include textViewerDefaultStrokeIconColor(grey); + @include textViewerFillStrokeIconColor(grey); + @include textViewerFillIconColor(grey); + } + // black + &.pi-text-viewer__icon-color--black { + @include textViewerDefaultStrokeIconColor(grey-900); + @include textViewerDefaultIconColor(grey-900); + } + + // White + &.pi-text-viewer__icon-color--white { + @include textViewerDefaultIconColor(white); + &.pi-text-viewer__icon-appearance--fill-stroke { + .mat-icon { + background-color: var(--pi-color-white); + color: var(--pi-color-primary); + } + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_icon-size.scss b/libs/ui/src/lib/ui/text/styles/_icon-size.scss new file mode 100644 index 0000000..6ca69b5 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_icon-size.scss @@ -0,0 +1,70 @@ +// Default icon +@mixin textViewerIconSize($size, $gap) { + & > .mat-icon { + height: var(--pi-spacing-#{$size}); + width: var(--pi-spacing-#{$size}); + } + gap: var(--pi-spacing-#{$gap}); +} + +// Stroke icon +@mixin textViewerFillStrokeIconSize($size, $padding, $border-radius) { + &.pi-text-viewer__icon-appearance--fill, + &.pi-text-viewer__icon-appearance--fill-stroke { + .mat-icon { + height: var(--pi-spacing-#{$size}); + width: var(--pi-spacing-#{$size}); + padding: var(--pi-spacing-#{$padding}); + border-radius: var(--pi-border-radius-#{$border-radius}); + } + } +} + +@mixin textViewerDefaultStrokeIconSize($size, $padding, $border-radius) { + &.pi-text-viewer__icon-appearance--stroke { + .mat-icon { + height: var(--pi-spacing-#{$size}); + width: var(--pi-spacing-#{$size}); + padding: var(--pi-spacing-#{$padding}); + border-radius: var(--pi-border-radius-#{$border-radius}); + } + } +} + +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Icon size + &.pi-text-viewer__icon-size--x-small { + @include textViewerIconSize(x-small, xx-small); + @include textViewerDefaultStrokeIconSize(medium, xxxxx-small, medium); + .pi-text-viewer__icon-appearance--stroke { + label { + display: flex; + } + } + } + &.pi-text-viewer__icon-size--small { + @include textViewerIconSize(medium, xx-small); + } + &.pi-text-viewer__icon-size--medium { + @include textViewerIconSize(large, small); + } + &.pi-text-viewer__icon-size--large { + @include textViewerFillStrokeIconSize(xxx-large, xx-small, medium); + @include textViewerIconSize(x-large, medium); + &.pi-text-viewer__font-weight--300-700, + &.pi-text-viewer__font-weight--700-300, + &.pi-text-viewer__font-weight--300-600, + &.pi-text-viewer__font-weight--600-300 { + & > .mat-icon { + min-width: var(--pi-spacing-xxx-large); + } + } + } + &.pi-text-viewer__icon-size--x-large { + @include textViewerFillStrokeIconSize(xxxx-large, small, medium); + @include textViewerIconSize(xx-large, medium); + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/_justify.scss b/libs/ui/src/lib/ui/text/styles/_justify.scss new file mode 100644 index 0000000..7de4066 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/_justify.scss @@ -0,0 +1,43 @@ +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + // Justify + // Flex start + &.pi-text-viewer__justify--flex-start { + justify-content: flex-start; + } + // Flex end + &.pi-text-viewer__justify--flex-end { + justify-content: flex-end; + text-align: right; + } + // Center + &.pi-text-viewer__justify--center { + justify-content: center; + & > article { + text-align: center; + } + } + // Space between + &.pi-text-viewer__justify--space-between { + justify-content: space-between; + width: 100%; + & > article { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + } + // Space evenly + &.pi-text-viewer__justify--space-evenly { + justify-content: space-evenly; + width: 100%; + & > article { + display: flex; + justify-content: space-evenly; + width: 100%; + } + } + } +} diff --git a/libs/ui/src/lib/ui/text/styles/base-text-viewer.component.scss b/libs/ui/src/lib/ui/text/styles/base-text-viewer.component.scss new file mode 100644 index 0000000..0cf0093 --- /dev/null +++ b/libs/ui/src/lib/ui/text/styles/base-text-viewer.component.scss @@ -0,0 +1,35 @@ +@use 'alignment'; +@use 'border-type'; +@use 'flex-direction'; +@use 'font-color'; +@use 'font-size'; +@use 'font-style'; +@use 'font-weight'; +@use 'icon-color'; +@use 'icon-size'; +@use 'icon-appearance'; +@use 'justify'; + +// Common text viewer styles +:host { + &.pi-text-viewer, + &.pi-text-viewer-link { + display: flex; + + label { + line-height: 115%; + } + span { + line-height: 130%; + } + + & > .mat-icon { + display: flex; + flex: 0 0 auto; + } + + label { + margin-bottom: unset; + } + } +} diff --git a/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.html b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.html new file mode 100644 index 0000000..b5030d4 --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.html @@ -0,0 +1,11 @@ +
+ + {{ isLongText ? (text | slice : 0 : characterLimit) : text + }}... + +
{{ text | slice : characterLimit : text?.length }}
+
+ + diff --git a/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.scss b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.scss new file mode 100644 index 0000000..3cafccd --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.scss @@ -0,0 +1,15 @@ +:host { + span { + &:nth-child(3) { + display: inline-block; + } + } + & > article { + & > section { + display: inline; + } + } + & > button { + margin-top: var(--pi-spacing-medium); + } +} diff --git a/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.ts b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.ts new file mode 100644 index 0000000..dd57b91 --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-area-expansion/text-area-expansion.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { DURATION, transitionAnimation } from '../../../styles'; + +@Component({ + standalone: false, + selector: 'pi-text-area-expansion', + templateUrl: './text-area-expansion.component.html', + styleUrls: ['./text-area-expansion.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + animations: transitionAnimation(DURATION.Medium), +}) +export class PiTextAreaExpansionComponent { + @Input() text = ''; + @Input() characterLimit = 50; + + isCollapsed = true; + + get isLongText(): boolean { + return this.text?.length > this.characterLimit; + } + + alterToggle(): void { + this.isCollapsed = !this.isCollapsed; + } +} diff --git a/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.html b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.html new file mode 100644 index 0000000..7cc2e9a --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.html @@ -0,0 +1,25 @@ + + diff --git a/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.scss b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.scss new file mode 100644 index 0000000..7000c75 --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.scss @@ -0,0 +1,98 @@ +@mixin textViewerLinkColor($color) { + &:not(:hover) { + a { + color: var(--pi-color-#{$color}); + span, + .mat-icon { + color: var(--pi-color-#{$color}); + } + } + } + + &:hover { + a { + text-decoration: underline; + color: var(--pi-color-#{$color}-900); + span, + .mat-icon { + color: var(--pi-color-#{$color}-900); + } + } + } +} + +@mixin textViewerLinkGrey { + &:not(:hover) { + a { + color: var(--pi-color-grey-300); + span, + .mat-icon { + color: var(--pi-color-grey-300); + } + } + } + + &:hover { + a { + color: var(--pi-color-grey); + span, + .mat-icon { + color: var(--pi-color-grey); + } + } + } +} + +:host { + display: flex; + gap: var(--pi-spacing-x-small); + + &:hover { + transition: var(--pi-animation); + a, + label, + .mat-icon { + cursor: pointer; + } + a { + text-decoration: underline; + } + } + label { + margin-bottom: unset; + color: var(--pi-color-grey-900); + } + label + a { + margin-left: var(--pi-spacing-xxx-small); + } + + &:not(.pi-color__white) { + // Icon & Link Color + // Default & Primary + &:not(.pi-text-viewer-link__color--accent):not(.pi-text-viewer-link__color--highlighted):not( + .pi-text-viewer-link__color--white + ):not(.pi-text-viewer-link__color--grey) { + @include textViewerLinkColor(primary); + } + &.pi-text-viewer-link__color--primary { + @include textViewerLinkColor(primary); + } + // Accent + &.pi-text-viewer-link__color--accent { + @include textViewerLinkColor(accent); + } + // Highlighted + &.pi-text-viewer-link__color--highlighted { + @include textViewerLinkColor(highlighted); + } + } + // white + &.pi-text-viewer-link__color--white { + @include textViewerLinkColor(white); + } + + // grey + &.pi-text-viewer-link__color--grey { + @include textViewerLinkGrey; + } +} diff --git a/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.ts b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.ts new file mode 100644 index 0000000..b20c5cd --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-viewer-link/text-viewer-link.component.ts @@ -0,0 +1,52 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { Params } from '@angular/router'; +import { + alignment, + display, + flexDirection, + fontSize, + justify +} from '../../../styles'; +import { PiBaseTextViewerComponent } from '../base-text-viewer.component'; + +@Component({ + standalone: false, + selector: 'pi-text-viewer-link', + templateUrl: './text-viewer-link.component.html', + styleUrls: ['./text-viewer-link.component.scss', '../styles/base-text-viewer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiTextViewerLinkComponent extends PiBaseTextViewerComponent { + @HostBinding('class') get textViewerLinkClasses(): string { + const colorClass = this.color !== undefined && `pi-text-viewer-link__color--${this.color}`; + return ['pi-text-viewer-link', colorClass].join(' '); + } + + // Content + + @Input() link: string | undefined; + @Input() route: string | undefined; + @Input() target = '_self'; + @Input() queryParams: Params | undefined; + + @Input() override color!: 'primary' | 'accent' | 'white' | 'highlighted' | 'grey' | undefined; + + // Skin + @Input() override alignment: alignment = 'center'; + @Input() override flexDirection: flexDirection = 'row'; + @Input() override fontSize: fontSize = 's'; + @Input() override justify: justify = 'flex-start'; + @Input() display: display = 'flex'; + + template: 'link' | 'route' | 'onClick'; + + getTemplate(): 'link' | 'route' | 'onClick' { + if (this.route) { + return 'route'; + } else if (this.link) { + return 'link'; + } else { + return 'onClick'; + } + } +} diff --git a/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.html b/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.html new file mode 100644 index 0000000..1a5fe10 --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.html @@ -0,0 +1,7 @@ + +
+ + + {{ value }} + +
diff --git a/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.ts b/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.ts new file mode 100644 index 0000000..a736859 --- /dev/null +++ b/libs/ui/src/lib/ui/text/text-viewer/text-viewer.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; + +import { ComponentFamilyClass } from '../../../styles'; +import { PiBaseTextViewerComponent } from '../base-text-viewer.component'; + +@Component({ + standalone: false, + selector: 'pi-text-viewer', + templateUrl: './text-viewer.component.html', + styleUrls: ['../styles/base-text-viewer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiTextViewerComponent extends PiBaseTextViewerComponent { + @HostBinding('class') get textViewerClasses(): string { + return ['pi-text-viewer'].join(' '); + } +} diff --git a/libs/ui/src/lib/ui/text/ui-text.module.ts b/libs/ui/src/lib/ui/text/ui-text.module.ts new file mode 100644 index 0000000..0b4e1e5 --- /dev/null +++ b/libs/ui/src/lib/ui/text/ui-text.module.ts @@ -0,0 +1,107 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { PiSharedUIButtonModule } from '../button'; +import { PiSharedUIIconModule } from '../icon'; +import { PiSharedUITooltipModule } from '../tooltip'; + +import { PiTextAreaExpansionComponent } from './text-area-expansion/text-area-expansion.component'; +import { PiTextViewerLinkComponent } from './text-viewer-link/text-viewer-link.component'; +import { PiTextViewerComponent } from './text-viewer/text-viewer.component'; +import { PiTextColorDirective } from './directives/text-color.directive'; +import { PiTextGroupIndicatorDirective } from './directives/text-group-indicator.directive'; +import { + PiTextLargeDirective, + PiTextMediumDirective, + PiTextSizeDirective, + PiTextSmallDirective, + PiTextXLargeDirective, + PiTextXSmallDirective, + PiTextXXLargeDirective, + PiTextXXSmallDirective, + PiTextXXXLargeDirective, +} from './directives/text-sizes.directive'; +import { + PiDisabledDirective, + PiTextCenteredDirective, + PiTextCoveredDirective, + PiTextGroupDirective, + PiTextLetterSpacingDirective, +} from './directives/text-styles.directive'; +import { + PiTextWeightBlackDirective, + PiTextWeightBoldDirective, + PiTextWeightLightDirective, + PiTextWeightMediumDirective, + PiTextWeightNormalDirective, +} from './directives/text-weights.directive'; + +// Material +import { MatIconModule } from '@angular/material/icon'; + +const MATERIAL_IMPORTS = [MatIconModule]; + +@NgModule({ + imports: [ + CommonModule, + RouterModule, + PiSharedUIIconModule, + PiSharedUIButtonModule, + PiSharedUITooltipModule, + MATERIAL_IMPORTS, + ], + declarations: [ + PiDisabledDirective, + PiTextAreaExpansionComponent, + PiTextCenteredDirective, + PiTextColorDirective, + PiTextCoveredDirective, + PiTextGroupDirective, + PiTextGroupIndicatorDirective, + PiTextLargeDirective, + PiTextLetterSpacingDirective, + PiTextMediumDirective, + PiTextSmallDirective, + PiTextViewerComponent, + PiTextViewerLinkComponent, + PiTextWeightBlackDirective, + PiTextWeightBoldDirective, + PiTextWeightLightDirective, + PiTextWeightMediumDirective, + PiTextWeightNormalDirective, + PiTextXLargeDirective, + PiTextXSmallDirective, + PiTextXXLargeDirective, + PiTextXXSmallDirective, + PiTextXXXLargeDirective, + PiTextSizeDirective, + ], + exports: [ + PiDisabledDirective, + PiTextAreaExpansionComponent, + PiTextCenteredDirective, + PiTextColorDirective, + PiTextCoveredDirective, + PiTextGroupDirective, + PiTextGroupIndicatorDirective, + PiTextLargeDirective, + PiTextLetterSpacingDirective, + PiTextMediumDirective, + PiTextSmallDirective, + PiTextViewerComponent, + PiTextViewerLinkComponent, + PiTextWeightBlackDirective, + PiTextWeightBoldDirective, + PiTextWeightLightDirective, + PiTextWeightMediumDirective, + PiTextWeightNormalDirective, + PiTextXLargeDirective, + PiTextXSmallDirective, + PiTextXXLargeDirective, + PiTextXXSmallDirective, + PiTextXXXLargeDirective, + PiTextSizeDirective, + ], +}) +export class PiSharedUITextModule {} diff --git a/libs/ui/src/lib/ui/tooltip/index.ts b/libs/ui/src/lib/ui/tooltip/index.ts new file mode 100644 index 0000000..b047100 --- /dev/null +++ b/libs/ui/src/lib/ui/tooltip/index.ts @@ -0,0 +1,2 @@ +export * from './ui-tooltip.module'; +export * from './tooltip.directive'; \ No newline at end of file diff --git a/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts b/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts new file mode 100644 index 0000000..c428b20 --- /dev/null +++ b/libs/ui/src/lib/ui/tooltip/tooltip.directive.ts @@ -0,0 +1,62 @@ +import { + Directive, + ElementRef, + HostBinding, + Input, + OnDestroy, + OnInit, + Renderer2, +} from '@angular/core'; +import { MatTooltip, TooltipPosition } from '@angular/material/tooltip'; + +@Directive({ + standalone: false, + selector: '[pi-tooltip]', + providers: [MatTooltip], +}) +export class PiTooltipDirective implements OnInit, OnDestroy { + @HostBinding('class') className = 'pi-tooltip-dispatcher'; + + @Input() tooltipText: string | undefined; + @Input() tooltipType: 'info' | 'error' = 'info'; + @Input() tooltipColor: 'white' | 'grey' | 'dark' = 'white'; + @Input() tooltipPosition: TooltipPosition = 'right'; + + private mouseoverListener: () => void; + private mouseoutListener: () => void; + private clickListener: () => void; + + constructor(private tooltip: MatTooltip, private el: ElementRef, private renderer: Renderer2) {} + + ngOnInit(): void { + this.tooltip.tooltipClass = ['pi-tooltip', this.typeClass, this.colorClass].join(' '); + + this.mouseoverListener = this.renderer.listen(this.el.nativeElement, 'mouseover', () => { + this.tooltip.message = this.tooltipText; + this.tooltip.show(); + }); + + this.mouseoutListener = this.renderer.listen(this.el.nativeElement, 'mouseout', () => { + this.tooltip.hide(); + }); + + this.clickListener = this.renderer.listen(this.el.nativeElement, 'click', (event) => { + event.stopPropagation(); + this.tooltip.toggle(); + }); + } + + ngOnDestroy() { + this.mouseoverListener(); + this.mouseoutListener(); + this.clickListener(); + } + + get colorClass(): string | undefined { + return this.tooltipColor && `pi-tooltip__${this.tooltipColor}`; + } + + get typeClass(): string | undefined { + return this.tooltipType && `pi-tooltip__type--${this.tooltipType}`; + } +} diff --git a/libs/ui/src/lib/ui/tooltip/ui-tooltip.module.ts b/libs/ui/src/lib/ui/tooltip/ui-tooltip.module.ts new file mode 100644 index 0000000..cce974b --- /dev/null +++ b/libs/ui/src/lib/ui/tooltip/ui-tooltip.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +// Directive +import { PiTooltipDirective } from './tooltip.directive'; + +import { OverlayModule } from '@angular/cdk/overlay'; +import { MatTooltipModule } from '@angular/material/tooltip'; + + +@NgModule({ + imports: [CommonModule, OverlayModule, MatTooltipModule], + declarations: [ + PiTooltipDirective + ], + exports: [ + PiTooltipDirective + ] +}) +export class PiSharedUITooltipModule {} diff --git a/libs/ui/src/lib/ui/wrapper/action/action.wrapper.ts b/libs/ui/src/lib/ui/wrapper/action/action.wrapper.ts new file mode 100644 index 0000000..64414d3 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/action/action.wrapper.ts @@ -0,0 +1,34 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { WrapperTypeClass } from '../../../styles'; +import { PiBaseWrapper } from '../base.wrapper'; + +@Component({ + standalone: false, + selector: 'pi-action-wrapper', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiActionWrapper extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + return [ + WrapperTypeClass.Action, + this.maxWidthClass, + this.flexDirectionClass, + this.justifyClass, + this.alignmentClass, + this.hasPaddingClass, + this.gapClass, + ].join(' '); + } + @HostBinding('class.pi-wrapper__action--position-fixed') get fixedClass(): string | boolean { + return this.position === 'fixed'; + } + + @HostBinding('class.pi-wrapper__action--position-fixed-mobile') get fixedMobileClass(): + | string + | boolean { + return this.position === 'fixed-mobile'; + } + + @Input() position: 'default' | 'fixed' | 'fixed-mobile'; +} diff --git a/libs/ui/src/lib/ui/wrapper/base.wrapper.ts b/libs/ui/src/lib/ui/wrapper/base.wrapper.ts new file mode 100644 index 0000000..2693499 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/base.wrapper.ts @@ -0,0 +1,72 @@ +// Angular +import { Directive, Input, OnInit } from '@angular/core'; +import { itemsWidth } from '../../styles'; + +import { + alignment, + flexDirection, + gap, + grid, + justify, + maxWidth, +} from '../../styles'; + +@Directive() +export abstract class PiBaseWrapper implements OnInit { + @Input() alignment: alignment = 'center'; + @Input() flexDirection: flexDirection = 'row'; + @Input() gap!: gap; + @Input() grid: grid = 'half'; + @Input() hasBackground = false; + @Input() hasPadding = false; + @Input() justify: justify = 'center'; + @Input() maxWidth: maxWidth = 'edge-to-edge'; + @Input() itemsWidth: itemsWidth; + + ngOnInit(): void { + this.alignmentClass; + this.flexDirectionClass; + this.gridClass; + this.hasBackgroundClass; + this.hasPaddingClass; + this.justifyClass; + this.itemsWidthClass; + this.maxWidthClass; + } + + protected get maxWidthClass(): string | undefined { + return this.maxWidth && `pi-wrapper__${this.maxWidth}`; + } + + protected get alignmentClass(): string | undefined { + return this.alignment && `pi-wrapper__align-items--${this.alignment}`; + } + + protected get justifyClass(): string | undefined { + return this.justify && `pi-wrapper__justify--${this.justify}`; + } + + protected get gapClass(): string | undefined { + return this.gap && `pi-wrapper__gap--${this.gap}`; + } + + protected get gridClass(): string | undefined { + return this.grid !== 'none' ? `pi-wrapper__grid--${this.grid}` : 'pi-wrapper__full-grid'; + } + + protected get flexDirectionClass(): string | undefined { + return this.flexDirection && `pi-wrapper__flex-direction--${this.flexDirection}`; + } + + protected get hasBackgroundClass(): string | boolean { + return this.hasBackground ? 'pi-wrapper__has-background' : ''; + } + + protected get hasPaddingClass(): string | boolean { + return this.hasPadding ? 'pi-wrapper__has-padding' : ''; + } + + protected get itemsWidthClass(): string | undefined { + return this.itemsWidth && `pi-wrapper__items-width--${this.itemsWidth}`; + } +} diff --git a/libs/ui/src/lib/ui/wrapper/definitions.ts b/libs/ui/src/lib/ui/wrapper/definitions.ts new file mode 100644 index 0000000..dac6f63 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/definitions.ts @@ -0,0 +1,7 @@ +import { Params } from '@angular/router'; + +export type backLink = { + route: string; + label: string; + queryParams?: Params; +}; diff --git a/libs/ui/src/lib/ui/wrapper/directives/flex-wrapper.directive.ts b/libs/ui/src/lib/ui/wrapper/directives/flex-wrapper.directive.ts new file mode 100644 index 0000000..4711abb --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/directives/flex-wrapper.directive.ts @@ -0,0 +1,40 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { justify, padding } from '../../../styles'; +import { PiBaseWrapper } from '../base.wrapper'; + +// Flex +@Directive({ + standalone: false, + selector: '[pi-flex]', +}) +export class PiFlexDirective extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + const flexWrapClass = + this.flexWrap === 'wrap' ? `pi-wrapper__flex-direction--${this.flexWrap}` : ''; + return [ + 'pi-wrapper', + 'pi-wrapper__flex', + this.alignmentClass, + this.borderClass, + this.flexDirectionClass, + this.gapClass, + this.itemsWidthClass, + this.justifyClass, + this.maxWidthClass, + this.paddingClass, + flexWrapClass, + ].join(' '); + } + @Input() override justify: justify = 'flex-start'; + @Input() flexWrap: 'wrap' | 'no-wrap' = 'no-wrap'; + @Input() padding: padding = 'xl'; + @Input() border!: 'top' | 'bottom'; + + protected get borderClass(): string | undefined { + return this.border ? `pi-wrapper__border--${this.border}` : ''; + } + + protected get paddingClass(): string | undefined { + return this.padding ? `pi-wrapper__padding--${this.padding}` : ''; + } +} diff --git a/libs/ui/src/lib/ui/wrapper/directives/grid-wrapper.directive.ts b/libs/ui/src/lib/ui/wrapper/directives/grid-wrapper.directive.ts new file mode 100644 index 0000000..505f092 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/directives/grid-wrapper.directive.ts @@ -0,0 +1,105 @@ +import { Directive, HostBinding, Input, OnInit } from '@angular/core'; +import { grid } from '../../../styles'; + +@Directive() +export abstract class PiBaseGridWrapper { + @Input() alignment: 'start' | 'center' | 'end'; + @Input() alignSelf: 'start' | 'center' | 'end'; + @Input() gap: 'xxxxl' | 'xxxl' | 'xxl' | 'xl' | 'l' | 'xxs' | 's' | 'none' = 'xl'; + @Input() fill: 'fit-content' | 'fill-available' = 'fit-content'; + @Input() maxWidth: 'full-width' | 'edge-to-edge'; + @Input() isResponsive = false; + @Input() justifyItems: 'start' | 'center' | 'end'; + + protected get alignmentClass(): string | undefined { + return this.alignment && `pi-wrapper__align-items--${this.alignment}`; + } + + protected get alignSelfClass(): string | undefined { + return this.alignSelf && `pi-wrapper__align-self--${this.alignSelf}`; + } + + protected get gapClass(): string | undefined { + return this.gap && `pi-wrapper__gap--${this.gap}`; + } + + protected get fillClass(): string | undefined { + return this.fill !== 'fit-content' && `pi-wrapper__fill--${this.fill}`; + } + + protected get isResponsiveClass(): boolean | string { + return this.isResponsive ? 'is-responsive' : ''; + } + + protected get justifyItemsClass(): string | undefined { + return this.justifyItems && `pi-wrapper__justify-items--${this.justifyItems}`; + } + protected get maxWidthClass(): string | undefined { + return this.maxWidth && `pi-wrapper__max-width--${this.maxWidth}`; + } +} + +@Directive({ + standalone: false, + selector: '[pi-grid-two]', +}) +export class PiGridTwoDirective extends PiBaseGridWrapper { + @HostBinding('class') get classes(): string { + return [ + 'pi-wrapper pi-wrapper__grid pi-wrapper__grid--two', + this.alignmentClass, + this.gapClass, + this.fillClass, + this.justifyItemsClass, + ].join(' '); + } +} +// 4 elements +@Directive({ + standalone: false, + selector: '[pi-grid-four]', +}) +export class PiGridFourDirective extends PiBaseGridWrapper { + @HostBinding('class') get classes(): string { + const gridClass = this.grid && `pi-wrapper__grid--four-${this.grid}`; + return [ + 'pi-wrapper pi-wrapper__grid pi-wrapper__grid--four', + gridClass, + this.alignmentClass, + this.gapClass, + this.fillClass, + this.isResponsiveClass, + this.justifyItemsClass, + ].join(' '); + } + @Input() grid: '1-1-1-1' | '0-1-1-1' | '1-0-1-1' | '1-1-0-1' | '1-1-1-0' = '1-1-1-1'; +} + +// Custom grid +@Directive({ + standalone: false, + selector: '[pi-grid]', +}) +export class PiGridDirective extends PiBaseGridWrapper { + @HostBinding('class') get classes(): string { + const columnsClass = this.columns && `pi-wrapper__grid--columns-${this.columns}`; + const gridClass = this.grid && `pi-wrapper__grid--${this.grid}`; + return [ + 'pi-wrapper pi-wrapper__grid', + columnsClass, + gridClass, + this.isResponsiveClass, + this.alignmentClass, + this.alignSelfClass, + this.gapClass, + this.fillClass, + this.isResponsiveClass, + this.justifyItemsClass, + this.maxWidthClass, + ].join(' '); + } + + @Input() columns: string | number; + @Input() grid: grid; + @Input() override isResponsive = true; +} diff --git a/libs/ui/src/lib/ui/wrapper/directives/position-wrapper.directive.ts b/libs/ui/src/lib/ui/wrapper/directives/position-wrapper.directive.ts new file mode 100644 index 0000000..d594f17 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/directives/position-wrapper.directive.ts @@ -0,0 +1,20 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { PiBaseWrapper } from '../base.wrapper'; + +// Fixed wrapper +@Directive({ + standalone: false, + selector: '[pi-fixed]', +}) +export class PiFixedDirective { + @HostBinding('class') get classes(): string { + const FixedClass = 'is-fixed'; + const positionClass = this.position ? `is-fixed--${this.position}` : ''; + return [ + FixedClass, + positionClass + ].join(' '); + } + + @Input() position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +} diff --git a/libs/ui/src/lib/ui/wrapper/form/form.wrapper.html b/libs/ui/src/lib/ui/wrapper/form/form.wrapper.html new file mode 100644 index 0000000..ab12ef8 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/form/form.wrapper.html @@ -0,0 +1,4 @@ +

{{ title }}

+
+ +
diff --git a/libs/ui/src/lib/ui/wrapper/form/form.wrapper.ts b/libs/ui/src/lib/ui/wrapper/form/form.wrapper.ts new file mode 100644 index 0000000..7745bc8 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/form/form.wrapper.ts @@ -0,0 +1,30 @@ +import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { grid, WrapperTypeClass } from '../../../styles'; +import { PiBaseWrapper } from '../base.wrapper'; + +@Component({ + standalone: false, + selector: 'pi-form-wrapper', + templateUrl: './form.wrapper.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiFormWrapper extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + return [ + WrapperTypeClass.Form, + this.alignmentClass, + this.flexDirectionClass, + this.gridClass, + this.justifyClass, + this.maxWidthClass, + ].join(' '); + } + + @Input() formGroup!: FormGroup; + @Input() title: string | undefined; + @Input() subtitle: string | undefined; + + @Input() override grid: grid = 'none'; +} diff --git a/libs/ui/src/lib/ui/wrapper/header/header.wrapper.html b/libs/ui/src/lib/ui/wrapper/header/header.wrapper.html new file mode 100644 index 0000000..c42fd85 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/header/header.wrapper.html @@ -0,0 +1,18 @@ +
+ + + + +
+ + + + diff --git a/libs/ui/src/lib/ui/wrapper/header/header.wrapper.ts b/libs/ui/src/lib/ui/wrapper/header/header.wrapper.ts new file mode 100644 index 0000000..ad5925e --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/header/header.wrapper.ts @@ -0,0 +1,50 @@ +import { ChangeDetectionStrategy, Component, Directive, HostBinding, Input } from '@angular/core'; +import { alignment, WrapperTypeClass } from '../../../styles'; + +import { PiBaseWrapper } from '../base.wrapper'; +import { backLink } from '../definitions'; + +@Directive({ + standalone: false, + selector: 'pi-header-wrapper-left-content', + host: { class: 'pi-header-wrapper-content pi-header-wrapper-content__left' }, +}) +export class PiHeaderWrapperLeftContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-header-wrapper-center-content', + host: { class: 'pi-header-wrapper-content pi-header-wrapper-content__center' }, +}) +export class PiHeaderWrapperCenterContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-header-wrapper-right-content', + host: { class: 'pi-header-wrapper-content pi-header-wrapper-content__right' }, +}) +export class PiHeaderWrapperRightContentDirective {} + +@Component({ + standalone: false, + selector: 'pi-header-wrapper', + templateUrl: './header.wrapper.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiHeaderWrapper extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + return [ + WrapperTypeClass.Header, + this.maxWidthClass, + this.alignmentClass, + this.gapClass, + this.gridClass, + this.flexDirectionClass, + this.justifyClass, + ].join(' '); + } + + @Input() backLink: backLink | undefined; + + @Input() override alignment: alignment = 'flex-end'; +} diff --git a/libs/ui/src/lib/ui/wrapper/index.ts b/libs/ui/src/lib/ui/wrapper/index.ts new file mode 100644 index 0000000..018b215 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/index.ts @@ -0,0 +1,12 @@ +export * from './directives/flex-wrapper.directive'; +export * from './directives/grid-wrapper.directive'; +export * from './directives/position-wrapper.directive'; + +export * from './action/action.wrapper'; +export * from './header/header.wrapper'; +export * from './form/form.wrapper'; +export * from './list/list.wrapper'; +export * from './main/main.wrapper'; + +export * from './ui-wrapper.module'; +export * from './definitions'; diff --git a/libs/ui/src/lib/ui/wrapper/list/list.wrapper.html b/libs/ui/src/lib/ui/wrapper/list/list.wrapper.html new file mode 100644 index 0000000..cf2206b --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/list/list.wrapper.html @@ -0,0 +1,23 @@ + + +
+ +
+
+ + + + diff --git a/libs/ui/src/lib/ui/wrapper/list/list.wrapper.ts b/libs/ui/src/lib/ui/wrapper/list/list.wrapper.ts new file mode 100644 index 0000000..741feec --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/list/list.wrapper.ts @@ -0,0 +1,46 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; +import { WrapperTypeClass, flexDirection } from '../../../styles'; +import { ListMode } from '../../../util/list-configs'; +import { PiBaseWrapper } from '../base.wrapper'; + +@Component({ + standalone: false, + selector: 'pi-list-wrapper', + templateUrl: './list.wrapper.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiListWrapper extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + const hasScrollClass = this.hasScroll ? 'has-scroll' : ''; + const gridTypeClass = this.gridType !== 'flex' && `pi-wrapper__grid-type--${this.gridType}`; + return [ + WrapperTypeClass.List, + this.flexDirectionClass, + this.hasPaddingClass, + this.justifyClass, + this.maxWidthClass, + hasScrollClass, + gridTypeClass, + ].join(' '); + } + + page = 1; + limit = 12; + + @Input() isLoading!: boolean; + @Input() hasScroll = true; + @Input() mode: ListMode | null = 'list'; + + @Input() gridType: 'grid' | 'flex' = 'flex'; + + @Input() override flexDirection: flexDirection = 'column'; + + @Output() scrollList: EventEmitter = new EventEmitter(); +} diff --git a/libs/ui/src/lib/ui/wrapper/main/main.wrapper.html b/libs/ui/src/lib/ui/wrapper/main/main.wrapper.html new file mode 100644 index 0000000..7b2a210 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/main/main.wrapper.html @@ -0,0 +1,5 @@ +
+ + + +
diff --git a/libs/ui/src/lib/ui/wrapper/main/main.wrapper.ts b/libs/ui/src/lib/ui/wrapper/main/main.wrapper.ts new file mode 100644 index 0000000..f2f3c90 --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/main/main.wrapper.ts @@ -0,0 +1,55 @@ +import { ChangeDetectionStrategy, Component, Directive, HostBinding, Input } from '@angular/core'; +import { WrapperTypeClass } from '../../../styles'; +import { PiBaseWrapper } from '../base.wrapper'; + +@Directive() +export abstract class PiMainWrapperBaseContentDirective { + @HostBinding('class') get classes(): string { + const isStickyClass = this.isSticky === true ? 'is-sticky' : ''; + return ['pi-main-wrapper-content', isStickyClass].join(' '); + } + @Input() isSticky = false; +} + +@Directive({ + standalone: false, + selector: 'pi-main-wrapper-left-content', + host: { class: 'pi-main-wrapper-content__left' }, +}) +export class PiMainWrapperLeftContentDirective extends PiMainWrapperBaseContentDirective {} + +@Directive({ + standalone: false, + selector: 'pi-main-wrapper-right-content', + host: { class: 'pi-main-wrapper-content__right' }, +}) +export class PiMainWrapperRightContentDirective extends PiMainWrapperBaseContentDirective{} + +@Component({ + standalone: false, + selector: 'pi-main-wrapper', + templateUrl: './main.wrapper.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PiMainWrapper extends PiBaseWrapper { + @HostBinding('class') get classes(): string { + const typeClass = this.type && `pi-wrapper__main--type-${this.type}`; + const isCenteredClass = this.isCentered === true ? 'is-centered' : ''; + return [ + WrapperTypeClass.Main, + this.alignmentClass, + this.flexDirectionClass, + this.gapClass, + this.gridClass, + this.justifyClass, + this.maxWidthClass, + isCenteredClass, + typeClass, + ].join(' '); + } + + @Input() isCentered = false; + @Input() type: '' | 'document' = ''; + + @Input() override gap: 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl' = 'xxl' +} diff --git a/libs/ui/src/lib/ui/wrapper/ui-wrapper.module.ts b/libs/ui/src/lib/ui/wrapper/ui-wrapper.module.ts new file mode 100644 index 0000000..8ce342f --- /dev/null +++ b/libs/ui/src/lib/ui/wrapper/ui-wrapper.module.ts @@ -0,0 +1,87 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +// External +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; + +// Shared +import { PiSharedUIPanelModule } from '../panel'; +import { PiSharedUITextModule } from '../text'; +import { PiSharedUtilColorModule } from '../../util/color'; + +// Wrappers +import { RouterModule } from '@angular/router'; +import { PiActionWrapper } from './action/action.wrapper'; +import { PiFormWrapper } from './form/form.wrapper'; +import { + PiHeaderWrapper, + PiHeaderWrapperCenterContentDirective, + PiHeaderWrapperLeftContentDirective, + PiHeaderWrapperRightContentDirective, +} from './header/header.wrapper'; +import { PiListWrapper } from './list/list.wrapper'; +import { + PiMainWrapper, + PiMainWrapperLeftContentDirective, + PiMainWrapperRightContentDirective, +} from './main/main.wrapper'; + +// Wrapper directives +import { PiSharedUIButtonModule } from '../button'; +import { PiFlexDirective } from './directives/flex-wrapper.directive'; +import { + PiGridDirective, + PiGridFourDirective, + PiGridTwoDirective, +} from './directives/grid-wrapper.directive'; +import { PiFixedDirective } from './directives/position-wrapper.directive'; + +@NgModule({ + imports: [ + CommonModule, + ReactiveFormsModule, + RouterModule, + FormsModule, + PiSharedUIButtonModule, + PiSharedUITextModule, + PiSharedUIPanelModule, + PiSharedUtilColorModule, + InfiniteScrollModule, + ], + declarations: [ + PiActionWrapper, + PiFixedDirective, + PiFlexDirective, + PiFormWrapper, + PiGridDirective, + PiGridFourDirective, + PiGridTwoDirective, + PiHeaderWrapper, + PiHeaderWrapperCenterContentDirective, + PiHeaderWrapperLeftContentDirective, + PiHeaderWrapperRightContentDirective, + PiListWrapper, + PiMainWrapper, + PiMainWrapperLeftContentDirective, + PiMainWrapperRightContentDirective, + ], + exports: [ + PiActionWrapper, + PiFixedDirective, + PiFlexDirective, + PiFormWrapper, + PiGridDirective, + PiGridFourDirective, + PiGridTwoDirective, + PiHeaderWrapper, + PiHeaderWrapperCenterContentDirective, + PiHeaderWrapperLeftContentDirective, + PiHeaderWrapperRightContentDirective, + PiListWrapper, + PiMainWrapper, + PiMainWrapperLeftContentDirective, + PiMainWrapperRightContentDirective, + ], +}) +export class PiSharedUIWrapperModule {} diff --git a/libs/ui/src/lib/util/affix/affix.directive.ts b/libs/ui/src/lib/util/affix/affix.directive.ts new file mode 100644 index 0000000..846e046 --- /dev/null +++ b/libs/ui/src/lib/util/affix/affix.directive.ts @@ -0,0 +1,40 @@ +import { Directive, ElementRef, HostBinding, Input, OnInit, Renderer2 } from '@angular/core'; +import { unit } from '../models'; + +@Directive() +export abstract class PiBaseAffixDirective implements OnInit { + @HostBinding('class') get classes(): string { + return ['pi-affix', `pi-affix__appearance--${this.appearance}`].join(' '); + } + + @Input() unit: unit | string | undefined; + @Input() appearance: 'ghost' | 'fill' = 'ghost'; + + constructor(private renderer: Renderer2, private el: ElementRef) {} + + ngOnInit() { + if (this.unit) { + const span = this.renderer.createElement('span'); + const text = this.renderer.createText(this.unit); + + this.renderer.appendChild(span, text); + this.renderer.appendChild(this.el.nativeElement, span); + } + } +} + +@Directive({ + standalone: false, + selector: 'pi-prefix', +}) +export class PiPrefixDirective extends PiBaseAffixDirective { + @HostBinding('class') className = 'pi-affix__prefix'; +} + +@Directive({ + standalone: false, + selector: 'pi-suffix', +}) +export class PiSuffixDirective extends PiBaseAffixDirective { + @HostBinding('class') className = 'pi-affix__suffix'; +} diff --git a/libs/ui/src/lib/util/affix/index.ts b/libs/ui/src/lib/util/affix/index.ts new file mode 100644 index 0000000..2baac1a --- /dev/null +++ b/libs/ui/src/lib/util/affix/index.ts @@ -0,0 +1,3 @@ +export * from './affix.directive'; + +export * from './util-affix.module'; diff --git a/libs/ui/src/lib/util/affix/util-affix.module.ts b/libs/ui/src/lib/util/affix/util-affix.module.ts new file mode 100644 index 0000000..0ba35bb --- /dev/null +++ b/libs/ui/src/lib/util/affix/util-affix.module.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { PiPrefixDirective, PiSuffixDirective } from './affix.directive'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiPrefixDirective, PiSuffixDirective], + exports: [PiPrefixDirective, PiSuffixDirective], +}) +export class PiSharedUtilAffixModule {} diff --git a/libs/ui/src/lib/util/amplitude/amplitude.service.ts b/libs/ui/src/lib/util/amplitude/amplitude.service.ts new file mode 100644 index 0000000..f7066e4 --- /dev/null +++ b/libs/ui/src/lib/util/amplitude/amplitude.service.ts @@ -0,0 +1,44 @@ +import { isPlatformBrowser } from '@angular/common'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class AmplitudeService { + private amp: any; + private ampEnabled = false; + + constructor(@Inject(PLATFORM_ID) public platformId: object) { + if (isPlatformBrowser(this.platformId)) { + this.amp = (window as any).amplitude; + if (this.amp) this.ampEnabled = true; + } + } + + trackEvent(eventName: string, extraParam?: any): void { + if (!this.ampEnabled) { + this.enableCheck(); + return; + } + this.amp.logEvent(eventName, extraParam); + console.log('Event tracked: ', eventName, extraParam); + } + + identifyUser(tag: string, data: unknown): void { + const identify1: unknown = new this.amp.Identify().set(tag, data); + this.amp.identify(identify1); + } + + identifyUserMultiple(data: unknown): void { + const identify: unknown = new this.amp.setUserProperties(data); + this.amp.identify(identify); + } + + setUserId(userId: string): void { + this.amp.setUserId(userId); + } + + private enableCheck(): void { + if (!this.ampEnabled) console.log('Amplitude is not working'); + } +} diff --git a/libs/ui/src/lib/util/amplitude/events.ts b/libs/ui/src/lib/util/amplitude/events.ts new file mode 100644 index 0000000..655394e --- /dev/null +++ b/libs/ui/src/lib/util/amplitude/events.ts @@ -0,0 +1,70 @@ +export const AMP_EVENTS: Record = { + // PUBLIC EVENTS + APP_OPENED: 'App Opened', + GOOGLE_SIGNIN_SUCCESS: 'Sign in with Google Success', + LOGIN_SUCCESS: 'Login Success', + USER_REGISTERED: 'User Registered', + WEB_OPENED: 'Web Opened', + + // AUTHENTICATION + EMAIL_PASSWORD_PAGE_VIEWED: 'Email and Password Page Viewed', + PERSONAL_INFORMATION_PAGE_VIEWED: 'Personal Information and User Type Page Viewed', + PERSONAL_INFO_UPDATED: 'Personal Information Updated', + + // PRIVATE EVENTS + // COMPANY FORM + COMPANY_DETAILS_VIEWED: 'Company Details Viewed', + COMPANY_LIST_VIEWED: 'Company List Viewed', + COMPANY_MODIFIED: 'Company Modified', + COMPANY_REGISTERED: 'Company Registered', + + // DEALS + CONNECTED_COMPANY: 'Connected Company', + DEAL_ACCEPTED: 'Deal Accepted', + DEAL_VIEWED: 'Deal Viewed', + DEALBOX_VIEWED: 'Dealbox Viewed', + + // QA + QA_SESSION_CREATED: 'QA Session Created', + QUESTION_ADDED: 'Question Added', + QUESTION_ANSWERED: 'Question Answered', + QUESTIONS_SENT: 'Questions Sent', + ANSWERS_SENT: 'Answers Sent', + + + // INVESTOR FORM + SUBSCRIBE: 'Subscribe', + SUBSCRIBE_PRICING_PAGE_VIEWED: 'Subscribe Pricing Page Viewed', + USER_SUBSCRIBED: 'User Subscribed', + THESIS_SAVED: 'Thesis Saved', + THESIS_FORM_VIEWED: 'Thesis Form Viewed', + SUBSCRIBE_PRICING_VIEW: 'Subscribe Pricing Page Viewed', + + // COMPANY PUBLIC + BASIC_PANEL_SAVED: 'Basic Panel Saved', + PROFIT_LOSS_PANEL_SAVED: ' Profit And Loss Panel Saved', + BALANCE_PANEL_SAVED: 'Balance Panel Saved', + BUSINESS_PLAN_PANEL_SAVED: 'Business Plan Panel Saved', + TEAM_PANEL_SAVED: 'Team Panel Saved', + INVESTMENT_TRANSACTION_SAVED: 'Investment Transaction Panel Saved', + PRODUCTS_CLIENTS_SAVED: 'Products And Clients Panel Saved', + MY_COMPANY_OPENED: 'My Company Opened', + + TEASER_TYPEFORM_OPENED: 'Teaser Typeform Opened', + TEASER_TYPEFORM_MODAL_OPENED: 'Teaser Typeform Modal Opened', + TEASER_TYPEFORM_MODAL_CLOSED: 'Teaser Typeform Modal Closed', + + // INVESTOR LIST + INVESTOR_LIST_VIEWED: 'Investor List Viewed', + TEASER_DOWNLOADED: 'Teaser Downloaded', + + // ADVISOR + ADVISOR_COMPANY_CREATION_FORM_PAGE_VIEWED: 'Advisor Company Creation Form Page Viewed', + ADVISOR_COMPANY_REGISTERED: 'Advisor Company Registered', + ADVISOR_COMPANY_MODIFIED: 'Advisor Company Modified', + + + // WEB + VALORAR_MI_EMPRESA_TYPEFORM_MODAL_OPENED: 'Valorar Mi Empresa Typeform Modal Opened', + VENDER_MI_EMPRESA_TYPEFORM_MODAL_OPENED: 'Vender Mi Empresa Typeform Modal Opened' +}; diff --git a/libs/ui/src/lib/util/amplitude/index.ts b/libs/ui/src/lib/util/amplitude/index.ts new file mode 100644 index 0000000..7609ab0 --- /dev/null +++ b/libs/ui/src/lib/util/amplitude/index.ts @@ -0,0 +1,5 @@ +// Amplitude services +export * from './amplitude.service'; + +// Events +export * from './events'; diff --git a/libs/ui/src/lib/util/color/directives/colors.directive.ts b/libs/ui/src/lib/util/color/directives/colors.directive.ts new file mode 100644 index 0000000..8c331e5 --- /dev/null +++ b/libs/ui/src/lib/util/color/directives/colors.directive.ts @@ -0,0 +1,116 @@ +import { Directive, HostBinding, Input, OnInit } from '@angular/core'; +import { density, mode, opacity, styleType } from '../../../styles'; + +@Directive() +export abstract class PiBaseColor implements OnInit { + @HostBinding('class') get commonClasses(): string { + return [this.densityClass, this.modeClass].join(' '); + } + @Input() density: density = ''; + @Input() opacity: opacity = '100'; + @Input() mode: mode = 'dark'; + + ngOnInit(): void { + this.densityClass; + } + protected get densityClass(): string { + return !!this.density ? `pi-color__density--${this.density}` : ''; + } + protected get modeClass(): string { + return this.mode && `pi-color__mode--${this.mode}`; + } +} + +@Directive({ + standalone: false, + selector: '[pi-dynamic-color]', +}) +export class PiDynamicColorDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return this.colorClass; + } + + @Input() color!: styleType; + + protected get colorClass(): string { + return !!this.color && `pi-color__${this.color}`; + } +} + +@Directive({ + standalone: false, + selector: '[pi-primary]', +}) +export class PiColorPrimaryDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__primary'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-accent]', +}) +export class PiColorAccentDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__accent'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-warn]', +}) +export class PiColorWarnDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__warn'; + } +} +@Directive({ + standalone: false, + selector: '[pi-success]', +}) +export class PiColorSuccessDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__success'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-highlighted]', +}) +export class PiColorHighlightedDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__highlighted'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-white]', +}) +export class PiColorWhiteDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__white'; + } +} +@Directive({ + standalone: false, + selector: '[pi-grey]', +}) +export class PiColorGreyDirective extends PiBaseColor { + @HostBinding('class') get classes(): string { + return 'pi-color__grey'; + } +} + +@Directive({ + standalone: false, + selector: '[pi-black]', +}) +export class PiColorBlackDirective { + @HostBinding('class') get classes(): string { + return 'pi-color__black'; + } +} diff --git a/libs/ui/src/lib/util/color/entities/color.type.ts b/libs/ui/src/lib/util/color/entities/color.type.ts new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/libs/ui/src/lib/util/color/entities/color.type.ts @@ -0,0 +1 @@ + diff --git a/libs/ui/src/lib/util/color/index.ts b/libs/ui/src/lib/util/color/index.ts new file mode 100644 index 0000000..ce86236 --- /dev/null +++ b/libs/ui/src/lib/util/color/index.ts @@ -0,0 +1,3 @@ +export * from './util-color.module'; + +export * from './directives/colors.directive'; diff --git a/libs/ui/src/lib/util/color/util-color.module.ts b/libs/ui/src/lib/util/color/util-color.module.ts new file mode 100644 index 0000000..e3d04f0 --- /dev/null +++ b/libs/ui/src/lib/util/color/util-color.module.ts @@ -0,0 +1,42 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { + PiColorAccentDirective, + PiColorBlackDirective, + PiColorWarnDirective, + PiColorGreyDirective, + PiColorHighlightedDirective, + PiColorPrimaryDirective, + PiColorSuccessDirective, + PiColorWhiteDirective, + PiDynamicColorDirective, +} from './directives/colors.directive'; + +@NgModule({ + imports: [CommonModule, RouterModule], + declarations: [ + PiDynamicColorDirective, + PiColorPrimaryDirective, + PiColorAccentDirective, + PiColorSuccessDirective, + PiColorWarnDirective, + PiColorHighlightedDirective, + PiColorWhiteDirective, + PiColorGreyDirective, + PiColorBlackDirective, + ], + exports: [ + PiDynamicColorDirective, + PiColorPrimaryDirective, + PiColorAccentDirective, + PiColorSuccessDirective, + PiColorWarnDirective, + PiColorHighlightedDirective, + PiColorWhiteDirective, + PiColorGreyDirective, + PiColorBlackDirective, + ], +}) +export class PiSharedUtilColorModule {} diff --git a/libs/ui/src/lib/util/common/common.ts b/libs/ui/src/lib/util/common/common.ts new file mode 100644 index 0000000..b9b3b05 --- /dev/null +++ b/libs/ui/src/lib/util/common/common.ts @@ -0,0 +1,159 @@ +// Angular +import { HttpParams } from '@angular/common/http'; +import { Inject, PLATFORM_ID } from '@angular/core'; + +// Shared +import { Params } from '../models'; +import { timer } from 'rxjs'; +export class Commons { + constructor(@Inject(PLATFORM_ID) public platformId: object) {} + + static followUrl($event: MouseEvent, item: { url: string }): void { + Commons.followExternalUrl($event, item); + } + + static followExternalUrl( + $event: MouseEvent, + item: { url: string; externalLink?: boolean } + ): void { + if ( + item && + item.url && + (item.externalLink || $event.button === 1 || ($event.button === 0 && $event.ctrlKey)) + ) { + Commons.openExternalUrl(item.url); + } + } + + static openExternalSelfUrl(url: string): void { + Commons.openExternalUrl(url, '_self'); + } + + static openExternalUrlWithoutParams(url: string, target?: string): void { + window.open(url, target || '_blank'); + } + + + static openExternalUrl(url: string, target?: string): void { + window.open(url + window.location.search, target || '_blank'); + } + + static downloadPDFBase64(pdf: string, name: string): void { + const linkSource = `data:application/pdf;base64,${pdf}`; + const downloadLink: HTMLAnchorElement = document.createElement('a'); + const fileName: string = name; + downloadLink.href = linkSource; + downloadLink.download = fileName; + downloadLink.click(); + } + + static downloadPDF(url: string, name: string, target?: string): void { + const targetLink = target || '_self'; + let link = document.createElement('a'); + link.href = url; + link.download = name; + link.target = targetLink; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + link = null; + } + + static getDefaultLanguage(): string { + return navigator.language.split('-')[0] as string; + } + + static getFullName(user: { name: string; lastName: string }): string { + if (!user || !user.name) { + return ' '; + } + if (!user.lastName) { + return user.name; + } + return `${user.name} ${user.lastName}`; + } + + static setParamsToQueryUrl(obj: Params): string { + let params: string = ''; + for (const key in obj) { + if (obj[key]) { + const element = obj[key]; + params = params + '&' + key + '=' + element; + } + } + return params; + } + + static isEmpty(obj: unknown): boolean { + if (!obj) { + return true; + } + return JSON.stringify(obj) === JSON.stringify({}); + } + + static isNotEmpty(obj: unknown): boolean { + return !this.isEmpty(obj); + } + + static isStringEmpty = (str: string) => !str?.length; + + static buildRangeString( + minValue: number, + maxValue: number, + unit: string, + defaultLocale?: string + ): string { + if (!!minValue && !!maxValue) { + return `${minValue}${unit} - ${maxValue}${unit}`; + } + if (!!minValue) { + return `>${minValue}${unit}`; + } + if (!!maxValue) { + return `<${maxValue}${unit}`; + } + return defaultLocale; + } + + static removeEmptyValuesFromObject(obj: unknown): unknown { + return Object.fromEntries( + Object.entries(obj).filter( + ([_, v]) => v != null && v !== '' && (v as Array).length !== 0 + ) + ); + } + + static removeNullValuesFromQueryParams(params: HttpParams) { + const paramsKeysAux = params.keys(); + paramsKeysAux.forEach((key) => { + const value = params.get(key); + if (value === null || value === undefined || value === '' || value === 'null') { + params['map'].delete(key); + } + }); + return params; + } + + static removeDiacriticsFromString(str: string): string { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + } + + static getScrollParent(node: HTMLElement): HTMLElement { + if (node == null) { + return null; + } + if (node.scrollHeight > node.clientHeight) { + return node; + } else { + return this.getScrollParent(node.parentNode as HTMLElement); + } + } + + static createIntervalUsingTimer(delayValue1: number, delayValue2: number) { + return timer(delayValue1, delayValue2); + } + + static isNotNullish(value: unknown): boolean { + return value !== undefined && value !== null; + } +} diff --git a/libs/ui/src/lib/util/common/index.ts b/libs/ui/src/lib/util/common/index.ts new file mode 100644 index 0000000..3adb498 --- /dev/null +++ b/libs/ui/src/lib/util/common/index.ts @@ -0,0 +1,4 @@ +// Common +export * from './common'; +export * from './sector'; +export * from './select-options'; diff --git a/libs/ui/src/lib/util/common/sector.ts b/libs/ui/src/lib/util/common/sector.ts new file mode 100644 index 0000000..f532540 --- /dev/null +++ b/libs/ui/src/lib/util/common/sector.ts @@ -0,0 +1,26 @@ +// Angular +import { Inject, PLATFORM_ID } from '@angular/core'; +import { Sector } from '../models'; +import { SectorType, SectorUidMatchLocale } from '../models'; + +export class SectorCommons { + constructor(@Inject(PLATFORM_ID) public platformId: object) {} + + static getSectorLocaleFromUid(uid: string): string { + const sectorLocale: string = SectorUidMatchLocale[uid as SectorType]; + return `common.sectors.${sectorLocale}`; + } + + static getLocaleListFromUidList(sectorUids: Array): Array { + return sectorUids?.map((uid) => SectorCommons.getSectorLocaleFromUid(uid)); + } + + static convertUidListToSectorList(sectorUids: Array, sectors: Array): Array { + return sectors?.filter((sector) => sectorUids?.includes(sector.sectorUid)); + } + + static convertSectorListToUidList(sectors: Array): Array { + return sectors?.map((sector) => sector?.sectorUid); + } +} + diff --git a/libs/ui/src/lib/util/common/select-options.ts b/libs/ui/src/lib/util/common/select-options.ts new file mode 100644 index 0000000..51e375c --- /dev/null +++ b/libs/ui/src/lib/util/common/select-options.ts @@ -0,0 +1,21 @@ +import { Inject, PLATFORM_ID } from '@angular/core'; +import { SelectOption } from '../../ui/select'; + +export class SelectOptionsCommons { + constructor(@Inject(PLATFORM_ID) public platformId: object) {} + + static convertStringstoSelectOptions( + values: Array, + options: Array + ): Array { + return options?.filter((item) => values?.includes(item.value)); + } + + static convertSelectOptionstoStrings(options: Array): Array { + return options?.map((item) => item?.value); + } + + static getSelectOptionValue(value: string, options: Array): any { + return options?.find((item) => item?.value == value); + } +} diff --git a/libs/ui/src/lib/util/dato-cms/graphql.module.ts b/libs/ui/src/lib/util/dato-cms/graphql.module.ts new file mode 100644 index 0000000..8f6408c --- /dev/null +++ b/libs/ui/src/lib/util/dato-cms/graphql.module.ts @@ -0,0 +1 @@ +export const datoCms = "https://graphql.datocms.com/"; // our GraphQL API diff --git a/libs/ui/src/lib/util/dato-cms/index.ts b/libs/ui/src/lib/util/dato-cms/index.ts new file mode 100644 index 0000000..5461bc4 --- /dev/null +++ b/libs/ui/src/lib/util/dato-cms/index.ts @@ -0,0 +1 @@ +export * from './util-dato-cms.module'; diff --git a/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts b/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts new file mode 100644 index 0000000..ac99c64 --- /dev/null +++ b/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts @@ -0,0 +1,36 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; + +// Apollo +import { InMemoryCache } from '@apollo/client/core'; +import { APOLLO_OPTIONS } from 'apollo-angular'; +import { HttpLink } from 'apollo-angular/http'; + +// Shared +import { environment } from '../environments'; + +// datoCMS +import { datoCms } from './graphql.module'; + +/** + * Sustituye a `PiSharedUtilDatoCmsModule`: apollo-angular ya no expone + * `ApolloModule`, la configuración se hace con providers. + */ +export function providePiDatoCms(): EnvironmentProviders { + return makeEnvironmentProviders([ + { + provide: APOLLO_OPTIONS, + useFactory: (httpLink: HttpLink) => { + return { + cache: new InMemoryCache(), + link: httpLink.create({ + uri: datoCms, + headers: { + Authorization: environment.DATO_KEY, + } as any, + }), + }; + }, + deps: [HttpLink], + }, + ]); +} diff --git a/libs/ui/src/lib/util/device-configs/application/device-configs.facade.ts b/libs/ui/src/lib/util/device-configs/application/device-configs.facade.ts new file mode 100644 index 0000000..6f18075 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/application/device-configs.facade.ts @@ -0,0 +1,35 @@ +// Angular +import { Injectable } from '@angular/core'; + +// Ngrx +import { Store } from '@ngrx/store'; + +// Rxjs +import { Observable } from 'rxjs'; + +// State +import { Browser, Device, Size } from '../entities/device-configs.model'; +import * as fromActions from '../state/device-configs.actions'; +import * as fromState from '../state/device-configs.reducer'; +import * as fromSelectors from '../state/device-configs.selectors'; + +@Injectable({ providedIn: 'root' }) +export class DeviceConfigsFacade { + // Device + readonly device$: Observable = this.store.select(fromSelectors.getDevice); + readonly size$: Observable = this.store.select(fromSelectors.getSize); + readonly isSmallDevice$: Observable = this.store.select(fromSelectors.getIsSmallDevice); + + // Browser + readonly browser$: Observable = this.store.select(fromSelectors.getBrowser); + + constructor(private store: Store) {} + + getBrowser(): void { + this.store.dispatch(fromActions.BrowserActions.getBrowser()); + } + + setDevice(device: Device): void { + this.store.dispatch(fromActions.DeviceActions.saveDeviceType({ device })); + } +} diff --git a/libs/ui/src/lib/util/device-configs/entities/breakpoints.ts b/libs/ui/src/lib/util/device-configs/entities/breakpoints.ts new file mode 100644 index 0000000..8b0f290 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/entities/breakpoints.ts @@ -0,0 +1,19 @@ +// BreakPoint Observer +// Main Breakpoints +export const MaxMobileBreakpoint = '(max-width: 640px)'; +export const MinTabletBreakpoint = '(min-width: 640px)'; +export const MaxTabletBreakpoint = '(max-width: 1240px)'; +export const MinDesktopBreakpoint = '(min-width: 1240px)'; + +// Inner Breakpoints +export const MinTabletSmallBreakpoint = '(min-width: 640px)'; +export const MaxTabletSmallBreakpoint = '(max-width: 992px)'; +export const MinTabletLargeBreakpoint = '(min-width: 992px)'; +export const MaxTabletLargeBreakpoint = '(max-width: 1240px)'; + +export const MinDesktopSmallBreakpoint = '(min-width: 1240px)'; +export const MaxDesktopSmallBreakpoint = '(max-width: 1440px)'; +export const MinDesktopMediumBreakpoint = '(min-width: 1440px)'; +export const MaxDesktopMediumBreakpoint = '(max-width: 1920px)'; + +export const MinDesktopLargeBreakpoint = '(min-width: 1920px)'; diff --git a/libs/ui/src/lib/util/device-configs/entities/device-configs.model.ts b/libs/ui/src/lib/util/device-configs/entities/device-configs.model.ts new file mode 100644 index 0000000..f390f89 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/entities/device-configs.model.ts @@ -0,0 +1,5 @@ +export type Device = 'MOBILE' | 'TABLET' | 'DESKTOP'; + +export type Size = 'SMALL' | 'MEDIUM' | 'LARGE'; + +export type Browser = 'EXPLORER' | 'FIREFOX' | 'EDGE' | 'CHROME' | 'SAFARI' | 'UNKNOWN'; diff --git a/libs/ui/src/lib/util/device-configs/index.ts b/libs/ui/src/lib/util/device-configs/index.ts new file mode 100644 index 0000000..e22ae1a --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/index.ts @@ -0,0 +1,18 @@ +// Application +export * from './application/device-configs.facade'; + +// Entities +export * from './entities/breakpoints'; +export * from './entities/device-configs.model'; + +// Services +export * from './services/device-configs.service'; + +// State +export * from './state/device-configs.actions'; +export * from './state/device-configs.effects'; +export * from './state/device-configs.selectors'; +export * from './state/device-configs.reducer'; +import * as DeviceTypeActions from './state/device-configs.actions'; +import * as DeviceTypeSelectors from './state/device-configs.selectors'; +export { DeviceTypeActions, DeviceTypeSelectors }; diff --git a/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts b/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts new file mode 100644 index 0000000..a3f0bb4 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/services/device-configs.service.ts @@ -0,0 +1,176 @@ +import { BreakpointObserver } from '@angular/cdk/layout'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; + +import { Store } from '@ngrx/store'; + +// Entities +import { + MaxDesktopMediumBreakpoint, + MaxDesktopSmallBreakpoint, + MaxMobileBreakpoint, + MaxTabletBreakpoint, + MaxTabletLargeBreakpoint, + MaxTabletSmallBreakpoint, + MinDesktopBreakpoint, + MinDesktopLargeBreakpoint, + MinDesktopMediumBreakpoint, + MinDesktopSmallBreakpoint, + MinTabletBreakpoint, + MinTabletLargeBreakpoint, + MinTabletSmallBreakpoint, +} from '../entities/breakpoints'; +import { Browser } from '../entities/device-configs.model'; + +// State +import * as fromActions from '../state/device-configs.actions'; +import { DeviceConfigsState } from '../state/device-configs.reducer'; + +@Injectable({ + providedIn: 'root', +}) +export class DeviceConfigsService { + private htmlTag!: HTMLElement; + + constructor( + private store: Store, + private breakpointObserver: BreakpointObserver, + @Inject(DOCUMENT) private document: Document, + @Inject(PLATFORM_ID) public platformId: Object + ) { + this.deviceConfigsHandler(); + } + + getUserBrowser(): Browser { + const userAgent = window.navigator.userAgent; + if (/MSIE|Trident/.test(userAgent)) { + return 'EXPLORER'; + } else if (/Firefox/.test(userAgent)) { + return'FIREFOX'; + } else if (/Edg|Edge/.test(userAgent)) { + return 'EDGE'; + } else if (/Chrome/.test(userAgent) && !/Edg|Edge/.test(userAgent)) { + return 'CHROME'; + } else if (/Safari/.test(userAgent) && !/Edg|Edge/.test(userAgent)) { + return 'SAFARI'; + } else { + return 'UNKNOWN'; + } + } + + private deviceConfigsHandler(): void { + if (isPlatformBrowser(this.platformId)) { + this.htmlTag = this.document?.documentElement; + this.breakpointObserver + .observe([ + MaxMobileBreakpoint, + MinTabletBreakpoint, + MaxTabletBreakpoint, + MinDesktopBreakpoint, + // Inner + MinTabletSmallBreakpoint, + MaxTabletSmallBreakpoint, + MinTabletLargeBreakpoint, + MaxTabletLargeBreakpoint, + MinDesktopSmallBreakpoint, + MaxDesktopSmallBreakpoint, + MinDesktopMediumBreakpoint, + MaxDesktopMediumBreakpoint, + MinDesktopLargeBreakpoint, + ]) + .subscribe((result) => { + if (result.matches) { + // Mobile + if (result.breakpoints[MaxMobileBreakpoint]) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceType({ device: 'MOBILE' })); + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' })); + this.setClassIsMobile(); + } + // Tablet + if ( + result.breakpoints[MinTabletBreakpoint] && + result.breakpoints[MaxTabletBreakpoint] + ) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceType({ device: 'TABLET' })); + this.setClassIsTablet(); + // Tablet S + if ( + result.breakpoints[MinTabletSmallBreakpoint] && + result.breakpoints[MaxTabletSmallBreakpoint] + ) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' })); + this.setClassIsTabletSmall(); + } + // Tablet L + if ( + result.breakpoints[MinTabletLargeBreakpoint] && + result.breakpoints[MaxTabletLargeBreakpoint] + ) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' })); + this.setClassIsTabletLarge(); + } + } + + // Desktop + if (result.breakpoints[MinDesktopBreakpoint]) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceType({ device: 'DESKTOP' })); + this.setClassIsDesktop(); + // Desktop S + if ( + result.breakpoints[MinDesktopSmallBreakpoint] && + result.breakpoints[MaxDesktopSmallBreakpoint] + ) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' })); + this.setClassIsDesktopSmall(); + } + // Desktop M + if ( + result.breakpoints[MinDesktopMediumBreakpoint] && + result.breakpoints[MaxDesktopMediumBreakpoint] + ) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'MEDIUM' })); + this.setClassIsDesktopMedium(); + } + // Desktop L + if (result.breakpoints[MinDesktopLargeBreakpoint]) { + this.store.dispatch(fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' })); + this.setClassIsDesktopLarge(); + } + } + } + }); + } + } + + private setClassIsMobile(): void { + this.htmlTag.setAttribute('class', 'is-mobile'); + } + + private setClassIsTablet(): void { + this.htmlTag.setAttribute('class', 'is-tablet'); + } + + private setClassIsTabletSmall(): void { + this.htmlTag.setAttribute('class', 'is-tablet is-tablet__small'); + } + + private setClassIsTabletLarge(): void { + this.htmlTag.setAttribute('class', 'is-tablet is-tablet__large'); + } + + private setClassIsDesktop(): void { + this.htmlTag.setAttribute('class', 'is-desktop'); + } + + private setClassIsDesktopSmall(): void { + this.htmlTag.setAttribute('class', 'is-desktop is-desktop__small'); + } + + private setClassIsDesktopMedium(): void { + this.htmlTag.setAttribute('class', 'is-desktop is-desktop__medium'); + } + + private setClassIsDesktopLarge(): void { + this.htmlTag.setAttribute('class', 'is-desktop is-desktop__large'); + } +} diff --git a/libs/ui/src/lib/util/device-configs/state/device-configs.actions.ts b/libs/ui/src/lib/util/device-configs/state/device-configs.actions.ts new file mode 100644 index 0000000..cccd46e --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/state/device-configs.actions.ts @@ -0,0 +1,22 @@ +import { createActionGroup, emptyProps, props } from '@ngrx/store'; + +// Entities +import { Device, Size, Browser } from '../entities/device-configs.model'; + + +export const DeviceActions = createActionGroup({ + source: '[Main] Get Device Breakpoints', + events: { + 'Save Device Type': props<{ device: Device }>(), + 'Save Device Size': props<{ size: Size }>() + }, +}); + +export const BrowserActions = createActionGroup({ + source: '[Main] Get Browser', + events: { + 'Get Browser': emptyProps(), + 'Get Browser Success': props<{ response: Browser }>(), + 'Get Browser Error': emptyProps(), + }, +}); diff --git a/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts b/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts new file mode 100644 index 0000000..cc789a6 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/state/device-configs.effects.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; + +// Rxjs +import { of } from 'rxjs'; +import { catchError, exhaustMap, map } from 'rxjs/operators'; + +// Ngrx +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +// Actions +import * as fromActions from './device-configs.actions'; + +// Service +import { DeviceConfigsService } from '../services/device-configs.service'; + +@Injectable() +export class DeviceConfigsEffects { + constructor(private deviceConfigsService: DeviceConfigsService, private actions: Actions) {} + + getBrowser$ = createEffect(() => { + return this.actions.pipe( + ofType(fromActions.BrowserActions.getBrowser), + exhaustMap(() => + of(this.deviceConfigsService.getUserBrowser()).pipe( + map((response) => { + return fromActions.BrowserActions.getBrowserSuccess({ response }); + }), + catchError(() => { + return of(fromActions.BrowserActions.getBrowserError()); + }) + ) + ) + ); + }); +} diff --git a/libs/ui/src/lib/util/device-configs/state/device-configs.reducer.ts b/libs/ui/src/lib/util/device-configs/state/device-configs.reducer.ts new file mode 100644 index 0000000..a3ff10b --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/state/device-configs.reducer.ts @@ -0,0 +1,32 @@ +import { createReducer, on } from '@ngrx/store'; +import * as fromActions from './device-configs.actions'; + +import { Device, Size, Browser } from '../entities/device-configs.model'; + +export const featureKey = 'CORE_deviceConfigs'; + +export interface DeviceConfigsState { + device: Device; + size: Size; + browser: Browser +} + +export const initialState: DeviceConfigsState = { + device: 'DESKTOP', + size: null, + browser: 'UNKNOWN' +}; + +export const deviceConfigsReducer = createReducer( + initialState, + on(fromActions.DeviceActions.saveDeviceType, (state, action) => { + return { ...state, device: action.device }; + }), + + on(fromActions.DeviceActions.saveDeviceSize, (state, action) => { + return { ...state, size: action.size }; + }), + on(fromActions.BrowserActions.getBrowserSuccess, (state, action) => { + return { ...state, browser: action.response }; + }) +); diff --git a/libs/ui/src/lib/util/device-configs/state/device-configs.selectors.ts b/libs/ui/src/lib/util/device-configs/state/device-configs.selectors.ts new file mode 100644 index 0000000..b4547c2 --- /dev/null +++ b/libs/ui/src/lib/util/device-configs/state/device-configs.selectors.ts @@ -0,0 +1,19 @@ +import { createSelector, createFeatureSelector } from '@ngrx/store'; + +import * as fromReducer from './device-configs.reducer'; + +export const getFeatureState = createFeatureSelector( + fromReducer.featureKey +); + +// Device Breakpoints +export const getDevice = createSelector(getFeatureState, (state) => state.device); +export const getSize = createSelector(getFeatureState, (state) => state.size); + +// Mobile & Tablet S +export const getIsSmallDevice = createSelector(getFeatureState, (state) => { + return state.device !== 'DESKTOP' && state.size !== 'LARGE'; +}); + +// Browser +export const getBrowser = createSelector(getFeatureState, (state) => state.browser); diff --git a/libs/ui/src/lib/util/environments/environment.dev.ts b/libs/ui/src/lib/util/environments/environment.dev.ts new file mode 100644 index 0000000..97edbd7 --- /dev/null +++ b/libs/ui/src/lib/util/environments/environment.dev.ts @@ -0,0 +1,26 @@ +export const environment: any = { + production: false, + mandatorySignIn: false, + authenticationFlowType: 'USER_SRP_AUTH', + identityPoolId: 'eu-west-1:25f8388c-3826-4d38-acaa-0bfcb3f6251f', + region: 'eu-west-1', + identityPoolRegion: 'eu-west-1', + userPoolId: 'eu-west-1_vYyt3eApk', + userPoolWebClientId: '4bss36r712padssbhco9vg2uej', + API: 'https://d1u3b69g4c.execute-api.eu-west-1.amazonaws.com/staging/', + CREATE_LEAD_INVESTOR: + 'https://d1u3b69g4c.execute-api.eu-west-1.amazonaws.com/staging/lead/investor', + COGNITO_ID: '4bss36r712padssbhco9vg2uej', + INTERCOM_IDENTITY_VERIFICATION_SECRET: 'jpIdk_gk6pGDaJkTOMpfEWzhaoTKCi6zpR-GbkRi', + DATO_KEY: 'Bearer ca9c292a00a5ca85e7b08e5a04185a', + DATO_DEV_ENVIRONMENT: 'development', + DATO_PROD_ENVIRONMENT: 'master', + oauth: { + domain: 'staging-oauth.deale.es', + scope: ['phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin'], + redirectSignIn: 'https://staging-app.deale.es/auth', + redirectSignOut: 'https://staging-app.deale.es/auth', + responseType: 'code', + }, + federationTarget: 'COGNITO_USER_POOLS', +}; diff --git a/libs/ui/src/lib/util/environments/environment.prod.ts b/libs/ui/src/lib/util/environments/environment.prod.ts new file mode 100644 index 0000000..c5b5ca3 --- /dev/null +++ b/libs/ui/src/lib/util/environments/environment.prod.ts @@ -0,0 +1,25 @@ +export const environment: any = { + production: true, + identityPoolId: 'eu-west-1:571db516-5c84-40bd-bd16-aeb121805dea', + region: 'eu-west-1', + identityPoolRegion: 'eu-west-1', + userPoolId: 'eu-west-1_nZwcGc84a', + userPoolWebClientId: '31k1diku58oftoddd35l5ablri', + mandatorySignIn: false, + authenticationFlowType: 'USER_SRP_AUTH', + API: 'https://9e02sawl11.execute-api.eu-west-1.amazonaws.com/prod/', + CREATE_LEAD_INVESTOR: 'https://9e02sawl11.execute-api.eu-west-1.amazonaws.com/prod/lead/investor', + COGNITO_ID: '31k1diku58oftoddd35l5ablri', + INTERCOM_IDENTITY_VERIFICATION_SECRET: "jpIdk_gk6pGDaJkTOMpfEWzhaoTKCi6zpR-GbkRi", + DATO_KEY: 'Bearer ca9c292a00a5ca85e7b08e5a04185a', + DATO_DEV_ENVIRONMENT: 'development', + DATO_PROD_ENVIRONMENT: 'master', + oauth: { + domain: 'oauth.deale.es', + scope: ['phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin'], + redirectSignIn: 'https://app.deale.es/auth', + redirectSignOut: 'https://app.deale.es/auth', + responseType: 'code', + }, + federationTarget: 'COGNITO_USER_POOLS', +}; diff --git a/libs/ui/src/lib/util/environments/environment.ts b/libs/ui/src/lib/util/environments/environment.ts new file mode 100644 index 0000000..68d242d --- /dev/null +++ b/libs/ui/src/lib/util/environments/environment.ts @@ -0,0 +1,39 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment: any = { + production: false, + mandatorySignIn: false, + authenticationFlowType: 'USER_SRP_AUTH', + identityPoolId: 'eu-west-1:25f8388c-3826-4d38-acaa-0bfcb3f6251f', + region: 'eu-west-1', + identityPoolRegion: 'eu-west-1', + userPoolId: 'eu-west-1_vYyt3eApk', + userPoolWebClientId: '4bss36r712padssbhco9vg2uej', + API: 'https://d1u3b69g4c.execute-api.eu-west-1.amazonaws.com/staging/', + CREATE_LEAD_INVESTOR: + 'https://d1u3b69g4c.execute-api.eu-west-1.amazonaws.com/staging/lead/investor', + COGNITO_ID: '4bss36r712padssbhco9vg2uej', + INTERCOM_IDENTITY_VERIFICATION_SECRET: 'jpIdk_gk6pGDaJkTOMpfEWzhaoTKCi6zpR-GbkRi', + DATO_KEY: 'Bearer ca9c292a00a5ca85e7b08e5a04185a', + DATO_DEV_ENVIRONMENT: 'development', + DATO_PROD_ENVIRONMENT: 'master', + oauth: { + domain: 'staging-oauth.deale.es', + scope: ['phone', 'email', 'openid', 'profile', 'aws.cognito.signin.user.admin'], + redirectSignIn: 'https://staging-app.deale.es/auth', + redirectSignOut: 'https://staging-app.deale.es/auth', + responseType: 'code', + }, + federationTarget: 'COGNITO_USER_POOLS', +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/libs/ui/src/lib/util/environments/index.ts b/libs/ui/src/lib/util/environments/index.ts new file mode 100644 index 0000000..309b845 --- /dev/null +++ b/libs/ui/src/lib/util/environments/index.ts @@ -0,0 +1 @@ +export * from './environment'; diff --git a/libs/ui/src/lib/util/form-control/form-control.ts b/libs/ui/src/lib/util/form-control/form-control.ts new file mode 100644 index 0000000..b8c27e7 --- /dev/null +++ b/libs/ui/src/lib/util/form-control/form-control.ts @@ -0,0 +1,52 @@ +import { Injectable, Injector } from '@angular/core'; +import { ControlContainer, ControlValueAccessor } from '@angular/forms'; + +@Injectable() +export class PiFormControl implements ControlValueAccessor { + value: number | string | Array | boolean; + + onChange = (value: number | string | Array | boolean) => {}; + + onTouched = () => {}; + + disabled = false; + focused = false; + touched = false; + + constructor(protected injector: Injector) {} + + get controlContainer(): any { + return this.injector.get(ControlContainer); + } + + writeValue(value: number | string | Array | boolean) { + this.value = value; + } + + registerOnChange(onChange: any) { + this.onChange = onChange; + } + + registerOnTouched(onTouched: any) { + this.onTouched = onTouched; + } + + markAsTouched() { + if (!this.touched) { + this.onTouched(); + this.touched = true; + } + } + + setDisabledState(disabled: boolean) { + this.disabled = disabled; + } + + onFocus(): void { + this.focused = true; + } + + onBlur(): void { + this.focused = false; + } +} diff --git a/libs/ui/src/lib/util/form-control/index.ts b/libs/ui/src/lib/util/form-control/index.ts new file mode 100644 index 0000000..ef845da --- /dev/null +++ b/libs/ui/src/lib/util/form-control/index.ts @@ -0,0 +1 @@ +export * from './form-control'; diff --git a/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts b/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts new file mode 100644 index 0000000..347ca82 --- /dev/null +++ b/libs/ui/src/lib/util/icon-registry/icon-registry.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import { customIcons } from './icons.data'; + +@Injectable({ + providedIn: 'root', +}) +export class IconRegistryService { + constructor( + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer + ) {} + + init(): void { + this.registerIcons(customIcons, 'assets/icons/'); + } + + registerIcons(icons: Array, path: string) { + icons.forEach((icon) => { + this.matIconRegistry.addSvgIcon( + icon, + this.domSanitizer.bypassSecurityTrustResourceUrl(path + icon + '.svg') + ); + }); + } +} + + diff --git a/libs/ui/src/lib/util/icon-registry/icons.data.ts b/libs/ui/src/lib/util/icon-registry/icons.data.ts new file mode 100644 index 0000000..a0d04e8 --- /dev/null +++ b/libs/ui/src/lib/util/icon-registry/icons.data.ts @@ -0,0 +1,167 @@ +export const customIcons = [ + 'add-stroked', + 'add', + 'agriculture', + 'arrow-back', + 'arrow-down', + 'arrow-forward', + 'arrow-secondary-back', + 'arrow-secondary-down', + 'arrow-secondary-forward', + 'arrow-secondary-up', + 'arrow-up', + 'arrow-up-rounded', + 'attachment', + 'bankruptcy', + 'biotech', + 'briefcase', + 'box', + 'buildings', + 'calendar', + 'calendar-plus', + 'calendly', + 'caret', + 'chat', + 'check-rounded', + 'check', + 'chemistry-and-materials', + 'chevron-back', + 'chevron-down', + 'chevron-forward', + 'chevron-up', + 'choose', + 'circle', + 'click', + 'clip', + 'close-rounded', + 'close', + 'coin', + 'coins', + 'connect', + 'construction', + 'consumer', + 'dashboard', + 'dealbox', + 'details', + 'diagram', + 'dialog', + 'distribution', + 'document', + 'document-add', + 'document-search', + 'document-euro', + 'document-substract', + 'dollar', + 'double-chevron-back', + 'download-teaser', + 'download', + 'due-diligence', + 'e-commerce', + 'ebitda', + 'edit', + 'education', + 'email', + 'email-plus', + 'enter', + 'entertainment', + 'estimation-time', + 'euro', + 'exclamation', + 'exclusivity', + 'expand', + 'eye-off', + 'eye', + 'file-coins', + 'filter', + 'financial-services', + 'flag-grey', + 'focus', + 'globe', + 'google', + 'graphic', + 'grid', + 'hamburguer', + 'headquarters', + 'healthcare', + 'hospitality', + 'industrial', + 'info', + 'it-internet', + 'legal', + 'light-bulb', + 'link', + 'linkedin', + 'list', + 'location', + 'lock', + 'login', + 'logistics', + 'manufacturing', + 'marketing-media', + 'meeting', + 'messages', + 'messages-left', + 'messy-coin-stack', + 'metric-down-arrow', + 'metric-up-arrow', + 'mining', + 'mobility', + 'money-transfer', + 'more', + 'my-company', + 'nif-company', + 'notification-off', + 'notification-on', + 'operations', + 'opportunities', + 'optimum', + 'other-services', + 'percentage', + 'person-lines', + 'phone', + 'phone-arrow', + 'physical', + 'play-arrow', + 'premium', + 'pricing', + 'professional-services', + 'public-administration', + 'purchase-mandates', + 'question', + 'raw-materials', + 'real-estate', + 'search', + 'select', + 'sell-mandates', + 'send-information', + 'shareholders', + 'signature', + 'sort', + 'star-primary', + 'star-fill', + 'star-outline', + 'substract-stroked', + 'substract', + 'support', + 'telecomunications', + 'telephone', + 'telephone-fill', + 'time-detail', + 'time-zone', + 'transmission', + 'transport-logistics', + 'trash', + 'twitter', + 'upload', + 'user-profile', + 'user-profile-advisor', + 'user-profile-company', + 'user', + 'utilities', + 'verification-list', + 'verified-money', + 'website', + 'whatsapp', + 'youtube', + 'zoom', +]; diff --git a/libs/ui/src/lib/util/icon-registry/index.ts b/libs/ui/src/lib/util/icon-registry/index.ts new file mode 100644 index 0000000..3352b26 --- /dev/null +++ b/libs/ui/src/lib/util/icon-registry/index.ts @@ -0,0 +1,3 @@ +export * from './util-icon-registry.module'; + +export * from './icon-registry.service'; diff --git a/libs/ui/src/lib/util/icon-registry/util-icon-registry.module.ts b/libs/ui/src/lib/util/icon-registry/util-icon-registry.module.ts new file mode 100644 index 0000000..272b8b7 --- /dev/null +++ b/libs/ui/src/lib/util/icon-registry/util-icon-registry.module.ts @@ -0,0 +1,16 @@ +import { APP_INITIALIZER, NgModule } from '@angular/core'; +import { IconRegistryService } from './icon-registry.service'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + useFactory: (iconRegistryService: IconRegistryService) => { + return () => iconRegistryService.init(); + }, + deps: [IconRegistryService], + multi: true, + }, + ], +}) +export class PiSharedUtilIconRegistryModule {} diff --git a/libs/ui/src/lib/util/infinite-scroll/index.ts b/libs/ui/src/lib/util/infinite-scroll/index.ts new file mode 100644 index 0000000..6444c1f --- /dev/null +++ b/libs/ui/src/lib/util/infinite-scroll/index.ts @@ -0,0 +1 @@ +export * from './infinite-scroll.data'; \ No newline at end of file diff --git a/libs/ui/src/lib/util/infinite-scroll/infinite-scroll.data.ts b/libs/ui/src/lib/util/infinite-scroll/infinite-scroll.data.ts new file mode 100644 index 0000000..2a77ceb --- /dev/null +++ b/libs/ui/src/lib/util/infinite-scroll/infinite-scroll.data.ts @@ -0,0 +1,6 @@ +export enum INFINITE_SCROLL { + page = 1, + limit = 12, + throttle = 50, + scrollDistance = 2, +} diff --git a/libs/ui/src/lib/util/list-configs/application/list-configs.facade.ts b/libs/ui/src/lib/util/list-configs/application/list-configs.facade.ts new file mode 100644 index 0000000..e73a6be --- /dev/null +++ b/libs/ui/src/lib/util/list-configs/application/list-configs.facade.ts @@ -0,0 +1,28 @@ +// Angular +import { Injectable } from '@angular/core'; + +// Ngrx +import { Store } from '@ngrx/store'; + +// Rxjs +import { Observable } from 'rxjs'; + +// State +import * as fromSelectors from '../state/list-configs.selectors'; +import * as fromActions from '../state/list-configs.actions'; +import * as fromState from '../state/list-configs.reducer'; + +@Injectable({ providedIn: 'root' }) +export class ListConfigsFacade { + readonly mode$: Observable = this.store.select(fromSelectors.getListMode); + + constructor(private store: Store) {} + + setListMode(payload: fromState.ListMode): void { + this.store.dispatch(fromActions.setListMode({ payload })); + } + + resetListMode(): void { + this.store.dispatch(fromActions.resetListMode()); + } +} diff --git a/libs/ui/src/lib/util/list-configs/index.ts b/libs/ui/src/lib/util/list-configs/index.ts new file mode 100644 index 0000000..6ffe730 --- /dev/null +++ b/libs/ui/src/lib/util/list-configs/index.ts @@ -0,0 +1,7 @@ +// Facade +export * from './application/list-configs.facade'; + +// State +export * from './state/list-configs.actions'; +export * from './state/list-configs.selectors'; +export * from './state/list-configs.reducer'; diff --git a/libs/ui/src/lib/util/list-configs/state/list-configs.actions.ts b/libs/ui/src/lib/util/list-configs/state/list-configs.actions.ts new file mode 100644 index 0000000..3fc0204 --- /dev/null +++ b/libs/ui/src/lib/util/list-configs/state/list-configs.actions.ts @@ -0,0 +1,15 @@ +import { createAction, props } from '@ngrx/store'; + +import { ListMode } from './list-configs.reducer'; + +export enum ListModeActionsType { + SET_LIST_MODE = '[Main] Set List Mode', + RESET_LIST_MODE = '[Main] Reset List Mode', +} + +export const setListMode = createAction( + ListModeActionsType.SET_LIST_MODE, + props<{ payload: ListMode }>() +); + +export const resetListMode = createAction(ListModeActionsType.RESET_LIST_MODE); diff --git a/libs/ui/src/lib/util/list-configs/state/list-configs.reducer.ts b/libs/ui/src/lib/util/list-configs/state/list-configs.reducer.ts new file mode 100644 index 0000000..5a6978c --- /dev/null +++ b/libs/ui/src/lib/util/list-configs/state/list-configs.reducer.ts @@ -0,0 +1,25 @@ +import { createReducer, on } from '@ngrx/store'; + +import { resetListMode, setListMode } from './list-configs.actions'; + +export const featureKey = 'CORE_listConfigs'; + +export interface ListConfigsState { + mode: ListMode; +} + +export type ListMode = 'grid' | 'list'; + +export const initialState: ListConfigsState = { + mode: 'list', +}; + +export const listConfigsReducer = createReducer( + initialState, + on(setListMode, (state, action) => { + return { ...state, mode: action.payload }; + }), + on(resetListMode, (state) => { + return { ...initialState }; + }) +); diff --git a/libs/ui/src/lib/util/list-configs/state/list-configs.selectors.ts b/libs/ui/src/lib/util/list-configs/state/list-configs.selectors.ts new file mode 100644 index 0000000..6e2ad4c --- /dev/null +++ b/libs/ui/src/lib/util/list-configs/state/list-configs.selectors.ts @@ -0,0 +1,9 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +import * as fromReducer from './list-configs.reducer'; + +export const getFeatureState = createFeatureSelector( + fromReducer.featureKey +); + +export const getListMode = createSelector(getFeatureState, (state) => state.mode); diff --git a/libs/ui/src/lib/util/loaders/aplication/loader.facade.ts b/libs/ui/src/lib/util/loaders/aplication/loader.facade.ts new file mode 100644 index 0000000..d681d1a --- /dev/null +++ b/libs/ui/src/lib/util/loaders/aplication/loader.facade.ts @@ -0,0 +1,28 @@ +// Angular +import { Injectable } from '@angular/core'; + +// RxJS +import { Observable, Subscribable } from 'rxjs'; + +// NgRx +import { Store, select } from '@ngrx/store'; + +// Store +import * as LoaderSelectors from '../state/loader.selectors'; +import * as LoaderActions from '../state/loader.actions'; + +@Injectable() +export class LoaderFacade { + loader$: Observable = this.store.pipe(select(LoaderSelectors.getLoader)); + showLoader$: Observable | Subscribable | Promise; + + constructor(private store: Store) {} + + showLoader() { + this.store.dispatch(LoaderActions.showLoader()); + } + + hideLoader() { + this.store.dispatch(LoaderActions.hideLoader()); + } +} diff --git a/libs/ui/src/lib/util/loaders/index.ts b/libs/ui/src/lib/util/loaders/index.ts new file mode 100644 index 0000000..c3fe237 --- /dev/null +++ b/libs/ui/src/lib/util/loaders/index.ts @@ -0,0 +1,11 @@ +// Module +export * from './util-loader.module'; + +// Facade +export * from './aplication/loader.facade'; + +// State +export * from './state/loader.actions'; +export * from './state/loader.reducer'; +export * from './state/loader.selectors'; + diff --git a/libs/ui/src/lib/util/loaders/state/loader.actions.ts b/libs/ui/src/lib/util/loaders/state/loader.actions.ts new file mode 100644 index 0000000..54e7262 --- /dev/null +++ b/libs/ui/src/lib/util/loaders/state/loader.actions.ts @@ -0,0 +1,6 @@ +// NgRx +import { createAction } from '@ngrx/store'; + +export const showLoader = createAction('[Loader] Show Loader'); + +export const hideLoader = createAction('[Loader] Hide Loader'); diff --git a/libs/ui/src/lib/util/loaders/state/loader.reducer.ts b/libs/ui/src/lib/util/loaders/state/loader.reducer.ts new file mode 100644 index 0000000..a313284 --- /dev/null +++ b/libs/ui/src/lib/util/loaders/state/loader.reducer.ts @@ -0,0 +1,37 @@ +// NgRx +import { createReducer, on, Action } from '@ngrx/store'; + +// Store +import * as LoaderActions from './loader.actions'; + +export const loadersKey = 'CORE_loaderState'; + +export interface LoaderState { + loader: boolean; +} + +export const initialLoaderState: LoaderState = { + loader: false, +}; + +export const loaderReducer = createReducer( + initialLoaderState, + + on(LoaderActions.showLoader, (state) => { + return { + ...state, + loader: true, + }; + }), + + on(LoaderActions.hideLoader, (state) => { + return { + ...state, + loader: false, + }; + }) +); + +export function loaderReducerFunction(state: LoaderState | undefined, action: Action) { + return loaderReducer(state, action); +} diff --git a/libs/ui/src/lib/util/loaders/state/loader.selectors.ts b/libs/ui/src/lib/util/loaders/state/loader.selectors.ts new file mode 100644 index 0000000..5607199 --- /dev/null +++ b/libs/ui/src/lib/util/loaders/state/loader.selectors.ts @@ -0,0 +1,11 @@ +// NgRx +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +// Store +import { loadersKey, LoaderState } from './loader.reducer'; + +export const selectLoaderState = createFeatureSelector(loadersKey); + +export const getLoader = createSelector(selectLoaderState, (state) => { + return state.loader; +}); diff --git a/libs/ui/src/lib/util/loaders/util-loader.module.ts b/libs/ui/src/lib/util/loaders/util-loader.module.ts new file mode 100644 index 0000000..4175e07 --- /dev/null +++ b/libs/ui/src/lib/util/loaders/util-loader.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; + +import * as fromReducer from './state/loader.reducer'; + +@NgModule({ + imports: [ + CommonModule, + StoreModule.forFeature(fromReducer.loadersKey, fromReducer.loaderReducerFunction), + ], +}) +export class PiSharedUtilLoadersModule {} diff --git a/libs/ui/src/lib/util/local-storage/index.ts b/libs/ui/src/lib/util/local-storage/index.ts new file mode 100644 index 0000000..b67632b --- /dev/null +++ b/libs/ui/src/lib/util/local-storage/index.ts @@ -0,0 +1 @@ +export * from './local-storage.service'; diff --git a/libs/ui/src/lib/util/local-storage/local-storage.service.ts b/libs/ui/src/lib/util/local-storage/local-storage.service.ts new file mode 100644 index 0000000..92808f7 --- /dev/null +++ b/libs/ui/src/lib/util/local-storage/local-storage.service.ts @@ -0,0 +1,26 @@ +// Angular +import { isPlatformBrowser } from '@angular/common'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class LocalStorageService { + constructor(@Inject(PLATFORM_ID) public platformId: Object) {} + + getItem(tag: string) { + if (isPlatformBrowser(this.platformId)) { + const value = (window as any).localStorage.getItem(tag); + if (value) return JSON.parse(value); + } + return undefined; + } + + setItem(tag: string, value: any) { + if (isPlatformBrowser(this.platformId)) + (window as Window).localStorage.setItem(tag, JSON.stringify(value)); + } + + deleteItem(tag: string) { + if (isPlatformBrowser(this.platformId)) + (window as Window).localStorage.removeItem(tag); + } +} diff --git a/libs/ui/src/lib/util/menu/base.menu.component.ts b/libs/ui/src/lib/util/menu/base.menu.component.ts new file mode 100644 index 0000000..e76eb3f --- /dev/null +++ b/libs/ui/src/lib/util/menu/base.menu.component.ts @@ -0,0 +1,54 @@ +// Angular +import { ContentChild, Directive, HostBinding, Input, TemplateRef } from '@angular/core'; + +// Style definitions +import { MenuEntry, MenuItemText } from './entities/menu-entry.model'; + +@Directive() +export abstract class PiBaseMenuComponent { + @HostBinding('class') baseClassName = 'pi-menu pi-menu__parent'; + + isMouseOver = false; + + @Input() + label: string; + + _menuItems: Array = []; + + @Input() + set menuItems(menuItems: MenuEntry[]) { + this._menuItems = menuItems; + } + + get menuItems(): MenuEntry[] { + return this._menuItems; + } + + /** + * Parent Menu + */ + @ContentChild('parentMenuTemplate', { static: false }) + parentMenuTemplateRef: TemplateRef | undefined; + + /** + * Children Menu (Level 1) + */ + @ContentChild('childMenuTemplate', { static: false }) + childMenuTemplateRef: TemplateRef | undefined; + + /** + * Additional Children Menu (Level 1) + */ + @ContentChild('childAsideMenuTemplate', { static: false }) + childAsideMenuTemplateRef: TemplateRef | undefined; + + /** + * Additional Children Menu (Level 1) + */ + @ContentChild('asideMenuTemplate', { static: false }) + asideMenuTemplateRef: TemplateRef | undefined; + + trackByFn(index: number, item: MenuEntry): MenuItemText | string | undefined { + return item.label; + } +} diff --git a/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts b/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts new file mode 100644 index 0000000..7bae4ff --- /dev/null +++ b/libs/ui/src/lib/util/menu/directives/base-menu-item.directive.ts @@ -0,0 +1,57 @@ +import { + Directive, + ElementRef, + HostBinding, + HostListener, + Renderer2 +} from '@angular/core'; + +@Directive() +export abstract class PiBaseMenuEventItem { + @HostBinding('class') className = 'pi-menu-event-item'; + + constructor( + protected el: ElementRef, + protected renderer: Renderer2 + ) {} + + @HostListener('click') + onClick() { + if (!this.isChildrenVisible()) { + this.setIsVisible(); + } else { + this.setIsHidden(); + } + } + + + @HostListener('document:click', ['$event']) + onDocumentClick(event: MouseEvent) { + if (!this.el.nativeElement.contains(event.target)) { + this.setIsHidden(); + } + } + + protected setIsVisible(): void { + const parent = this.el.nativeElement as HTMLElement; + const children = (parent.children as HTMLCollection).item(1) ?? null; + + parent && this.renderer.addClass(parent, 'is-selected'); + children && this.renderer.addClass(children, 'is-visible'); + } + + protected setIsHidden(): void { + const parent = this.el.nativeElement; + const children = (parent.children as HTMLCollection).item(1) ?? null; + + parent && this.renderer.removeClass(parent, 'is-selected'); + children && this.renderer.removeClass(children, 'is-visible'); + } + + protected isChildrenVisible(): boolean { + const parent = this.el.nativeElement as HTMLElement; + const children = (parent.children as HTMLCollection).item(1) ?? null; + + return children?.classList.contains('is-visible'); + } +} diff --git a/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts b/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts new file mode 100644 index 0000000..9562001 --- /dev/null +++ b/libs/ui/src/lib/util/menu/directives/menu-children.directive.ts @@ -0,0 +1,15 @@ +import { Directive, ElementRef, HostBinding, HostListener, Renderer2 } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-menu-event-children]', +}) +export class PiMenuEventChildrenDirective { + @HostBinding('class') className = 'pi-menu pi-menu__children'; + constructor(private el: ElementRef, private renderer: Renderer2) {} + + @HostListener('click') onClickMenuItem() { + const menuChildren = this.el.nativeElement; + this.renderer.removeClass(menuChildren, 'is-visible'); + } +} diff --git a/libs/ui/src/lib/util/menu/directives/menu-entry.directive.ts b/libs/ui/src/lib/util/menu/directives/menu-entry.directive.ts new file mode 100644 index 0000000..74a8081 --- /dev/null +++ b/libs/ui/src/lib/util/menu/directives/menu-entry.directive.ts @@ -0,0 +1,15 @@ +import { Directive, HostBinding, Input } from '@angular/core'; +import { MenuEntryContext } from '../entities/menu-entry.model'; + +@Directive({ + standalone: false, + selector: '[pi-menu-entry]', +}) +export class PiMenuEntryDirective { + @HostBinding('class') get classes(): string { + const contextClass = this.context && `pi-menu-entry__context--${this.context}`; + return ['pi-menu-entry', contextClass].join(' '); + } + + @Input() context: MenuEntryContext = 'parent-menu'; +} diff --git a/libs/ui/src/lib/util/menu/entities/menu-entry.model.ts b/libs/ui/src/lib/util/menu/entities/menu-entry.model.ts new file mode 100644 index 0000000..9e7a9cb --- /dev/null +++ b/libs/ui/src/lib/util/menu/entities/menu-entry.model.ts @@ -0,0 +1,46 @@ +import { styleType } from '../../../styles'; + +export type MenuEntry = Partial<{ + id: number; + icon?: MenuItemIcon | string; + label?: MenuItemText | string; + route?: string | null; + link?: string; + userProfile?: string; + rights?: Array; + // Parent & Child Level 1 + entries?: Array; + // Child Additional Level 1 + asideEntries?: Array; + state?: boolean; + data?: any; + isDisabled?: boolean; + isActive?: boolean; + anchor?: string; + color?: styleType; +}>; + +export type Rights = { + id: number; + name: string; +}; + +export interface MenuItemIcon { + name: string; + color?: styleType; +} + +export interface MenuItemText { + name: string; + description?: string; +} + +export interface FooterLink { + id: number; + icon?: string; + label?: string; + link?: string; +} + +// Defines where the menu entry belongs +export type MenuEntryContext = 'parent-menu' | 'children-menu' | 'children-submenu' | 'aside-menu'; diff --git a/libs/ui/src/lib/util/menu/index.ts b/libs/ui/src/lib/util/menu/index.ts new file mode 100644 index 0000000..ea12657 --- /dev/null +++ b/libs/ui/src/lib/util/menu/index.ts @@ -0,0 +1,9 @@ +export * from './ui-menu.module'; + +export * from './entities/menu-entry.model'; + +export * from './base.menu.component'; + +export * from './directives/base-menu-item.directive'; +export * from './directives/menu-children.directive'; +export * from './directives/menu-entry.directive'; diff --git a/libs/ui/src/lib/util/menu/ui-menu.module.ts b/libs/ui/src/lib/util/menu/ui-menu.module.ts new file mode 100644 index 0000000..c39cc4f --- /dev/null +++ b/libs/ui/src/lib/util/menu/ui-menu.module.ts @@ -0,0 +1,20 @@ +// Angular +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslatePipe, TranslateDirective } from '@ngx-translate/core'; + +// Directives +import { PiMenuEventChildrenDirective } from './directives/menu-children.directive'; +import { PiMenuEntryDirective } from './directives/menu-entry.directive'; + + + +@NgModule({ + imports: [CommonModule, TranslatePipe, TranslateDirective], + declarations: [ + PiMenuEventChildrenDirective, + PiMenuEntryDirective, + ], + exports: [PiMenuEventChildrenDirective, PiMenuEntryDirective], +}) +export class PiSharedUtilMenuModule {} diff --git a/libs/ui/src/lib/util/models/chart-data.ts b/libs/ui/src/lib/util/models/chart-data.ts new file mode 100644 index 0000000..accb864 --- /dev/null +++ b/libs/ui/src/lib/util/models/chart-data.ts @@ -0,0 +1,23 @@ +export interface Dataset { + label: string; + data: Array; + borderColor: DatasetColor; + cubicInterpolationMode: 'monotone' | 'default'; + unit?: string; + unitPosition?: 'before' | 'after'; + backgroundColor?: DatasetColor; +} + +export enum DatasetColor { + primary = '#799ef0', + primary50 = '#F0F4FF', + highlighted = '#0D2A6B', + highlighted50 = '#EEF1fB', + accent = '#EDA82A', + accent50 = '#FFFAED', + grey = '#AEAEB6', + grey50 = '#F3F4F4', + grey75 = '#EEEFF1', + grey300 = '#89888F', + white= '#FFFFFF', +} \ No newline at end of file diff --git a/libs/ui/src/lib/util/models/date.ts b/libs/ui/src/lib/util/models/date.ts new file mode 100644 index 0000000..a4a7b42 --- /dev/null +++ b/libs/ui/src/lib/util/models/date.ts @@ -0,0 +1,5 @@ +export interface Date { + day: string; + month: string; + weekDay?: string; +} diff --git a/libs/ui/src/lib/util/models/faqs.ts b/libs/ui/src/lib/util/models/faqs.ts new file mode 100644 index 0000000..c5ee392 --- /dev/null +++ b/libs/ui/src/lib/util/models/faqs.ts @@ -0,0 +1,4 @@ +export interface FAQ { + question: string; + answer: string; +} diff --git a/libs/ui/src/lib/util/models/index.ts b/libs/ui/src/lib/util/models/index.ts new file mode 100644 index 0000000..ccbc568 --- /dev/null +++ b/libs/ui/src/lib/util/models/index.ts @@ -0,0 +1,7 @@ +export * from './chart-data'; +export * from './faqs'; +export * from './params'; +export * from './pricing'; +export * from './sector'; +export * from './unit'; +export * from './date'; diff --git a/libs/ui/src/lib/util/models/params.ts b/libs/ui/src/lib/util/models/params.ts new file mode 100644 index 0000000..2d13ecb --- /dev/null +++ b/libs/ui/src/lib/util/models/params.ts @@ -0,0 +1,3 @@ +export interface Params { + [key: string]: string; +} diff --git a/libs/ui/src/lib/util/models/pricing.ts b/libs/ui/src/lib/util/models/pricing.ts new file mode 100644 index 0000000..3da2648 --- /dev/null +++ b/libs/ui/src/lib/util/models/pricing.ts @@ -0,0 +1,59 @@ +import { mode, theming } from '../../styles'; + +export enum UserType { + INVESTOR = 'INVESTOR', + COMPANY = 'COMPANY', + ADVISOR = 'ADVISOR', + ADMIN = 'ADMIN', +} + +export enum PricingPlanType { + FREE = 'FREE', + BASIC = 'BASIC', + PREMIUM = 'PREMIUM', +} + +export enum BillingCycleType { + MONTHLY = 'MONTHLY', + YEARLY = 'YEARLY', + BIANNUAL = 'BIANNUAL', + QUARTERLY = 'QUARTERLY', +} + +export interface PricingCardLocales { + cta?: string; + icon: string; + priceSuffix: string; + subtitle: string; + title: string; +} + +export interface PricingPlan { + billingCycle: BillingCycleType; + price: number; + planType: PricingPlanType; + mode: mode; + color: theming; + featureOverview: Array; + priceHelpText?: Array; +} + +export interface PricingPlanCompleteInfo extends PricingPlan, PricingCardLocales {} + +export interface BillingCycleOption { + value: BillingCycleType; + label: string; + discount?: number; +} + +export interface Feature { + isEnabledFor: Array; + localeKey: string; + categoryLocaleKey?: string; +} + +export interface Phase { + icon: string; + features: Array; + localeKey: string; +} diff --git a/libs/ui/src/lib/util/models/sector.ts b/libs/ui/src/lib/util/models/sector.ts new file mode 100644 index 0000000..0bb6d52 --- /dev/null +++ b/libs/ui/src/lib/util/models/sector.ts @@ -0,0 +1,62 @@ +export enum SectorUidMatchLocale { + AGRICULTURE_FORESTRY_FISHING = 'agricultureAndOthers', + CONSTRUCTION = 'construction', + CONSUMER = 'consumer', + DISTRIBUTION_WHOLESALE_TRADE = 'distributionWholesaleTrade', + E_COMMERCE = 'ecommerce', + EDUCATION = 'education', + ENTERTAINMENT = 'entertainment', + FINANCIAL_SERVICES_INSURANCE = 'financialServicesInsurance', + HEALTHCARE = 'healthcare', + HOSPITALITY = 'hospitality', + IT_INTERNET = 'itInternet', + LEGAL_ACCOUNTING_MANAGEMENT_SERVICES = 'legalAccountingManagementServices', + MANUFACTURING = 'manufacturing', + MARKETING_MEDIA = 'marketingMedia', + MINING_OTHER_CHEMICAL_MATERIALS_EXTRACTION = 'miningAndChemicals', + MOBILITY = 'mobility', + OTHER_SERVICES = 'otherServices', + PHYSICAL_AND_LIFE_SCIENCES = 'scientificServices', + PUBLIC_ADMINISTRATION = 'publicAdministration', + REAL_ESTATE = 'realEstate', + TELECOMUNICATIONS = 'telecommunications', + TRANSPORT_LOGISTICS = 'transportLogistics', + UTILITIES = 'utilities', +} + +export enum SectorType { + AGRICULTURE_FORESTRY_FISHING = 'AGRICULTURE_FORESTRY_FISHING', + CONSTRUCTION = 'CONSTRUCTION', + CONSUMER = 'CONSUMER', + DISTRIBUTION_WHOLESALE_TRADE = 'DISTRIBUTION_WHOLESALE_TRADE', + E_COMMERCE = 'E_COMMERCE', + EDUCATION = 'EDUCATION', + ENTERTAINMENT = 'ENTERTAINMENT', + FINANCIAL_SERVICES_INSURANCE = 'FINANCIAL_SERVICES_INSURANCE', + HEALTHCARE = 'HEALTHCARE', + HOSPITALITY = 'HOSPITALITY', + IT_INTERNET = 'IT_INTERNET', + LEGAL_ACCOUNTING_MANAGEMENT_SERVICES = 'LEGAL_ACCOUNTING_MANAGEMENT_SERVICES', + MANUFACTURING = 'MANUFACTURING', + MARKETING_MEDIA = 'MARKETING_MEDIA', + MINING_OTHER_CHEMICAL_MATERIALS_EXTRACTION = 'MINING_OTHER_CHEMICAL_MATERIALS_EXTRACTION', + MOBILITY = 'MOBILITY', + OTHER_SERVICES = 'OTHER_SERVICES', + PHYSICAL_AND_LIFE_SCIENCES = 'PHYSICAL_AND_LIFE_SCIENCES', + PUBLIC_ADMINISTRATION = 'PUBLIC_ADMINISTRATION', + REAL_ESTATE = 'REAL_ESTATE', + TELECOMUNICATIONS = 'TELECOMUNICATIONS', + TRANSPORT_LOGISTICS = 'TRANSPORT_LOGISTICS', + UTILITIES = 'UTILITIES', +} + +/** + * Movido aquí desde `deale-saas/util/models` (lib de aplicación, fuera del + * alcance de esta librería) porque `SectorCommons` lo necesita. + */ +export interface Sector { + description: string; + name: string; + sectorUid: string; + icon?: string; +} diff --git a/libs/ui/src/lib/util/models/unit.ts b/libs/ui/src/lib/util/models/unit.ts new file mode 100644 index 0000000..9845970 --- /dev/null +++ b/libs/ui/src/lib/util/models/unit.ts @@ -0,0 +1 @@ +export type unit = '€' | '%' | '$' | 'M' | 'x' | ''; diff --git a/libs/ui/src/lib/util/pipes/index.ts b/libs/ui/src/lib/util/pipes/index.ts new file mode 100644 index 0000000..3455e58 --- /dev/null +++ b/libs/ui/src/lib/util/pipes/index.ts @@ -0,0 +1,4 @@ +// pipes +export * from './util-pipes.module'; + +export * from './pipes'; diff --git a/libs/ui/src/lib/util/pipes/pipes.ts b/libs/ui/src/lib/util/pipes/pipes.ts new file mode 100644 index 0000000..289a3a9 --- /dev/null +++ b/libs/ui/src/lib/util/pipes/pipes.ts @@ -0,0 +1,80 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + standalone: false, + name: 'numberFormat', +}) +export class NumberFormatPipe implements PipeTransform { + transform(value: unknown, ...args: Array): unknown { + return new Intl.NumberFormat('de-DE').format(+value); + } +} + +@Pipe({ + standalone: false, name: 'replace' }) +export class ReplacePipe implements PipeTransform { + transform(value: string, strToReplace: string, replacementStr: string): string { + if (!value || !strToReplace || !replacementStr) { + return value; + } + + return value.replace(new RegExp(strToReplace, 'g'), replacementStr); + } +} + +@Pipe({ + standalone: false, + name: 'sanitizerAltImage', +}) +export class SanitizerAltImagePipe implements PipeTransform { + transform(name: string): string { + return this.sanitizeAltImage(name); + } + + private sanitizeAltImage(name: string): string { + return name.split('.').slice(0, -1).join('.').replace(/-/g, ' '); + } +} + +@Pipe({ + standalone: false, + name: 'firstWord', +}) +export class FirstWordPipe implements PipeTransform { + transform(value: string): string { + if (!value) { + return ''; + } + return value.split(' ')[0]; + } +} + +@Pipe({ + standalone: false, name: 'truncate' }) +export class TruncatePipe implements PipeTransform { + transform(text: string, length: number = 20, suffix: string = '...'): string { + if (text.length > length) { + const truncated: string = text.substring(0, length).trim() + suffix; + return truncated; + } + return text; + } +} + +@Pipe({ + standalone: false, name: 'maxTwoDecimals' }) +export class MaxTwoDecimalsPipe implements PipeTransform { + transform(value: unknown, ...args: Array): unknown { + return Math.round(+value * 100) / 100; + } +} + +@Pipe({ + standalone: false, name: 'anonymize' }) +export class AnonymizePipe implements PipeTransform { + transform(text?: string, length = 1, char = '*') { + return text?.split(' ').map(word => + word.split('').map((letter, index) => (index < length ? letter : char)).join('') + ).join(' ') + } +} diff --git a/libs/ui/src/lib/util/pipes/util-pipes.module.ts b/libs/ui/src/lib/util/pipes/util-pipes.module.ts new file mode 100644 index 0000000..0ebc3c8 --- /dev/null +++ b/libs/ui/src/lib/util/pipes/util-pipes.module.ts @@ -0,0 +1,13 @@ +// Angular +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +// Pipes +import { AnonymizePipe, FirstWordPipe, MaxTwoDecimalsPipe, NumberFormatPipe, SanitizerAltImagePipe, TruncatePipe } from './pipes'; + +@NgModule({ + imports: [CommonModule], + declarations: [AnonymizePipe, FirstWordPipe, NumberFormatPipe, SanitizerAltImagePipe, TruncatePipe, MaxTwoDecimalsPipe ], + exports: [AnonymizePipe, FirstWordPipe, NumberFormatPipe, SanitizerAltImagePipe, TruncatePipe, MaxTwoDecimalsPipe], +}) +export class PiSharedUtilPipesModule {} diff --git a/libs/ui/src/lib/util/router-store/custom-route-serializer.ts b/libs/ui/src/lib/util/router-store/custom-route-serializer.ts new file mode 100644 index 0000000..863ad58 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/custom-route-serializer.ts @@ -0,0 +1,31 @@ +// Angular +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; + +// Router +import { RouterStateUrl } from './state/router.reducer'; + +// Ngrx +import * as fromRouter from '@ngrx/router-store'; + +export class CustomSerializer implements fromRouter.RouterStateSerializer { + serialize(routerState: RouterStateSnapshot): RouterStateUrl { + const { url } = routerState; + const { queryParams } = routerState.root; + + let route: ActivatedRouteSnapshot = routerState.root; + + while (route.firstChild) { + route = route.firstChild; + } + const { params, routeConfig } = route; + + // Only return an object including the URL, params and query params + // instead of the entire snapshot + return { + url, + params, + queryParams, + title: typeof routeConfig?.title === 'string' ? routeConfig.title : '', + }; + } +} diff --git a/libs/ui/src/lib/util/router-store/error-handler.ts b/libs/ui/src/lib/util/router-store/error-handler.ts new file mode 100644 index 0000000..fb40462 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/error-handler.ts @@ -0,0 +1,11 @@ +import { ErrorHandler, Injectable, Injector } from '@angular/core'; +import { Router } from '@angular/router'; + +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + constructor(private injector: Injector) { } + handleError(error: any) { + const router = this.injector.get(Router); + console.error(error.stack.toString()); + } +} \ No newline at end of file diff --git a/libs/ui/src/lib/util/router-store/index.ts b/libs/ui/src/lib/util/router-store/index.ts new file mode 100644 index 0000000..f754106 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/index.ts @@ -0,0 +1,10 @@ +export * from './util-router-store.module'; + +// State +export * from './state/router.actions'; +export * from './state/router.effects'; +export * from './state/router.reducer'; +export * from './state/router.selectors'; + +// Error handler +export * from './error-handler'; diff --git a/libs/ui/src/lib/util/router-store/state/router.actions.ts b/libs/ui/src/lib/util/router-store/state/router.actions.ts new file mode 100644 index 0000000..e6e9fe4 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/state/router.actions.ts @@ -0,0 +1,29 @@ +import { NavigationExtras } from '@angular/router'; + +import { createAction, props } from '@ngrx/store'; + +export const navigateTo = createAction( + '[Core Router] Navigate To', + props<{ + payload: { + path: any[]; + query?: object; + extras?: NavigationExtras; + }; + }>() +); + +export const navigateRefreshTo = createAction( + '[Core Router] Navigate Refresh To', + props<{ + payload: { + path: any[]; + query?: object; + extras?: NavigationExtras; + }; + }>() +); + +export const navigateBack = createAction('[Core Router] Navigate Back'); + +export const navigateForward = createAction('[Core Router] Navigate Forward'); diff --git a/libs/ui/src/lib/util/router-store/state/router.effects.ts b/libs/ui/src/lib/util/router-store/state/router.effects.ts new file mode 100644 index 0000000..055cb7b --- /dev/null +++ b/libs/ui/src/lib/util/router-store/state/router.effects.ts @@ -0,0 +1,74 @@ +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import * as RouterActions from './router.actions'; + +import { map, tap } from 'rxjs/operators'; + +import { Injectable } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; + +@Injectable() +export class RouterEffects { + navigateTo$ = createEffect( + () => { + return this.action$.pipe( + ofType(RouterActions.navigateTo), + map((action) => { + return action.payload; + }), + tap(({ path, query: queryParams, extras }) => { + this._router.navigate(path, { queryParams, ...extras }); + }) + ); + }, + { dispatch: false } + ); + + navigateRefreshTo$ = createEffect( + () => { + return this.action$.pipe( + ofType(RouterActions.navigateRefreshTo), + map((action) => { + return action.payload; + }), + tap(({ path, query: queryParams, extras }) => { + this._router.routeReuseStrategy.shouldReuseRoute = () => false; + this._router.onSameUrlNavigation = 'reload'; + this._router.navigate(path, { queryParams, ...extras }); + }) + ); + }, + { dispatch: false } + ); + + navigateBack$ = createEffect( + () => { + return this.action$.pipe( + ofType(RouterActions.navigateBack), + tap(() => { + this._location.back(); + }) + ); + }, + { dispatch: false } + ); + + navigateForward$ = createEffect( + () => { + return this.action$.pipe( + ofType(RouterActions.navigateForward), + tap(() => { + this._location.forward(); + }) + ); + }, + { dispatch: false } + ); + + constructor( + private _location: Location, + private action$: Actions, + private _router: Router + ) {} +} diff --git a/libs/ui/src/lib/util/router-store/state/router.reducer.ts b/libs/ui/src/lib/util/router-store/state/router.reducer.ts new file mode 100644 index 0000000..e28e8fb --- /dev/null +++ b/libs/ui/src/lib/util/router-store/state/router.reducer.ts @@ -0,0 +1,16 @@ +import * as fromRouter from '@ngrx/router-store'; + +import { Params } from '@angular/router'; + +export const coreRouterFeatureKey = 'CORE_router'; + +export interface RouterStateUrl { + url: string; + queryParams: Params; + params: Params; + title: string; +} + +export interface RouterState { + routerReducer: fromRouter.RouterReducerState; +} diff --git a/libs/ui/src/lib/util/router-store/state/router.selectors.ts b/libs/ui/src/lib/util/router-store/state/router.selectors.ts new file mode 100644 index 0000000..a8729d8 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/state/router.selectors.ts @@ -0,0 +1,15 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; + +import * as fromRouter from '@ngrx/router-store'; + +import * as fromReducer from './router.reducer'; + +export const getRouterState = createFeatureSelector< + fromRouter.RouterReducerState +>(fromReducer.coreRouterFeatureKey); + +export const getTitle = createSelector(getRouterState, (state) => { + return state?.state?.title +}); + +export const getRoute = createSelector(getRouterState, (state) => state?.state?.url); diff --git a/libs/ui/src/lib/util/router-store/util-router-store.module.ts b/libs/ui/src/lib/util/router-store/util-router-store.module.ts new file mode 100644 index 0000000..5422a49 --- /dev/null +++ b/libs/ui/src/lib/util/router-store/util-router-store.module.ts @@ -0,0 +1,16 @@ +import { ErrorHandler, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { RouterState, StoreRouterConnectingModule } from '@ngrx/router-store'; +import { CustomSerializer } from './custom-route-serializer'; + +@NgModule({ + imports: [ + CommonModule, + StoreRouterConnectingModule.forRoot({ + routerState: RouterState.Minimal, + serializer: CustomSerializer, + }), + ], +}) +export class PiSharedUtilRouterStoreModule {} diff --git a/libs/ui/src/lib/util/schema/index.ts b/libs/ui/src/lib/util/schema/index.ts new file mode 100644 index 0000000..6508cb2 --- /dev/null +++ b/libs/ui/src/lib/util/schema/index.ts @@ -0,0 +1 @@ +export * from './schema.service'; diff --git a/libs/ui/src/lib/util/schema/schema.service.ts b/libs/ui/src/lib/util/schema/schema.service.ts new file mode 100644 index 0000000..f53741e --- /dev/null +++ b/libs/ui/src/lib/util/schema/schema.service.ts @@ -0,0 +1,20 @@ +// Angular +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, Renderer2 } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class SchemaService { + script: HTMLScriptElement; + + constructor(@Inject(DOCUMENT) private doc: Document) {} + + loadJsScript(renderer: Renderer2, data: any): HTMLScriptElement { + const script = renderer.createElement('script'); + script.type = 'application/ld+json'; + script.text = `${JSON.stringify(data)}`; + renderer.appendChild(this.doc.body, script); + return script; + } +} diff --git a/libs/ui/src/lib/util/scroll-to/index.ts b/libs/ui/src/lib/util/scroll-to/index.ts new file mode 100644 index 0000000..1ee2991 --- /dev/null +++ b/libs/ui/src/lib/util/scroll-to/index.ts @@ -0,0 +1,5 @@ +export * from './scroll-to.module'; + +export * from './scroll-to.directive'; + +export * from './scroll-to.service'; \ No newline at end of file diff --git a/libs/ui/src/lib/util/scroll-to/scroll-to.directive.ts b/libs/ui/src/lib/util/scroll-to/scroll-to.directive.ts new file mode 100644 index 0000000..4f5d00f --- /dev/null +++ b/libs/ui/src/lib/util/scroll-to/scroll-to.directive.ts @@ -0,0 +1,15 @@ +import { + Directive, + HostBinding +} from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-scroll-to-element]', +}) +export class PiScrollToElementDirective { + @HostBinding('class') get classes(): string { + return 'pi-scroll-to pi-scroll-to__element'; + } +} + diff --git a/libs/ui/src/lib/util/scroll-to/scroll-to.module.ts b/libs/ui/src/lib/util/scroll-to/scroll-to.module.ts new file mode 100644 index 0000000..d901c23 --- /dev/null +++ b/libs/ui/src/lib/util/scroll-to/scroll-to.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiScrollToElementDirective } from './scroll-to.directive'; +import { PiSharedUtilScrollService } from './scroll-to.service'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiScrollToElementDirective], + exports: [PiScrollToElementDirective], + providers: [PiSharedUtilScrollService], +}) +export class PiSharedUtilScrollToModule {} diff --git a/libs/ui/src/lib/util/scroll-to/scroll-to.service.ts b/libs/ui/src/lib/util/scroll-to/scroll-to.service.ts new file mode 100644 index 0000000..bb19a1d --- /dev/null +++ b/libs/ui/src/lib/util/scroll-to/scroll-to.service.ts @@ -0,0 +1,43 @@ +import { ElementRef, Injectable, QueryList } from '@angular/core'; + +@Injectable() +export class PiSharedUtilScrollService { + scrollToNextElement(elements: QueryList): void { + for (let i = 0; i <= elements.length; i++) { + const element: ElementRef = elements.toArray()[i]; + if (this.isScrolledToBottom()) { + this.scrollToTop(); + break; + } + if (i === elements.length) { + this.scrollToBottom(); + break; + } + if (this.isElementInView(element.nativeElement)) { + element.nativeElement.scrollIntoView({ behavior: 'smooth' }); + break; + } + this.scrollToTop(); + } + } + + isElementInView(element: Element): boolean { + const rect = element.getBoundingClientRect(); + return rect.top > 40; + } + + scrollToTop(): void { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + scrollToBottom(): void { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + } + + isScrolledToBottom(): boolean { + const documentHeight = document.body.scrollHeight; + const currentScroll = window.scrollY + window.innerHeight; + return documentHeight <= currentScroll; + } +} + diff --git a/libs/ui/src/lib/util/status/directives/status.directive.ts b/libs/ui/src/lib/util/status/directives/status.directive.ts new file mode 100644 index 0000000..1c57a99 --- /dev/null +++ b/libs/ui/src/lib/util/status/directives/status.directive.ts @@ -0,0 +1,58 @@ +import { Directive, OnChanges, HostBinding, Input, SimpleChanges } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-disabled]', +}) +export class PiDisabledDirective implements OnChanges { + @HostBinding('style.opacity') get opacity(): number { + return this.isDisabled ? 0.33 : 1; + } + @HostBinding('class') get classes(): string | boolean { + return [this.isDisabled && 'is-disabled'].join(' '); + } + + @Input() isDisabled: boolean; + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { + this.isDisabled = changes['isDisabled'].currentValue; + } + } +} + +@Directive({ + standalone: false, + selector: '[is-active]', +}) +export class PiIsActiveDirective { + @HostBinding('class') get classes(): string | boolean { + return [this.isActive ? 'is-active' : '', 'pi-status'].join(' '); + } + + @Input() isActive!: boolean; + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { + this.isActive = changes['isActive'].currentValue; + } + } +} + +@Directive({ + standalone: false, + selector: '[is-visited]', +}) +export class PiIsVisitedDirective { + @HostBinding('class') get classes(): string | boolean { + return [this.isVisited ? 'is-visited' : '', 'pi-status'].join(' '); + } + + @Input() isVisited!: boolean; + + ngOnChanges(changes: SimpleChanges): void { + if (changes) { + this.isVisited = changes['isVisited'].currentValue; + } + } +} diff --git a/libs/ui/src/lib/util/status/entities/status.model.ts b/libs/ui/src/lib/util/status/entities/status.model.ts new file mode 100644 index 0000000..d63b78d --- /dev/null +++ b/libs/ui/src/lib/util/status/entities/status.model.ts @@ -0,0 +1 @@ +export type status = 'success' | 'error' | 'pending' | 'info' | 'disabled'; diff --git a/libs/ui/src/lib/util/status/index.ts b/libs/ui/src/lib/util/status/index.ts new file mode 100644 index 0000000..0c68c53 --- /dev/null +++ b/libs/ui/src/lib/util/status/index.ts @@ -0,0 +1,5 @@ +export * from './shared-util-status.module'; + +export * from './directives/status.directive'; + +export * from './entities/status.model'; diff --git a/libs/ui/src/lib/util/status/shared-util-status.module.ts b/libs/ui/src/lib/util/status/shared-util-status.module.ts new file mode 100644 index 0000000..411dc2c --- /dev/null +++ b/libs/ui/src/lib/util/status/shared-util-status.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + PiDisabledDirective, + PiIsActiveDirective, + PiIsVisitedDirective, +} from './directives/status.directive'; + +@NgModule({ + imports: [CommonModule], + declarations: [ + PiDisabledDirective, + PiIsActiveDirective, + PiIsVisitedDirective + ], + exports: [ + PiDisabledDirective, + PiIsActiveDirective, + PiIsVisitedDirective, + ], +}) +export class PiSharedUtilStatusModule {} diff --git a/libs/ui/src/lib/util/translate/custom-initializer.ts b/libs/ui/src/lib/util/translate/custom-initializer.ts new file mode 100644 index 0000000..9b25584 --- /dev/null +++ b/libs/ui/src/lib/util/translate/custom-initializer.ts @@ -0,0 +1,7 @@ +import { TranslateService } from '@ngx-translate/core'; + +export function appInitializerFactory(translateService: TranslateService) { + return () => { + translateService.use('es'); + }; +} diff --git a/libs/ui/src/lib/util/translate/i18n.service.ts b/libs/ui/src/lib/util/translate/i18n.service.ts new file mode 100644 index 0000000..b3e1254 --- /dev/null +++ b/libs/ui/src/lib/util/translate/i18n.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class I18nService { + constructor(private translateService: TranslateService) {} + + setLanguage(lang: string): Observable { + return new Observable((observer) => { + this.translateService.use(lang).subscribe({ + next: () => { + observer.next(); + observer.complete(); + }, + + error: (error: any) => observer.error(error), + }); + }); + } + + getTranslation(key: string): Observable { + return this.translateService.get(key); + } +} diff --git a/libs/ui/src/lib/util/translate/index.ts b/libs/ui/src/lib/util/translate/index.ts new file mode 100644 index 0000000..d44b5b2 --- /dev/null +++ b/libs/ui/src/lib/util/translate/index.ts @@ -0,0 +1,18 @@ +export * from './translate.module'; + +// Initializer +export * from './custom-initializer'; + +// Data +export * from './translate.data'; + +// Model +export * from './translate.model'; + +// Translate server +export * from './loaders/translate-browser.loader'; +export * from './loaders/translate-server.loader'; + + +// Service +export * from './i18n.service'; diff --git a/libs/ui/src/lib/util/translate/loaders/translate-browser.loader.ts b/libs/ui/src/lib/util/translate/loaders/translate-browser.loader.ts new file mode 100644 index 0000000..236f764 --- /dev/null +++ b/libs/ui/src/lib/util/translate/loaders/translate-browser.loader.ts @@ -0,0 +1,35 @@ +import { Observable } from 'rxjs'; +import { TranslateLoader } from '@ngx-translate/core'; + +import { makeStateKey, StateKey, TransferState } from '@angular/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { HttpClient } from '@angular/common/http'; + +export class TranslateBrowserLoader implements TranslateLoader { + constructor(private http: HttpClient, private transferState: TransferState) {} + + public getTranslation(lang: string): Observable { + const key: StateKey = makeStateKey( + 'transfer-translate-' + lang + ); + const data = this.transferState.get(key, null); + + // First we are looking for the translations in transfer-state, + // if none found, http load as fallback + if (data) { + return new Observable((observer) => { + observer.next(data); + observer.complete(); + }); + } else { + return new TranslateHttpLoader(this.http).getTranslation(lang); + } + } +} + +export function translateBrowserLoaderFactory( + httpClient: HttpClient, + transferState: TransferState +) { + return new TranslateBrowserLoader(httpClient, transferState); +} diff --git a/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts b/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts new file mode 100644 index 0000000..34d94bf --- /dev/null +++ b/libs/ui/src/lib/util/translate/loaders/translate-server.loader.ts @@ -0,0 +1,39 @@ +import { Observable } from 'rxjs'; +import { TranslateLoader } from '@ngx-translate/core'; +import { makeStateKey, StateKey, TransferState } from '@angular/core'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +export class TranslateServerLoader implements TranslateLoader { + constructor( + private transferState: TransferState, + private prefix: string = 'i18n', + private suffix: string = '.json' + ) {} + + public getTranslation(lang: string): Observable { + return new Observable((observer) => { + const assets_folder = join( + process.cwd(), + 'dist', + 'deale-website', // Your project name here + 'browser', + 'assets', + this.prefix + ); + + const jsonData = JSON.parse(readFileSync(`${assets_folder}/${lang}${this.suffix}`, 'utf8')); + + // Here we save the translations in the transfer-state + const key: StateKey = makeStateKey('transfer-translate-' + lang); + this.transferState.set(key, jsonData); + + observer.next(jsonData); + observer.complete(); + }); + } +} + +export function translateServerLoaderFactory(transferState: TransferState) { + return new TranslateServerLoader(transferState); +} diff --git a/libs/ui/src/lib/util/translate/translate.data.ts b/libs/ui/src/lib/util/translate/translate.data.ts new file mode 100644 index 0000000..6a46afc --- /dev/null +++ b/libs/ui/src/lib/util/translate/translate.data.ts @@ -0,0 +1,10 @@ +import { LanguageProps } from './translate.model'; + +export const LANGUAGES: Map = new Map([ + ['es', { languageCombo: 'common.languages.spanish', rtl: false, currency: 'EUR' }], + ['en', { languageCombo: 'common.languages.english', rtl: false, currency: 'USD' }], + ['ca', { languageCombo: 'common.languages.catalan', rtl: false, currency: 'EUR' }], + ['de', { languageCombo: 'common.languages.german', rtl: false, currency: 'EUR' }], + ['it', { languageCombo: 'common.languages.italian', rtl: false, currency: 'EUR' }], + ['fr', { languageCombo: 'common.languages.french', rtl: false, currency: 'EUR' }], +]); diff --git a/libs/ui/src/lib/util/translate/translate.model.ts b/libs/ui/src/lib/util/translate/translate.model.ts new file mode 100644 index 0000000..3752c6c --- /dev/null +++ b/libs/ui/src/lib/util/translate/translate.model.ts @@ -0,0 +1,10 @@ +export interface LanguageProps { + languageCombo: string; + rtl: boolean; + currency: string; +} + +export interface Language { + locale: string; + value: string; +} diff --git a/libs/ui/src/lib/util/translate/translate.module.ts b/libs/ui/src/lib/util/translate/translate.module.ts new file mode 100644 index 0000000..f7373c7 --- /dev/null +++ b/libs/ui/src/lib/util/translate/translate.module.ts @@ -0,0 +1,28 @@ +import { HttpClient } from '@angular/common/http'; +import { EnvironmentProviders, makeEnvironmentProviders, provideAppInitializer, inject } from '@angular/core'; + +import { TranslateLoader, TranslateService, provideTranslateService } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { appInitializerFactory } from './custom-initializer'; + +export function createTranslateLoader(http: HttpClient): TranslateHttpLoader { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} + +/** + * Sustituye a `PiSharedUtilTranslateModule`: ngx-translate 18 ya no expone + * `TranslateModule.forRoot()`, la configuración se hace con providers. + */ +export function providePiTranslate(): EnvironmentProviders { + return makeEnvironmentProviders([ + provideTranslateService({ + fallbackLang: 'es', + loader: { + provide: TranslateLoader, + useFactory: createTranslateLoader, + deps: [HttpClient], + }, + }), + provideAppInitializer(() => appInitializerFactory(inject(TranslateService))()), + ]); +} diff --git a/libs/ui/src/lib/util/validator/definitions.ts b/libs/ui/src/lib/util/validator/definitions.ts new file mode 100644 index 0000000..3061837 --- /dev/null +++ b/libs/ui/src/lib/util/validator/definitions.ts @@ -0,0 +1,18 @@ +export const TextFieldMaxLength = 300; +export const dniPattern = /^[\d]{8}[A-Za-z]{1}$/; +export const niePattern = /^[A-Za-z]{1}[\d]{7}[A-Za-z]{1}$/; +export const nifPattern = /^([A-Za-z][0-9]{8}|[0-9]{8}[A-Za-z])$/; +export const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; +export const emailPattern = + /^[A-Za-z0-9.+\-_]*@[A-Za-z0-9.\-_]*\.[A-Za-z]{1,20}$/; +export const phonePattern = /^(\+34|0034|34)?[ -]*(6|7|8|9)[ -]*([0-9][ -]*){8}$/; +export const passwordPattern = /^.{8,}$/; +export const fieldNamePattern = /^[A-Za-z0-9_]*$/; +export const onlyPositiveNumberPattern = /^[1-9]+[0-9]*$/; +export const positiveNumbersWithDecimals = /^\d+([.,]\d{0,2})?$/; +export const positiveNumbersIncludingZero = /^(0|\d+|\d+.\d+)$/; +export const onlyNumbersPattern = /^[0-9]*$/; +export const onlyNumbersDecimalsPattern = /^[0-9]*(\\,[0-9]{0,2})?$/; +export const repeatedPattern = /^(?!.*(\w)\1{3,}).+$/; +export const cognitoPhonePattern = /^\+[1-9]\d{10,14}$/; +export const monthYearPattern = /^(0[1-9]|1[0-2])\/([0-9]{2})$/; diff --git a/libs/ui/src/lib/util/validator/index.ts b/libs/ui/src/lib/util/validator/index.ts new file mode 100644 index 0000000..351bdc7 --- /dev/null +++ b/libs/ui/src/lib/util/validator/index.ts @@ -0,0 +1,2 @@ +export * from './definitions'; +export * from './utils'; diff --git a/libs/ui/src/lib/util/validator/utils.ts b/libs/ui/src/lib/util/validator/utils.ts new file mode 100644 index 0000000..1b87664 --- /dev/null +++ b/libs/ui/src/lib/util/validator/utils.ts @@ -0,0 +1,263 @@ +import { AbstractControl, FormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { environment } from '../environments'; +import { + dniPattern, + emailPattern, + urlPattern, + niePattern, + nifPattern, + onlyNumbersDecimalsPattern, + onlyNumbersPattern, + onlyPositiveNumberPattern, + passwordPattern, + phonePattern, + positiveNumbersWithDecimals, + positiveNumbersIncludingZero, + repeatedPattern, + cognitoPhonePattern, + monthYearPattern, +} from './definitions'; + +export function validateRequired(control: AbstractControl): ValidationErrors | null { + const value: string = control.value as string; + if (value === null || value === undefined || value === '' || value.length === 0) { + return { message: ['common.validator.requiredField'] }; + } + return null; +} + +export function validateRequiredIfDirty(control: AbstractControl): ValidationErrors | null { + const value: string = control.value as string; + if (value === null || value === undefined || value === '' || value.length === 0 || value === 'undefined') { + if (control.dirty) { + return { message: ['common.validator.requiredField'] }; + } + return null; + } + return null; +} + +export function validateNotZero(control: AbstractControl): ValidationErrors | null { + const value: number = control.value as number; + if (value === 0) { + return { message: ['common.validator.requiredField'] }; + } + return null; +} + +export function validateRequiredWithoutMessage(control: AbstractControl): ValidationErrors | null { + const value: boolean = control.value as boolean; + if (!value) { + return { message: [''] }; + } + return null; +} + +export function validatePassword(control: AbstractControl): ValidationErrors | null { + const password: string = control.value as string; + if (password && !passwordPattern.test(password)) { + return { message: ['common.validator.minimumPasswordCharacters'] }; + } + return null; +} + +export function validateEmail(control: AbstractControl): ValidationErrors | null { + const email: string = control.value as string; + if (email && !emailPattern.test(email)) { + return { message: ['common.validator.enterValidEmail'] }; + } + return null; +} + +export function validateUrl(control: AbstractControl): ValidationErrors | null { + const url: string = control.value as string; + if (url && !urlPattern.test(url)) { + return { message: ['common.validator.enterValidUrl'] }; + } + return null; +} + +export function validatePhone(control: AbstractControl): ValidationErrors | null { + const phone: string = control.value as string; + if (phone && !phonePattern.test(phone)) { + return { message: ['common.validator.enterValidPhoneNumber'] }; + } + return null; +} + +export function validateCognitoPhone(control: AbstractControl): ValidationErrors | null { + const phone: string = control.value as string; + if (phone && !cognitoPhonePattern.test(phone)) { + return { message: ['common.validator.enterValidPhoneNumber'] }; + } + return null; +} + +export function validateMaxLength(maxLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const maxChars: string = control.value as string; + if (maxChars && maxChars.length > maxLength) { + return { + message: [ + 'common.validator.noMoreThanCharacters.1', + `${maxLength}`, + 'common.validator.noMoreThanCharacters.2', + ], + }; + } + return null; + }; +} + +export function validateMinLength(minLength: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const minChars: string = control.value as string; + if (minChars && minChars.length <= minLength) { + return { + message: [ + 'common.validator.noLessThanCharacters.1', + `${minLength}`, + 'common.validator.noLessThanCharacters.2', + ], + }; + } + return null; + }; +} + +export function validateMaximumNumber(maximum: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const num: number = control.value as number; + if (maximum && num > Number(maximum)) { + return { + message: ['common.validator.noMoreThanMaxNumber.1', `${maximum}`], + }; + } + return null; + }; +} + +export function validateMinimumNumber(minimum: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const num: number = control.value as number; + if (minimum && num < Number(minimum)) { + return { message: ['common.validator.noLessThanMinNumbers', minimum] }; + } + return null; + }; +} + +export function validatePositiveNumber(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !onlyPositiveNumberPattern.test(num)) { + return { message: ['common.validator.onlyPositiveNumbers'] }; + } + return null; +} + +export function validateNif(control: AbstractControl): ValidationErrors | null { + const nif: string = control.value as string; + if (nif && !nifPattern.test(nif)) { + return { message: ['common.validator.letterAndNumberNIF'] }; + } + return null; +} + +export function validateNifInvestor(control: AbstractControl): ValidationErrors | null { + const nif: string = control.value as string; + if (nif && !nifPattern.test(nif) && !dniPattern.test(nif) && !niePattern.test(nif)) { + return { message: ['common.validator.incorrectNIF'] }; + } + return null; +} + +export function validateNifProd(control: AbstractControl): ValidationErrors | null { + const nif: string = control.value as string; + if (environment.production) { + if (nif && (nif === 'A00000000' || nif === 'B00000000')) { + return { message: ['common.validator.invalidNIF'] }; + } + } + return null; +} + +export function validateOnlyNumbers(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !onlyNumbersPattern.test(num)) { + return { message: ['common.validator.onlyNumbers'] }; + } + return null; +} + +export function validateOnlyNumbersWithDecimals(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !onlyNumbersDecimalsPattern.test(num)) { + return { message: ['common.validator.numbersWithDecimals'] }; + } + return null; +} + +export function validatePositiveNumbersWithDecimals( + control: AbstractControl +): ValidationErrors | null { + const num: string = control.value as string; + if (num && !positiveNumbersWithDecimals.test(num)) { + return { message: ['common.validator.numbersWithDecimals'] }; + } + return null; +} + +export function validatePositiveNumbersIncludingZero(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !positiveNumbersIncludingZero.test(num)) { + return { message: ['common.validator.positiveNumbersIncludingZero']} + } + return null; +} + +export function validateNotOnlyNumbers(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && onlyNumbersPattern.test(num)) { + return { message: ['common.validator.notOnlyNumbers'] }; + } + return null; +} + +export function notRepeatedChar(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !repeatedPattern.test(num)) { + return { message: ['common.validator.incorrectName'] }; + } + return null; +} + +export function validateMultipleChoiceRequired(control: AbstractControl): ValidationErrors | null { + const choices: { [key: string]: boolean } = control.value as { + [key: string]: boolean; + }; + let selected: boolean = false; + + selected = !!choices ? Object.entries(choices)?.some(([key, value]) => value) : null; + + if (choices && !selected) { + return { message: ['common.validator.atLeastOneOption'] }; + } + return null; +} + +export function rangeValidator(control: AbstractControl): ValidationErrors | null { + const from = Number(control.get('from').value); + const to = Number(control.get('to').value); + if (!from || !to) return null; + return from <= to + ? null + : { message: ['common.validator.minNotSuperiorToMax'] }; +} + +export function validateMonthYear(control: AbstractControl): ValidationErrors | null { + const num: string = control.value as string; + if (num && !monthYearPattern.test(num)) { + return { message: ['common.validator.monthYear'] }; + } + return null; +} diff --git a/libs/ui/src/lib/util/value/directives/value.directive.ts b/libs/ui/src/lib/util/value/directives/value.directive.ts new file mode 100644 index 0000000..7ab1015 --- /dev/null +++ b/libs/ui/src/lib/util/value/directives/value.directive.ts @@ -0,0 +1,21 @@ +import { Directive, HostBinding, Input } from '@angular/core'; + +@Directive({ + standalone: false, + selector: '[pi-value]', +}) +export class PiValueDirective { + @HostBinding('class') get classes(): string | boolean { + return ['pi-value', this.value > 0 ? 'is-positive' : 'is-negative'].join(' '); + } + + @Input() value!: number; + + checkValue(): string { + if (this.value > 0) { + return 'is-positive'; + } else { + return 'is-negative'; + } + } +} diff --git a/libs/ui/src/lib/util/value/index.ts b/libs/ui/src/lib/util/value/index.ts new file mode 100644 index 0000000..2945555 --- /dev/null +++ b/libs/ui/src/lib/util/value/index.ts @@ -0,0 +1,3 @@ +export * from './util-value.module'; + +export * from './directives/value.directive'; diff --git a/libs/ui/src/lib/util/value/util-value.module.ts b/libs/ui/src/lib/util/value/util-value.module.ts new file mode 100644 index 0000000..768881b --- /dev/null +++ b/libs/ui/src/lib/util/value/util-value.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PiValueDirective } from './directives/value.directive'; + +@NgModule({ + imports: [CommonModule], + declarations: [PiValueDirective], + exports: [PiValueDirective], + +}) +export class PiSharedUtilValueModule {} diff --git a/libs/ui/src/lib/util/view-title/index.ts b/libs/ui/src/lib/util/view-title/index.ts new file mode 100644 index 0000000..087b61d --- /dev/null +++ b/libs/ui/src/lib/util/view-title/index.ts @@ -0,0 +1 @@ +export * from './view-title.service'; diff --git a/libs/ui/src/lib/util/view-title/view-title.service.ts b/libs/ui/src/lib/util/view-title/view-title.service.ts new file mode 100644 index 0000000..1d0f2c3 --- /dev/null +++ b/libs/ui/src/lib/util/view-title/view-title.service.ts @@ -0,0 +1,20 @@ +import { RouterStateSnapshot, TitleStrategy } from '@angular/router'; +import { Title } from '@angular/platform-browser'; +import { Injectable } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root', +}) +export class ViewTitleService extends TitleStrategy { + constructor(private _title: Title, private _translate: TranslateService) { + super(); + } + + updateTitle(snapshot: RouterStateSnapshot): void { + const title = this.buildTitle(snapshot); + + if (title) this._title.setTitle(`Deale SaaS Platform | ${this._translate.instant(title)}`); + } +} diff --git a/libs/ui/src/test-setup.ts b/libs/ui/src/test-setup.ts new file mode 100644 index 0000000..b15cdf3 --- /dev/null +++ b/libs/ui/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true +}); diff --git a/apps/shop/tsconfig.json b/libs/ui/tsconfig.json similarity index 79% rename from apps/shop/tsconfig.json rename to libs/ui/tsconfig.json index 13a8853..40354b6 100644 --- a/apps/shop/tsconfig.json +++ b/libs/ui/tsconfig.json @@ -1,30 +1,27 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "isolatedModules": true, + "target": "es2022", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "target": "es2022", - "moduleResolution": "bundler", - "isolatedModules": true, "emitDecoratorMetadata": false, - "module": "preserve", - "lib": ["dom", "es2022"] + "module": "preserve" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, - "typeCheckHostBindings": true, "strictTemplates": true }, "files": [], "include": [], "references": [ { - "path": "./tsconfig.app.json" + "path": "./tsconfig.lib.json" }, { "path": "./tsconfig.spec.json" diff --git a/libs/ui/tsconfig.lib.json b/libs/ui/tsconfig.lib.json new file mode 100644 index 0000000..5bfe57f --- /dev/null +++ b/libs/ui/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "jest.config.ts", + "jest.config.cts", + "src/test-setup.ts" + ] +} diff --git a/libs/ui/tsconfig.lib.prod.json b/libs/ui/tsconfig.lib.prod.json new file mode 100644 index 0000000..61b5237 --- /dev/null +++ b/libs/ui/tsconfig.lib.prod.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": {} +} diff --git a/libs/ui/tsconfig.spec.json b/libs/ui/tsconfig.spec.json new file mode 100644 index 0000000..0fae7d0 --- /dev/null +++ b/libs/ui/tsconfig.spec.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": [ + "jest", + "node" + ], + "moduleResolution": "node10" + }, + "files": [ + "src/test-setup.ts" + ], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/nx.json b/nx.json index 7d56ecd..4afcdd1 100644 --- a/nx.json +++ b/nx.json @@ -48,6 +48,33 @@ "production", "^production" ] + }, + "@nx/angular:ng-packagr-lite": { + "cache": true, + "dependsOn": [ + "^build" + ], + "inputs": [ + "production", + "^production" + ] + }, + "@nx/jest:jest": { + "cache": true, + "inputs": [ + "default", + "^production", + "{workspaceRoot}/jest.preset.js" + ], + "options": { + "passWithNoTests": true + }, + "configurations": { + "ci": { + "ci": true, + "codeCoverage": true + } + } } }, "release": { diff --git a/package-lock.json b/package-lock.json index 3c5da93..76e965c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,32 @@ "license": "MIT", "dependencies": { "@angular-devkit/build-angular": "22.0.6", + "@angular/animations": "^22.0.6", + "@angular/cdk": "^22.0.6", "@angular/common": "22.0.6", "@angular/compiler": "22.0.6", "@angular/core": "22.0.6", "@angular/forms": "22.0.6", + "@angular/material": "^22.0.6", "@angular/platform-browser": "22.0.6", + "@angular/platform-browser-dynamic": "22.0.6", "@angular/platform-server": "22.0.6", "@angular/router": "22.0.6", "@angular/ssr": "22.0.6", + "@apollo/client": "^4.2.8", + "@ngrx/effects": "^21.1.1", + "@ngrx/router-store": "^21.1.1", + "@ngrx/store": "^21.1.1", + "@ngx-translate/core": "^18.0.0", + "@ngx-translate/http-loader": "^18.0.0", + "apollo-angular": "^14.1.0", + "chart.js": "^4.5.1", "express": "^4.21.2", + "graphql": "^17.0.2", + "lodash": "^4.18.1", + "luxon": "^3.7.2", + "ngx-currency": "^19.0.0", + "ngx-infinite-scroll": "^22.0.0", "rxjs": "~7.8.0", "zone.js": "0.16.2" }, @@ -38,6 +55,7 @@ "@nx/esbuild": "23.1.0", "@nx/eslint": "23.1.0", "@nx/eslint-plugin": "23.1.0", + "@nx/jest": "23.1.0", "@nx/js": "23.1.0", "@nx/node": "23.1.0", "@nx/playwright": "23.1.0", @@ -52,22 +70,33 @@ "@swc/core": "1.15.8", "@swc/helpers": "0.5.18", "@types/express": "^4.17.21", + "@types/jest": "~30.0.0", + "@types/lodash": "^4.17.24", + "@types/luxon": "^3.7.2", "@types/node": "^24.0.0", "@types/supertest": "^6.0.3", "@typescript-eslint/utils": "^8.40.0", "@vitest/coverage-v8": "~4.1.0", "@vitest/ui": "~4.1.0", "angular-eslint": "22.1.0", + "autoprefixer": "^10.4.0", "esbuild": "^0.27.0", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-playwright": "^1.6.2", + "jest": "~30.3.0", + "jest-environment-jsdom": "~30.3.0", + "jest-preset-angular": "~17.0.0", + "jest-util": "~30.3.0", "jiti": "2.4.2", - "jsdom": "~22.1.0", + "jsdom": "^26.0.0", "jsonc-eslint-parser": "^2.1.0", + "ng-packagr": "~22.0.0", "nx": "23.1.0", + "postcss": "^8.4.5", "prettier": "^3.8.1", "supertest": "^7.1.4", + "ts-jest": "^29.4.7", "ts-node": "10.9.1", "tslib": "^2.3.0", "typescript": "6.0.3", @@ -1442,6 +1471,22 @@ "typescript": "*" } }, + "node_modules/@angular/animations": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.0.6.tgz", + "integrity": "sha512-5bBAuKtawVK4KPq6U/yoE5yCyKymEnXNN7UJ1Lb7PUuuYmLnUXb7zJpC7HwtHTuV/0PTCkGlLE59NtcrrolTuA==", + "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.0.6" + } + }, "node_modules/@angular/build": { "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.6.tgz", @@ -2547,6 +2592,46 @@ "@esbuild/win32-x64": "0.27.7" } }, + "node_modules/@angular/cdk": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.0.6.tgz", + "integrity": "sha512-b36f4b+7yQpuulG9Ai2O38SV2tsqyNeYTYCH9TehMir18XlTx+NtLELwA8Sfi24tHByJHEDADQZtHPWWUj71/w==", + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cdk/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@angular/cdk/node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@angular/cli": { "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.6.tgz", @@ -2614,6 +2699,7 @@ "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.6.tgz", "integrity": "sha512-HPiceCIEyIcTmOuIFiyqu04kbLoJhSUavcOy/kzJkJ2OPXtPvGKaimC4lgs0C86uFy4P6rj6PDITBe3AVHkMTA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.29.7", @@ -2646,6 +2732,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -2676,12 +2763,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2691,6 +2780,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -2758,6 +2848,23 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" } }, + "node_modules/@angular/material": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.0.6.tgz", + "integrity": "sha512-EVKhP3689o+7J4TD7KMzmypxKGAUdvU3UsIhXYEoJ2IyxF/jAfuWSVKBmR8Ig6w74wSayBglfOTVoI3RuDL9NA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "22.0.6", + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/platform-browser": { "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.6.tgz", @@ -2780,6 +2887,25 @@ } } }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "22.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.0.6.tgz", + "integrity": "sha512-VKf8mg+BviBp3wlElZYxWFuXtMLQqd8rfm6jEzwP1Emaz2MCx69Y54dXopD81KJCqRwTmJUVxOjoubqUOlJzRw==", + "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.0.6", + "@angular/compiler": "22.0.6", + "@angular/core": "22.0.6", + "@angular/platform-browser": "22.0.6" + } + }, "node_modules/@angular/platform-server": { "version": "22.0.6", "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-22.0.6.tgz", @@ -2838,6 +2964,64 @@ } } }, + "node_modules/@apollo/client": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.2.8.tgz", + "integrity": "sha512-Iw/e/5fpOx+/2CgtcFNLdwxtdPzwTfmD4mYRjHA1UBdkfe6WOtWLRQeRESCUJCh49blXwp9I496A0MIoLgmCsw==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", + "@wry/equality": "^0.5.6", + "@wry/trie": "^0.5.0", + "graphql-tag": "^2.12.6", + "optimism": "^0.18.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "graphql": "^16.0.0 || ^17.0.0", + "graphql-ws": "^5.5.5 || ^6.0.3", + "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "react-dom": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", + "rxjs": "^7.3.0", + "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" + }, + "peerDependenciesMeta": { + "graphql-ws": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "subscriptions-transport-ws": { + "optional": true + } + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -4791,7 +4975,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.13.0.tgz", "integrity": "sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==", - "devOptional": true, + "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@colordx/core": { @@ -4825,6 +5009,121 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", @@ -4872,6 +5171,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4888,6 +5188,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4904,6 +5205,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4920,6 +5222,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4936,6 +5239,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4952,6 +5256,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4968,6 +5273,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4984,6 +5290,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5000,6 +5307,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5016,6 +5324,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5032,6 +5341,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5048,6 +5358,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5064,6 +5375,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5080,6 +5392,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5096,6 +5409,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5112,6 +5426,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5128,6 +5443,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5144,6 +5460,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5160,6 +5477,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5176,6 +5494,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5192,6 +5511,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5208,6 +5528,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5224,6 +5545,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5240,6 +5562,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5256,6 +5579,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5272,6 +5596,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5554,6 +5879,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@harperfast/extended-iterable": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", @@ -6083,6 +6417,919 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/console/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/reporters": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/@jest/core/node_modules/babel-jest/node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/@jest/core/node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jest/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/@jest/core/node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jest/core/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/@jest/diff-sequences": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", @@ -6109,6 +7356,188 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/expect": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", @@ -6154,6 +7583,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/@jest/fake-timers/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/get-type": { "version": "30.1.0", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", @@ -6237,6 +7684,24 @@ } } }, + "node_modules/@jest/reporters/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/schemas": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", @@ -6346,6 +7811,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@jest/transform/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/types": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", @@ -6830,6 +8313,12 @@ "tslib": "2" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -7677,6 +9166,49 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@ngrx/effects": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-21.1.1.tgz", + "integrity": "sha512-JQUv2tYyjkbdsE40WYtJDsiI+3qtn4V5m0Ybr2CYw9zKpVQshDV5MLLAni+SuijlHAWJkw8x4wwnYmNyG/Rmng==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "@ngrx/store": "21.1.1", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/router-store": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-21.1.1.tgz", + "integrity": "sha512-niO2J0aYgMcplHRgg3Nf1DPjHpg4ehZjqnA282DtLOXlYtHWoP2ePZcvnPxhK1K77Am5hSGOLa7U2DjjgwlSdQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/router": "^21.0.0", + "@ngrx/store": "21.1.1", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-21.1.1.tgz", + "integrity": "sha512-B0/IyBTqBExmwPAmrpcJrhlrn+FeeitvOW8ltf7O3kJb2vRKduV5Oi34GLiwYQ8eAukTbKyWQv/VV02gxLeEMg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, "node_modules/@ngtools/webpack": { "version": "22.0.6", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-22.0.6.tgz", @@ -7694,6 +9226,34 @@ "webpack": "^5.54.0" } }, + "node_modules/@ngx-translate/core": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-18.0.0.tgz", + "integrity": "sha512-Z9u9AXaeuyeHHimivvJQvhal/LJNB+5tlH+FimSU4QQ61FrtRDqgyn6nfFbc6Cb+59NDZdsh4QeTJyPCldFUwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=18", + "@angular/core": ">=18", + "rxjs": ">=7" + } + }, + "node_modules/@ngx-translate/http-loader": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-18.0.0.tgz", + "integrity": "sha512-p6QpNcU3rkCYjHbKVIadcYtml/Njpr0aLp8neJ4uJWQ4Xm9f09bIePhOjdjAFHupTptKqKrG1J2gSi3RSwgYeA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=18", + "@angular/core": ">=18", + "@ngx-translate/core": ">=18.0.0" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -7772,20 +9332,6 @@ "node": ">= 14" } }, - "node_modules/@npmcli/agent/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/@npmcli/agent/node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -8804,18 +10350,6 @@ } } }, - "node_modules/@nx/module-federation/node_modules/@swc/helpers": { - "version": "0.5.23", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", - "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/@nx/module-federation/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -9867,43 +11401,6 @@ "node": ">=14.0.0" } }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -10857,6 +12354,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10873,6 +12371,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10889,6 +12388,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10905,6 +12405,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10921,6 +12422,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10937,6 +12439,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10953,6 +12456,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10969,6 +12473,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -10985,6 +12490,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11001,6 +12507,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11017,6 +12524,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11033,6 +12541,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11049,6 +12558,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11064,6 +12574,7 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11075,6 +12586,7 @@ "version": "1.9.2", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11085,6 +12597,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11095,6 +12608,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11113,6 +12627,7 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11126,6 +12641,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11142,6 +12658,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11155,6 +12672,58 @@ "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.16.tgz", "integrity": "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -11482,6 +13051,26 @@ "win32" ] }, + "node_modules/@rollup/wasm-node": { + "version": "4.62.3", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.62.3.tgz", + "integrity": "sha512-sEOQWamme1YgXeVcE1JkB1k44RwtPWcpYpGUW1Lso7bOj+Ay9lN2Pak/CcfNeC4xqIPNhYSabQNaWYkJJR7m8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/@rspack/binding": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-2.1.5.tgz", @@ -12164,16 +13753,6 @@ "@swc/counter": "^0.1.3" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", - "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/@ts-morph/common": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", @@ -12350,7 +13929,7 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/deep-eql": "*", @@ -12387,7 +13966,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@types/eslint": { @@ -12499,12 +14078,75 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -12631,6 +14273,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -13331,7 +14980,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", @@ -13349,7 +14998,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vitest/spy": "4.1.10", @@ -13376,7 +15025,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "tinyrainbow": "^3.1.0" @@ -13389,7 +15038,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vitest/utils": "4.1.10", @@ -13403,7 +15052,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.10", @@ -13419,7 +15068,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "devOptional": true, + "dev": true, "license": "MIT", "funding": { "url": "https://opencollective.com/vitest" @@ -13451,7 +15100,7 @@ "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vitest/pretty-format": "4.1.10", @@ -13466,7 +15115,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/ast": { @@ -13615,6 +15264,54 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@wry/caches": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", + "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/context": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", + "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/equality": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", + "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@wry/trie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", + "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -13647,14 +15344,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/abbrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", @@ -13954,6 +15643,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/apollo-angular": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/apollo-angular/-/apollo-angular-14.1.0.tgz", + "integrity": "sha512-rE6gcY9pSsI6FuG0tNhoVGxVJ82qN6vQAtG0t2yfujWTJRW1YD6aZe85D5LyyADM1oXGYySuYQcTtWBqAAQ6XQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0", + "@apollo/client": "^4.0.1", + "graphql": "^16.0.0", + "rxjs": "^7.8.0" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -14008,7 +15715,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -14601,6 +16308,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -14827,7 +16547,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -14867,10 +16587,23 @@ "dev": true, "license": "MIT" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^5.0.0" @@ -15159,7 +16892,7 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/columnify": { @@ -15740,31 +17473,31 @@ "license": "CC0-1.0" }, "node_modules/cssstyle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", - "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "license": "MIT", "dependencies": { - "rrweb-cssom": "^0.6.0" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/data-urls": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", - "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/debug": { @@ -15893,6 +17626,16 @@ "node": ">= 0.8" } }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -15907,6 +17650,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -16004,20 +17748,6 @@ ], "license": "BSD-2-Clause" }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dev": true, - "license": "MIT", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -16307,7 +18037,7 @@ "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -16774,7 +18504,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" @@ -16836,6 +18566,37 @@ "node": ">=18.0.0" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/exit-x": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", @@ -16868,12 +18629,30 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, + "node_modules/expect/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -17216,6 +18995,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-cache-directory": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz", + "integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-directory/node_modules/pkg-dir": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz", + "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -17230,6 +19042,19 @@ "node": ">=8" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -17640,6 +19465,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -17704,6 +19530,19 @@ "node": ">= 0.4" } }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-them-args": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/get-them-args/-/get-them-args-1.3.2.tgz", @@ -17838,12 +19677,68 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/graphql": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", + "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", + "license": "MIT", + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } + }, + "node_modules/graphql-tag": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz", + "integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/harmony-reflect": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", @@ -18090,31 +19985,27 @@ } }, "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/http-proxy-middleware": { @@ -18175,6 +20066,16 @@ "node": ">= 20" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -18314,6 +20215,39 @@ "node": ">=4" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -18352,6 +20286,16 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/injection-js": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.6.1.tgz", + "integrity": "sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + } + }, "node_modules/ip-address": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", @@ -18582,6 +20526,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -18749,6 +20706,48 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", + "import-local": "^3.2.0", + "jest-cli": "30.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.3.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-circus": { "version": "30.4.2", "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", @@ -18781,6 +20780,896 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-circus/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/@jest/console": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.3.0", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/globals": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/test-result": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/test-sequencer": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/babel-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.3.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/jest-cli/node_modules/babel-jest/node_modules/babel-preset-jest": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/jest-cli/node_modules/babel-plugin-jest-hoist": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-cli/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-circus": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "p-limit": "^3.1.0", + "pretty-format": "30.3.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-config": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.3.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "parse-json": "^5.2.0", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-each": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "jest-util": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-environment-node": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-cli/node_modules/jest-leak-detector": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-resolve": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-runner": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-runtime": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-validate": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.3.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-watcher": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.3.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-cli/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/jest-config": { "version": "30.4.2", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", @@ -18832,6 +21721,24 @@ } } }, + "node_modules/jest-config/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-diff": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", @@ -18878,6 +21785,201 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-each/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-environment-node": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", @@ -18897,6 +21999,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-haste-map": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", @@ -18922,6 +22042,24 @@ "fsevents": "^2.3.3" } }, + "node_modules/jest-haste-map/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-leak-detector": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", @@ -18974,6 +22112,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-message-util/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-mock": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", @@ -18989,6 +22145,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-mock/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -19007,6 +22181,566 @@ } } }, + "node_modules/jest-preset-angular": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/jest-preset-angular/-/jest-preset-angular-17.0.0.tgz", + "integrity": "sha512-2yAHkA1c5rSICGJVtLYYqPC5RDsvo4+i4CwWFHVXwv41cHNX7gCWeh074IuZ5mFw7Vwsr+i25EowCnGtKkxNWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment-jsdom-abstract": "^30.4.1", + "bs-logger": "^0.2.6", + "esbuild-wasm": ">=0.28.0", + "jest-util": "^30.4.1", + "pretty-format": "^30.4.1", + "ts-jest": "^29.4.11" + }, + "engines": { + "node": "^20.11.1 || >=22.0.0" + }, + "optionalDependencies": { + "esbuild": ">=0.28.0" + }, + "peerDependencies": { + "@angular/compiler-cli": ">=20.0.0 <23.0.0", + "@angular/core": ">=20.0.0 <23.0.0", + "@angular/platform-browser": ">=20.0.0 <23.0.0", + "jest": "^30.0.0", + "jsdom": ">=26.0.0", + "typescript": ">=5.8" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-preset-angular/node_modules/@jest/environment-jsdom-abstract": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.4.1.tgz", + "integrity": "sha512-dSlKrqug3siYNHVnjwIldShY12wAH3spwRltO/+8VOjg0X+xEq7vOs3DbBs4LRKsu7OH+NUb9kuZUNBF9Ho3TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-preset-angular/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/jest-preset-angular/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-regex-util": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", @@ -19037,6 +22771,371 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-resolve-dependencies": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/snapshot-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/transform": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.3.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.3.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve-dependencies/node_modules/expect": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-diff": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-haste-map": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-matcher-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-snapshot": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/jest-worker": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.3.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-runner": { "version": "30.4.2", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", @@ -19071,6 +23170,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runner/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -19126,6 +23243,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-snapshot": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", @@ -19159,7 +23294,7 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util": { + "node_modules/jest-snapshot/node_modules/jest-util": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", @@ -19177,6 +23312,80 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-validate": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", @@ -19228,6 +23437,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-watcher/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-worker": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", @@ -19245,6 +23472,24 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-worker/node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -19261,11 +23506,67 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jest/node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -19312,41 +23613,38 @@ } }, "node_modules/jsdom": { - "version": "22.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", - "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "cssstyle": "^3.0.0", - "data-urls": "^4.0.0", - "decimal.js": "^10.4.3", - "domexception": "^4.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.4", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.6.0", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^12.0.1", - "ws": "^8.13.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -19355,16 +23653,13 @@ } }, "node_modules/jsdom/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/jsdom/node_modules/entities": { @@ -19380,18 +23675,44 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/jsdom/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" + } + }, + "node_modules/jsdom/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/jsdom/node_modules/parse5": { @@ -19407,6 +23728,20 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/jsdom/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -19700,6 +24035,7 @@ "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -19732,6 +24068,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19752,6 +24089,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19772,6 +24110,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19792,6 +24131,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19812,6 +24152,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19832,6 +24173,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19852,6 +24194,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19872,6 +24215,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19892,6 +24236,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19912,6 +24257,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -19932,6 +24278,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -20125,6 +24472,12 @@ "node": ">=8" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -20322,6 +24675,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -20918,6 +25280,576 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/ng-packagr": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-22.0.2.tgz", + "integrity": "sha512-8qQ+RWm0W3gK8Sch1kmo3EzIUiCTXIOP2gS7b+vtqCMBf4KBjBP0wFBikW5qRau9lGOzRTML0GPFGoARPR4+fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/wasm-node": "^4.24.0", + "ajv": "^8.17.1", + "browserslist": "^4.26.0", + "chokidar": "^5.0.0", + "commander": "^14.0.0", + "dependency-graph": "^1.0.0", + "esbuild": "^0.28.0", + "find-cache-directory": "^6.0.0", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.3.1", + "less": "^4.2.0", + "ora": "^9.0.0", + "piscina": "^5.0.0", + "postcss": "^8.4.47", + "rollup-plugin-dts": "^6.4.0", + "rxjs": "^7.8.1", + "sass": "^1.81.0", + "tinyglobby": "^0.2.12" + }, + "bin": { + "ng-packagr": "src/cli/main.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "optionalDependencies": { + "rollup": "^4.24.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^22.0.0 || ^22.1.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/ng-packagr/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/ng-packagr/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/ngx-currency": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-currency/-/ngx-currency-19.0.0.tgz", + "integrity": "sha512-N7SdB8D+ttPAf9GNiMyhChPUJs0t0mRTMiJ2UILWN83Vnx9dGc1ytjFrV7q3pCVA66svl1bpSFOfbZ2rhJ7zHA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@angular/forms": "^19.0.0" + } + }, + "node_modules/ngx-infinite-scroll": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-22.0.0.tgz", + "integrity": "sha512-+4eiWB04iDE45W5chhy5698gPAih3LRhzZ/gG6Ellv/yKlrLp4XDrOG/V7nptIElXqw08RwbXPyXTxULrbIdsQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=22.0.0 <23.0.0", + "@angular/core": ">=22.0.0 <23.0.0" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -21618,7 +26550,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", - "devOptional": true, + "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" @@ -21717,6 +26649,18 @@ "opener": "bin/opener-bin.js" } }, + "node_modules/optimism": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", + "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", + "license": "MIT", + "dependencies": { + "@wry/caches": "^1.0.0", + "@wry/context": "^0.7.0", + "@wry/trie": "^0.5.0", + "tslib": "^2.3.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -22290,7 +27234,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -23256,19 +28200,6 @@ "license": "MIT", "optional": true }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -23330,13 +28261,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -23383,6 +28307,13 @@ "node": ">= 0.10" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/react-is-18": { "name": "react-is", "version": "18.3.1", @@ -23427,6 +28358,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -23547,6 +28479,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -23667,6 +28612,7 @@ "version": "1.0.0-rc.16", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.16.tgz", "integrity": "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.126.0", @@ -23700,6 +28646,7 @@ "version": "0.126.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.126.0.tgz", "integrity": "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -23749,6 +28696,39 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-dts": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.4.1.tgz", + "integrity": "sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.5.5", + "convert-source-map": "^2.0.0", + "magic-string": "^0.30.21" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.29.0" + }, + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0 || ^6.0" + } + }, + "node_modules/rollup-plugin-dts/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup/node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -23784,9 +28764,9 @@ } }, "node_modules/rrweb-cssom": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", - "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, @@ -23885,7 +28865,7 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.100.0.tgz", "integrity": "sha512-Ut8wlQSk19tm7jMK6mz6cF1+e+E7tUnW2tM02zQDPnOTcVbV8qCQG8UWxZkkNlY50+hV3hqP24OOkUlMz8xBpw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@bufbuild/protobuf": "^2.5.0", @@ -23933,6 +28913,7 @@ "!riscv64", "!x64" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -23943,6 +28924,7 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -23967,6 +28949,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -23983,6 +28966,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -23999,6 +28983,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24015,6 +29000,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24031,6 +29017,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24047,6 +29034,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24063,6 +29051,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24079,6 +29068,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24095,6 +29085,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24111,6 +29102,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24127,6 +29119,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24143,6 +29136,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24159,6 +29153,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24175,6 +29170,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24188,6 +29184,7 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.100.0.tgz", "integrity": "sha512-c+naBgWId4MIpToXcI0DgqetjdAkwTTAxFAuOaBz7HUXLdyG1oZRrEvSsbe41nEdQOKH0vgofVFCeSQgoXOG9A==", + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24204,6 +29201,7 @@ "version": "1.100.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.100.0.tgz", "integrity": "sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -24228,6 +29226,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24244,6 +29243,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -24257,7 +29257,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -24664,7 +29664,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/signal-exit": { @@ -25011,7 +30011,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/statuses": { @@ -25027,7 +30027,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/stdin-discarder": { @@ -25172,6 +30172,16 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -25370,7 +30380,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "sync-message-port": "^1.0.0" @@ -25383,7 +30393,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=16.0.0" @@ -25705,14 +30715,14 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -25738,12 +30748,32 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -25793,42 +30823,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", "dependencies": { - "punycode": "^2.3.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/tree-dump": { @@ -25860,6 +30877,85 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-loader": { "version": "9.6.2", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz", @@ -26079,6 +31175,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -26112,6 +31209,20 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici": { "version": "6.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", @@ -26284,17 +31395,6 @@ "dev": true, "license": "MIT" }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -26363,7 +31463,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/vary": { @@ -26379,6 +31479,7 @@ "version": "8.0.9", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.9.tgz", "integrity": "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -26494,27 +31595,11 @@ } } }, - "node_modules/vite-tsconfig-paths/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@vitest/expect": "4.1.10", @@ -26601,16 +31686,16 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/walker": { @@ -27209,27 +32294,27 @@ } }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", - "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^4.1.1", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/which": { @@ -27252,7 +32337,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "siginfo": "^2.0.0", @@ -27281,6 +32366,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -27458,13 +32550,13 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { @@ -27478,6 +32570,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -27493,7 +32586,7 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -27509,6 +32602,7 @@ "version": "18.0.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^9.0.1", @@ -27536,6 +32630,7 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -27548,6 +32643,7 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -27560,6 +32656,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^7.2.0", @@ -27574,12 +32671,14 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, "license": "MIT" }, "node_modules/yargs/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", @@ -27597,6 +32696,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -27612,6 +32712,7 @@ "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -27629,6 +32730,7 @@ "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, "license": "ISC", "engines": { "node": "^20.19.0 || ^22.12.0 || >=23" diff --git a/package.json b/package.json index a73d52b..4597f7e 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,32 @@ "private": true, "dependencies": { "@angular-devkit/build-angular": "22.0.6", + "@angular/animations": "^22.0.6", + "@angular/cdk": "^22.0.6", "@angular/common": "22.0.6", "@angular/compiler": "22.0.6", "@angular/core": "22.0.6", "@angular/forms": "22.0.6", + "@angular/material": "^22.0.6", "@angular/platform-browser": "22.0.6", + "@angular/platform-browser-dynamic": "22.0.6", "@angular/platform-server": "22.0.6", "@angular/router": "22.0.6", "@angular/ssr": "22.0.6", + "@apollo/client": "^4.2.8", + "@ngrx/effects": "^21.1.1", + "@ngrx/router-store": "^21.1.1", + "@ngrx/store": "^21.1.1", + "@ngx-translate/core": "^18.0.0", + "@ngx-translate/http-loader": "^18.0.0", + "apollo-angular": "^14.1.0", + "chart.js": "^4.5.1", "express": "^4.21.2", + "graphql": "^17.0.2", + "lodash": "^4.18.1", + "luxon": "^3.7.2", + "ngx-currency": "^19.0.0", + "ngx-infinite-scroll": "^22.0.0", "rxjs": "~7.8.0", "zone.js": "0.16.2" }, @@ -34,6 +51,7 @@ "@nx/esbuild": "23.1.0", "@nx/eslint": "23.1.0", "@nx/eslint-plugin": "23.1.0", + "@nx/jest": "23.1.0", "@nx/js": "23.1.0", "@nx/node": "23.1.0", "@nx/playwright": "23.1.0", @@ -48,22 +66,33 @@ "@swc/core": "1.15.8", "@swc/helpers": "0.5.18", "@types/express": "^4.17.21", + "@types/jest": "~30.0.0", + "@types/lodash": "^4.17.24", + "@types/luxon": "^3.7.2", "@types/node": "^24.0.0", "@types/supertest": "^6.0.3", "@typescript-eslint/utils": "^8.40.0", "@vitest/coverage-v8": "~4.1.0", "@vitest/ui": "~4.1.0", "angular-eslint": "22.1.0", + "autoprefixer": "^10.4.0", "esbuild": "^0.27.0", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-playwright": "^1.6.2", + "jest": "~30.3.0", + "jest-environment-jsdom": "~30.3.0", + "jest-preset-angular": "~17.0.0", + "jest-util": "~30.3.0", "jiti": "2.4.2", - "jsdom": "~22.1.0", + "jsdom": "^26.0.0", "jsonc-eslint-parser": "^2.1.0", + "ng-packagr": "~22.0.0", "nx": "23.1.0", + "postcss": "^8.4.5", "prettier": "^3.8.1", "supertest": "^7.1.4", + "ts-jest": "^29.4.7", "ts-node": "10.9.1", "tslib": "^2.3.0", "typescript": "6.0.3", diff --git a/packages/api/products/README.md b/packages/api/products/README.md deleted file mode 100644 index cce1416..0000000 --- a/packages/api/products/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# products - -This library was generated with [Nx](https://nx.dev). - -## Building - -Run `nx build products` to build the library. - -## Running unit tests - -Run `nx test products` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/api/products/eslint.config.mjs b/packages/api/products/eslint.config.mjs deleted file mode 100644 index db9cad2..0000000 --- a/packages/api/products/eslint.config.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import baseConfig from '../../../eslint.config.mjs'; - -export default [ - ...baseConfig, - { - files: ['**/*.json'], - rules: { - '@nx/dependency-checks': [ - 'error', - { - ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'], - }, - ], - }, - languageOptions: { - parser: await import('jsonc-eslint-parser'), - }, - }, -]; diff --git a/packages/api/products/package.json b/packages/api/products/package.json deleted file mode 100644 index 4ea0dff..0000000 --- a/packages/api/products/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@org/products", - "version": "0.0.1", - "private": true, - "type": "commonjs", - "main": "./src/index.js", - "types": "./src/index.d.ts", - "dependencies": { - "tslib": "^2.3.0" - } -} diff --git a/packages/api/products/project.json b/packages/api/products/project.json deleted file mode 100644 index 2be9aa9..0000000 --- a/packages/api/products/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "products", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/api/products/src", - "projectType": "library", - "tags": ["scope:api"], - "targets": {} -} diff --git a/packages/api/products/src/index.ts b/packages/api/products/src/index.ts deleted file mode 100644 index 4cafdcb..0000000 --- a/packages/api/products/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/products.service'; diff --git a/packages/api/products/src/lib/products.service.spec.ts b/packages/api/products/src/lib/products.service.spec.ts deleted file mode 100644 index 35e8fab..0000000 --- a/packages/api/products/src/lib/products.service.spec.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ProductsService } from './products.service'; -import { ProductFilter } from '@org/models'; - -describe('ProductsService', () => { - let service: ProductsService; - - beforeEach(() => { - service = new ProductsService(); - }); - - it('should create', () => { - expect(service).toBeTruthy(); - }); - - describe('getAllProducts', () => { - it('should return paginated products with default pagination', () => { - const result = service.getAllProducts(); - - expect(result).toHaveProperty('items'); - expect(result).toHaveProperty('total'); - expect(result).toHaveProperty('page'); - expect(result).toHaveProperty('pageSize'); - expect(result.page).toBe(1); - expect(result.pageSize).toBe(12); - expect(result.items.length).toBeLessThanOrEqual(12); - }); - - it('should filter products by category', () => { - const filter: ProductFilter = { category: 'Electronics' }; - const result = service.getAllProducts(filter); - - result.items.forEach((product) => { - expect(product.category).toBe('Electronics'); - }); - }); - - it('should filter products by price range', () => { - const filter: ProductFilter = { minPrice: 50, maxPrice: 150 }; - const result = service.getAllProducts(filter); - - result.items.forEach((product) => { - expect(product.price).toBeGreaterThanOrEqual(50); - expect(product.price).toBeLessThanOrEqual(150); - }); - }); - - it('should handle search term filtering', () => { - const filter: ProductFilter = { searchTerm: 'Product 1' }; - const result = service.getAllProducts(filter); - - result.items.forEach((product) => { - const matchesSearch = - product.name.toLowerCase().includes('product 1') || - product.description.toLowerCase().includes('product 1'); - expect(matchesSearch).toBe(true); - }); - }); - - it('should paginate results correctly', () => { - const page1 = service.getAllProducts(undefined, 1, 5); - const page2 = service.getAllProducts(undefined, 2, 5); - - expect(page1.items.length).toBeLessThanOrEqual(5); - expect(page2.items.length).toBeLessThanOrEqual(5); - expect(page1.items[0]?.id).not.toBe(page2.items[0]?.id); - }); - }); - - describe('getProductById', () => { - it('should return a product by id', () => { - const product = service.getProductById('prod-1'); - - expect(product).toBeTruthy(); - expect(product?.id).toBe('prod-1'); - }); - - it('should return null for non-existent id', () => { - const product = service.getProductById('non-existent'); - - expect(product).toBeNull(); - }); - }); - - describe('getCategories', () => { - it('should return unique categories', () => { - const categories = service.getCategories(); - - expect(categories).toBeTruthy(); - expect(Array.isArray(categories)).toBe(true); - expect(categories.length).toBeGreaterThan(0); - - const uniqueCategories = new Set(categories); - expect(uniqueCategories.size).toBe(categories.length); - }); - }); - - describe('getPriceRange', () => { - it('should return min and max prices', () => { - const range = service.getPriceRange(); - - expect(range).toHaveProperty('min'); - expect(range).toHaveProperty('max'); - expect(range.min).toBeLessThanOrEqual(range.max); - expect(range.min).toBeGreaterThanOrEqual(0); - }); - }); -}); \ No newline at end of file diff --git a/packages/api/products/src/lib/products.service.ts b/packages/api/products/src/lib/products.service.ts deleted file mode 100644 index 02c851d..0000000 --- a/packages/api/products/src/lib/products.service.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Product, ProductFilter, PaginatedResponse } from '@org/models'; - -export class ProductsService { - private products: Product[] = this.generateMockProducts(); - - private generateMockProducts(): Product[] { - const categories = ['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports']; - const products: Product[] = []; - - for (let i = 1; i <= 50; i++) { - const category = categories[Math.floor(Math.random() * categories.length)]; - products.push({ - id: `prod-${i}`, - name: `Product ${i}`, - description: `This is a high-quality ${category.toLowerCase()} product with excellent features and great value for money.`, - price: Math.round(Math.random() * 500 * 100) / 100 + 10, - category, - imageUrl: `https://placehold.co/300x300?text=Product+${i}`, - inStock: Math.random() > 0.2, - rating: Math.round((Math.random() * 2 + 3) * 10) / 10, - reviewCount: Math.floor(Math.random() * 500), - }); - } - - return products; - } - - getAllProducts( - filter?: ProductFilter, - page = 1, - pageSize = 12 - ): PaginatedResponse { - let filteredProducts = [...this.products]; - - if (filter) { - if (filter.category) { - filteredProducts = filteredProducts.filter( - p => p.category === filter.category - ); - } - - if (filter.minPrice !== undefined) { - const minPrice = filter.minPrice; - filteredProducts = filteredProducts.filter(p => p.price >= minPrice); - } - - if (filter.maxPrice !== undefined) { - const maxPrice = filter.maxPrice; - filteredProducts = filteredProducts.filter(p => p.price <= maxPrice); - } - - if (filter.inStock !== undefined) { - filteredProducts = filteredProducts.filter( - p => p.inStock === filter.inStock - ); - } - - if (filter.searchTerm) { - const searchLower = filter.searchTerm.toLowerCase(); - filteredProducts = filteredProducts.filter( - p => - p.name.toLowerCase().includes(searchLower) || - p.description.toLowerCase().includes(searchLower) || - p.category.toLowerCase().includes(searchLower) - ); - } - } - - const total = filteredProducts.length; - const totalPages = Math.ceil(total / pageSize); - const startIndex = (page - 1) * pageSize; - const endIndex = startIndex + pageSize; - const items = filteredProducts.slice(startIndex, endIndex); - - return { - items, - total, - page, - pageSize, - totalPages, - }; - } - - getProductById(id: string): Product | null { - return this.products.find(p => p.id === id) || null; - } - - getCategories(): string[] { - return [...new Set(this.products.map(p => p.category))]; - } - - getPriceRange(): { min: number; max: number } { - const prices = this.products.map(p => p.price); - return { - min: Math.min(...prices), - max: Math.max(...prices), - }; - } -} \ No newline at end of file diff --git a/packages/api/products/tsconfig.json b/packages/api/products/tsconfig.json deleted file mode 100644 index 25f7201..0000000 --- a/packages/api/products/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs" - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/api/products/tsconfig.lib.json b/packages/api/products/tsconfig.lib.json deleted file mode 100644 index 4befa7f..0000000 --- a/packages/api/products/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/packages/api/products/tsconfig.spec.json b/packages/api/products/tsconfig.spec.json deleted file mode 100644 index 69d6af2..0000000 --- a/packages/api/products/tsconfig.spec.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ] - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ] -} diff --git a/packages/api/products/vite.config.ts b/packages/api/products/vite.config.ts deleted file mode 100644 index 696f5d2..0000000 --- a/packages/api/products/vite.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/api/products', - plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - test: { - name: 'products', - watch: false, - globals: true, - environment: 'node', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - passWithNoTests: true, - coverage: { - reportsDirectory: '../../../coverage/packages/api/products', - provider: 'v8' as const, - }, - }, -})); diff --git a/packages/shared/models/README.md b/packages/shared/models/README.md deleted file mode 100644 index 12e8a69..0000000 --- a/packages/shared/models/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# models - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test models` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/packages/shared/models/eslint.config.mjs b/packages/shared/models/eslint.config.mjs deleted file mode 100644 index de7b5ea..0000000 --- a/packages/shared/models/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import baseConfig from '../../../eslint.config.mjs'; - -export default [...baseConfig]; diff --git a/packages/shared/models/project.json b/packages/shared/models/project.json deleted file mode 100644 index 3dcd6cc..0000000 --- a/packages/shared/models/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "models", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/shared/models/src", - "projectType": "library", - "tags": ["scope:shared", "type:data"], - "targets": {} -} diff --git a/packages/shared/models/src/index.ts b/packages/shared/models/src/index.ts deleted file mode 100644 index 86cf29e..0000000 --- a/packages/shared/models/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/product.model'; diff --git a/packages/shared/models/src/lib/product.model.ts b/packages/shared/models/src/lib/product.model.ts deleted file mode 100644 index a04de50..0000000 --- a/packages/shared/models/src/lib/product.model.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface Product { - id: string; - name: string; - description: string; - price: number; - category: string; - imageUrl: string; - inStock: boolean; - rating: number; - reviewCount: number; -} - -export interface ApiResponse { - data: T; - success: boolean; - message?: string; - error?: string; -} - -export interface PaginatedResponse { - items: T[]; - total: number; - page: number; - pageSize: number; - totalPages: number; -} - -export interface ProductFilter { - category?: string; - minPrice?: number; - maxPrice?: number; - inStock?: boolean; - searchTerm?: string; -} \ No newline at end of file diff --git a/packages/shared/models/tsconfig.json b/packages/shared/models/tsconfig.json deleted file mode 100644 index 86622ac..0000000 --- a/packages/shared/models/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "importHelpers": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/shared/models/tsconfig.lib.json b/packages/shared/models/tsconfig.lib.json deleted file mode 100644 index 190d53f..0000000 --- a/packages/shared/models/tsconfig.lib.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx" - ] -} diff --git a/packages/shared/models/tsconfig.spec.json b/packages/shared/models/tsconfig.spec.json deleted file mode 100644 index 69d6af2..0000000 --- a/packages/shared/models/tsconfig.spec.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ] - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ] -} diff --git a/packages/shared/models/vite.config.ts b/packages/shared/models/vite.config.ts deleted file mode 100644 index b2182df..0000000 --- a/packages/shared/models/vite.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vite'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/shared/models', - plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'models', - watch: false, - globals: true, - environment: 'node', - include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - reporters: ['default'], - passWithNoTests: true, - coverage: { - reportsDirectory: '../../../coverage/packages/shared/models', - provider: 'v8' as const, - }, - }, -})); diff --git a/packages/shop/data/eslint.config.mjs b/packages/shop/data/eslint.config.mjs deleted file mode 100644 index 3bf3b5a..0000000 --- a/packages/shop/data/eslint.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import nx from '@nx/eslint-plugin'; -import baseConfig from '../../../eslint.config.mjs'; - -export default [ - ...baseConfig, - ...nx.configs['flat/angular'], - ...nx.configs['flat/angular-template'], - { - files: ['**/*.ts'], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: 'lib', - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: 'lib', - style: 'kebab-case', - }, - ], - }, - }, - { - files: ['**/*.html'], - // Override or add rules here - rules: {}, - }, -]; diff --git a/packages/shop/data/project.json b/packages/shop/data/project.json deleted file mode 100644 index b676f63..0000000 --- a/packages/shop/data/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "data", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/shop/data/src", - "prefix": "lib", - "projectType": "library", - "tags": ["scope:shop", "type:data"], - "targets": {} -} diff --git a/packages/shop/data/src/index.ts b/packages/shop/data/src/index.ts deleted file mode 100644 index 5e82b7e..0000000 --- a/packages/shop/data/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './lib/services/products.service'; diff --git a/packages/shop/data/src/lib/services/products.service.spec.ts b/packages/shop/data/src/lib/services/products.service.spec.ts deleted file mode 100644 index ce665fe..0000000 --- a/packages/shop/data/src/lib/services/products.service.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing'; -import { ProductsService } from './products.service'; -import { Product, ApiResponse, PaginatedResponse, ProductFilter } from '@org/models'; - -describe('ProductsService', () => { - let service: ProductsService; - let httpMock: HttpTestingController; - const apiUrl = 'http://localhost:3333/api'; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - provideHttpClient(), - provideHttpClientTesting(), - ProductsService - ], - }); - service = TestBed.inject(ProductsService); - httpMock = TestBed.inject(HttpTestingController); - }); - - afterEach(() => { - httpMock.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - describe('getProducts', () => { - const mockProductsResponse: ApiResponse> = { - success: true, - data: { - items: [ - { - id: '1', - name: 'Product 1', - description: 'Description 1', - price: 100, - imageUrl: 'image1.jpg', - category: 'Electronics', - inStock: true, - rating: 4.5, - reviewCount: 10, - }, - { - id: '2', - name: 'Product 2', - description: 'Description 2', - price: 200, - imageUrl: 'image2.jpg', - category: 'Clothing', - inStock: false, - rating: 3.5, - reviewCount: 5, - }, - ], - total: 2, - page: 1, - pageSize: 12, - totalPages: 1, - }, - }; - - it('should return products with default pagination', () => { - service.getProducts().subscribe((response) => { - expect(response.items.length).toBe(2); - expect(response.total).toBe(2); - expect(response.page).toBe(1); - expect(service.loading()).toBeFalsy(); - expect(service.error()).toBeNull(); - }); - - const req = httpMock.expectOne(`${apiUrl}/products?page=1&pageSize=12`); - expect(req.request.method).toBe('GET'); - req.flush(mockProductsResponse); - }); - - it('should apply filters when provided', () => { - const filter: ProductFilter = { - category: 'Electronics', - minPrice: 50, - maxPrice: 150, - inStock: true, - searchTerm: 'test', - }; - - service.getProducts(filter, 2, 20).subscribe((response) => { - expect(response).toBeTruthy(); - }); - - const req = httpMock.expectOne( - `${apiUrl}/products?page=2&pageSize=20&category=Electronics&minPrice=50&maxPrice=150&inStock=true&searchTerm=test` - ); - expect(req.request.method).toBe('GET'); - req.flush(mockProductsResponse); - }); - - it('should handle error response', () => { - const errorResponse: ApiResponse> = { - success: false, - error: 'Server error', - data: undefined as unknown - }; - - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - service.getProducts().subscribe((response) => { - expect(response.items).toEqual([]); - expect(response.total).toBe(0); - expect(service.error()).toContain('Server error'); - }); - - const req = httpMock.expectOne(`${apiUrl}/products?page=1&pageSize=12`); - req.flush(errorResponse); - - consoleErrorSpy.mockRestore(); - }); - - it('should handle network error', () => { - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - service.getProducts().subscribe((response) => { - expect(response.items).toEqual([]); - expect(response.total).toBe(0); - expect(service.error()).toBeTruthy(); - }); - - const req = httpMock.expectOne(`${apiUrl}/products?page=1&pageSize=12`); - req.error(new ProgressEvent('Network error')); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe('getProductById', () => { - const mockProduct: Product = { - id: '1', - name: 'Product 1', - description: 'Description 1', - price: 100, - imageUrl: 'image1.jpg', - category: 'Electronics', - inStock: true, - rating: 4.5, - reviewCount: 10, - }; - - it('should return a product by id', () => { - const mockResponse: ApiResponse = { - success: true, - data: mockProduct, - }; - - service.getProductById('1').subscribe((product) => { - expect(product).toEqual(mockProduct); - expect(service.loading()).toBeFalsy(); - expect(service.error()).toBeNull(); - }); - - const req = httpMock.expectOne(`${apiUrl}/products/1`); - expect(req.request.method).toBe('GET'); - req.flush(mockResponse); - }); - - it('should return null on error', () => { - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - service.getProductById('1').subscribe((product) => { - expect(product).toBeNull(); - expect(service.error()).toBeTruthy(); - }); - - const req = httpMock.expectOne(`${apiUrl}/products/1`); - req.error(new ProgressEvent('Network error')); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe('getCategories', () => { - it('should return categories list', () => { - const mockCategories = ['Electronics', 'Clothing', 'Books']; - const mockResponse: ApiResponse = { - success: true, - data: mockCategories, - }; - - service.getCategories().subscribe((categories) => { - expect(categories).toEqual(mockCategories); - }); - - const req = httpMock.expectOne(`${apiUrl}/products-metadata/categories`); - expect(req.request.method).toBe('GET'); - req.flush(mockResponse); - }); - - it('should return empty array on error', () => { - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - service.getCategories().subscribe((categories) => { - expect(categories).toEqual([]); - }); - - const req = httpMock.expectOne(`${apiUrl}/products-metadata/categories`); - req.error(new ProgressEvent('Network error')); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe('getPriceRange', () => { - it('should return price range', () => { - const mockPriceRange = { min: 10, max: 500 }; - const mockResponse: ApiResponse<{ min: number; max: number }> = { - success: true, - data: mockPriceRange, - }; - - service.getPriceRange().subscribe((range) => { - expect(range).toEqual(mockPriceRange); - }); - - const req = httpMock.expectOne(`${apiUrl}/products-metadata/price-range`); - expect(req.request.method).toBe('GET'); - req.flush(mockResponse); - }); - - it('should return default range on error', () => { - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - - service.getPriceRange().subscribe((range) => { - expect(range).toEqual({ min: 0, max: 1000 }); - }); - - const req = httpMock.expectOne(`${apiUrl}/products-metadata/price-range`); - req.error(new ProgressEvent('Network error')); - - consoleErrorSpy.mockRestore(); - }); - }); - - describe('loading and error signals', () => { - it('should set loading to true when fetching products', () => { - expect(service.loading()).toBeFalsy(); - - service.getProducts().subscribe(); - expect(service.loading()).toBeTruthy(); - - const req = httpMock.expectOne(`${apiUrl}/products?page=1&pageSize=12`); - req.flush({ success: true, data: { items: [], total: 0, page: 1, pageSize: 12, totalPages: 0 } }); - - expect(service.loading()).toBeFalsy(); - }); - - it('should set error message on failure', () => { - // Silence console.error for this test - const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - expect(service.error()).toBeNull(); - - service.getProductById('1').subscribe(() => { - expect(service.error()).toBeTruthy(); - }); - - const req = httpMock.expectOne(`${apiUrl}/products/1`); - req.error(new ProgressEvent('Network error')); - - consoleErrorSpy.mockRestore(); - }); - }); -}); diff --git a/packages/shop/data/src/lib/services/products.service.ts b/packages/shop/data/src/lib/services/products.service.ts deleted file mode 100644 index b26fe89..0000000 --- a/packages/shop/data/src/lib/services/products.service.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { inject, Injectable, signal } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { Observable, map, catchError, of } from 'rxjs'; -import { Product, ApiResponse, PaginatedResponse, ProductFilter } from '@org/models'; - -@Injectable({ - providedIn: 'root', -}) -export class ProductsService { - private readonly http = inject(HttpClient); - private readonly apiUrl = 'http://localhost:3333/api'; - - // Signals for state management - private readonly loadingSignal = signal(false); - private readonly errorSignal = signal(null); - - readonly loading = this.loadingSignal.asReadonly(); - readonly error = this.errorSignal.asReadonly(); - - getProducts( - filter?: ProductFilter, - page = 1, - pageSize = 12 - ): Observable> { - this.loadingSignal.set(true); - this.errorSignal.set(null); - - let params = new HttpParams() - .set('page', page.toString()) - .set('pageSize', pageSize.toString()); - - if (filter) { - if (filter.category) { - params = params.set('category', filter.category); - } - if (filter.minPrice !== undefined) { - params = params.set('minPrice', filter.minPrice.toString()); - } - if (filter.maxPrice !== undefined) { - params = params.set('maxPrice', filter.maxPrice.toString()); - } - if (filter.inStock !== undefined) { - params = params.set('inStock', filter.inStock.toString()); - } - if (filter.searchTerm) { - params = params.set('searchTerm', filter.searchTerm); - } - } - - return this.http - .get>>(`${this.apiUrl}/products`, { - params, - }) - .pipe( - map((response) => { - this.loadingSignal.set(false); - if (!response.success) { - throw new Error(response.error || 'Failed to load products'); - } - return response.data; - }), - catchError((error) => { - this.loadingSignal.set(false); - this.errorSignal.set( - error.message || 'An error occurred while loading products' - ); - console.error('Error loading products:', error); - return of({ - items: [], - total: 0, - page: 1, - pageSize: 12, - totalPages: 0, - }); - }) - ); - } - - getProductById(id: string): Observable { - this.loadingSignal.set(true); - this.errorSignal.set(null); - - return this.http - .get>(`${this.apiUrl}/products/${id}`) - .pipe( - map((response) => { - this.loadingSignal.set(false); - if (!response.success) { - throw new Error(response.error || 'Failed to load product'); - } - return response.data; - }), - catchError((error) => { - this.loadingSignal.set(false); - this.errorSignal.set( - error.message || 'An error occurred while loading the product' - ); - console.error('Error loading product:', error); - return of(null); - }) - ); - } - - getCategories(): Observable { - return this.http - .get>(`${this.apiUrl}/products-metadata/categories`) - .pipe( - map((response) => { - if (!response.success) { - throw new Error(response.error || 'Failed to load categories'); - } - return response.data; - }), - catchError((error) => { - console.error('Error loading categories:', error); - return of([]); - }) - ); - } - - getPriceRange(): Observable<{ min: number; max: number }> { - return this.http - .get>( - `${this.apiUrl}/products-metadata/price-range` - ) - .pipe( - map((response) => { - if (!response.success) { - throw new Error(response.error || 'Failed to load price range'); - } - return response.data; - }), - catchError((error) => { - console.error('Error loading price range:', error); - return of({ min: 0, max: 1000 }); - }) - ); - } -} \ No newline at end of file diff --git a/packages/shop/data/src/test-setup.ts b/packages/shop/data/src/test-setup.ts deleted file mode 100644 index 9157d5f..0000000 --- a/packages/shop/data/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import '@angular/compiler'; -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() -); diff --git a/packages/shop/data/tsconfig.json b/packages/shop/data/tsconfig.json deleted file mode 100644 index 820e27c..0000000 --- a/packages/shop/data/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "target": "es2022", - "moduleResolution": "bundler", - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "module": "preserve", - "lib": ["dom", "es2022"] - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "typeCheckHostBindings": true, - "strictTemplates": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/shop/data/tsconfig.lib.json b/packages/shop/data/tsconfig.lib.json deleted file mode 100644 index 312ee86..0000000 --- a/packages/shop/data/tsconfig.lib.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "types": [] - }, - "exclude": [ - "src/**/*.spec.ts", - "src/test-setup.ts", - "jest.config.ts", - "src/**/*.test.ts", - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx" - ], - "include": ["src/**/*.ts"] -} diff --git a/packages/shop/data/tsconfig.spec.json b/packages/shop/data/tsconfig.spec.json deleted file mode 100644 index 3a4ffcc..0000000 --- a/packages/shop/data/tsconfig.spec.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ], - "isolatedModules": true - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ], - "files": ["src/test-setup.ts"] -} diff --git a/packages/shop/data/vite.config.mts b/packages/shop/data/vite.config.mts deleted file mode 100644 index d783d1f..0000000 --- a/packages/shop/data/vite.config.mts +++ /dev/null @@ -1,28 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import angular from '@analogjs/vite-plugin-angular'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/shop/data', - plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'data', - watch: false, - globals: true, - environment: 'jsdom', - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['src/test-setup.ts'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../../coverage/packages/shop/data', - provider: 'v8' as const, - }, - }, -})); diff --git a/packages/shop/feature-product-detail/project.json b/packages/shop/feature-product-detail/project.json deleted file mode 100644 index 277f74f..0000000 --- a/packages/shop/feature-product-detail/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "feature-product-detail", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/shop/feature-product-detail/src", - "prefix": "shop", - "tags": ["scope:shop", "type:feature"], - "targets": {} -} diff --git a/packages/shop/feature-product-detail/src/index.ts b/packages/shop/feature-product-detail/src/index.ts deleted file mode 100644 index 46be25d..0000000 --- a/packages/shop/feature-product-detail/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/product-detail/product-detail.component'; -export * from './lib/feature-product-detail.routes'; \ No newline at end of file diff --git a/packages/shop/feature-product-detail/src/lib/feature-product-detail.routes.ts b/packages/shop/feature-product-detail/src/lib/feature-product-detail.routes.ts deleted file mode 100644 index 00e9f62..0000000 --- a/packages/shop/feature-product-detail/src/lib/feature-product-detail.routes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from '@angular/router'; -import { ProductDetailComponent } from './product-detail/product-detail.component'; - -export const featureProductDetailRoutes: Routes = [ - { - path: ':id', - component: ProductDetailComponent, - }, -]; \ No newline at end of file diff --git a/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.spec.ts b/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.spec.ts deleted file mode 100644 index 2a24ee5..0000000 --- a/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; -import { of, throwError } from 'rxjs'; -import { ProductDetailComponent } from './product-detail.component'; -import { ProductsService } from '@org/shop/data'; -import { Product } from '@org/models'; -import { describe, it, beforeEach, expect, vi } from 'vitest'; - -describe('ProductDetailComponent', () => { - let component: ProductDetailComponent; - let fixture: ComponentFixture; - let mockProductsService: Partial; - let mockRouter: Partial; - let mockActivatedRoute: Partial; - - const mockProduct: Product = { - id: '1', - name: 'Test Product', - description: 'Test Description', - price: 99.99, - imageUrl: 'https://example.com/image.jpg', - category: 'Electronics', - inStock: true, - rating: 4.5, - reviewCount: 100, - }; - - beforeEach(async () => { - mockProductsService = { - getProductById: vi.fn(), - }; - - mockRouter = { - navigate: vi.fn(), - }; - - mockActivatedRoute = { - snapshot: { - paramMap: { - get: vi.fn().mockReturnValue('1'), - }, - }, - }; - - await TestBed.configureTestingModule({ - imports: [ProductDetailComponent], - providers: [ - { provide: ProductsService, useValue: mockProductsService }, - { provide: Router, useValue: mockRouter }, - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ProductDetailComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should load product on init', () => { - mockProductsService.getProductById.mockReturnValue(of(mockProduct)); - - component.ngOnInit(); - - expect(mockProductsService.getProductById).toHaveBeenCalledWith('1'); - expect(component.product()).toEqual(mockProduct); - expect(component.loading()).toBe(false); - expect(component.error()).toBe(null); - }); - - it('should handle error when product not found', () => { - mockProductsService.getProductById.mockReturnValue(of(null)); - - component.ngOnInit(); - - expect(component.error()).toBe('Product not found'); - expect(component.loading()).toBe(false); - }); - - it('should handle error when loading fails', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); - mockProductsService.getProductById.mockReturnValue( - throwError(() => new Error('Network error')) - ); - - component.ngOnInit(); - - expect(component.error()).toBe('Failed to load product details'); - expect(component.loading()).toBe(false); - consoleSpy.mockRestore(); - }); - - it('should calculate star ratings correctly', () => { - component.product.set(mockProduct); - - const stars = component.getStars(); - - expect(stars).toEqual([true, true, true, true, true]); - }); - - it('should handle add to cart action', () => { - const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined); - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined); - component.product.set(mockProduct); - - component.addToCart(); - - expect(consoleSpy).toHaveBeenCalledWith('Adding to cart:', '1'); - expect(alertSpy).toHaveBeenCalledWith('Product added to cart!'); - }); -}); diff --git a/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.ts b/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.ts deleted file mode 100644 index d0431ae..0000000 --- a/packages/shop/feature-product-detail/src/lib/product-detail/product-detail.component.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { Component, inject, signal, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule, CurrencyPipe } from '@angular/common'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { ProductsService } from '@org/shop/data'; -import { Product } from '@org/models'; -import { LoadingSpinnerComponent, ErrorMessageComponent } from '@org/shop/shared-ui'; - -@Component({ - selector: 'shop-product-detail', - imports: [ - CommonModule, - CurrencyPipe, - RouterLink, - LoadingSpinnerComponent, - ErrorMessageComponent, - ], - template: ` -
- @if (loading()) { - - } @else if (error()) { - - } @else if (product()) { - - -
-
- - @if (!product()!.inStock) { -
Out of Stock
- } -
- -
-
{{ product()!.category }}
-

{{ product()!.name }}

- -
- - @for (star of getStars(); track $index) { - ★ - } - - - {{ product()!.rating }} out of 5 - - - ({{ product()!.reviewCount }} reviews) - -
- -
- {{ product()!.price | currency }} -
- -
-

Description

-

{{ product()!.description }}

-
- -
- @if (product()!.inStock) { - - - } @else { - - } -
- -
-

Product Information

-
-
Product ID:
-
{{ product()!.id }}
-
Category:
-
{{ product()!.category }}
-
Availability:
-
{{ product()!.inStock ? 'In Stock' : 'Out of Stock' }}
-
-
-
-
- } -
- `, - styles: [` - .product-detail-container { - max-width: 1200px; - margin: 0 auto; - padding: 24px; - } - - .breadcrumb { - margin-bottom: 24px; - } - - .breadcrumb a { - color: #3498db; - text-decoration: none; - font-size: 0.95rem; - transition: color 0.3s; - } - - .breadcrumb a:hover { - color: #2980b9; - text-decoration: underline; - } - - .product-detail { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 48px; - background: white; - border-radius: 8px; - overflow: hidden; - } - - .product-image-section { - position: relative; - } - - .product-image { - width: 100%; - height: auto; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } - - .out-of-stock-badge { - position: absolute; - top: 16px; - right: 16px; - background: rgba(220, 53, 69, 0.9); - color: white; - padding: 8px 16px; - border-radius: 4px; - font-weight: bold; - } - - .product-info-section { - display: flex; - flex-direction: column; - gap: 24px; - } - - .product-category { - color: #6c757d; - font-size: 0.95rem; - text-transform: uppercase; - letter-spacing: 1px; - } - - .product-name { - font-size: 2rem; - margin: 0; - color: #333; - } - - .product-rating { - display: flex; - align-items: center; - gap: 12px; - } - - .stars { - color: #ddd; - font-size: 1.25rem; - } - - .stars .filled { - color: #ffd700; - } - - .rating-text { - font-weight: 600; - color: #333; - } - - .review-count { - color: #666; - font-size: 0.95rem; - } - - .product-price { - font-size: 2rem; - font-weight: bold; - color: #28a745; - } - - .product-description { - border-top: 1px solid #e0e0e0; - padding-top: 24px; - } - - .product-description h2 { - font-size: 1.25rem; - margin-bottom: 12px; - color: #333; - } - - .product-description p { - color: #666; - line-height: 1.6; - } - - .product-actions { - display: flex; - gap: 16px; - } - - .btn-primary { - flex: 1; - padding: 12px 24px; - background: #3498db; - color: white; - border: none; - border-radius: 4px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: background 0.3s; - } - - .btn-primary:hover { - background: #2980b9; - } - - .btn-secondary { - flex: 1; - padding: 12px 24px; - background: transparent; - color: #3498db; - border: 2px solid #3498db; - border-radius: 4px; - font-size: 1rem; - font-weight: 600; - cursor: pointer; - transition: all 0.3s; - } - - .btn-secondary:hover { - background: #3498db; - color: white; - } - - .btn-disabled { - flex: 1; - padding: 12px 24px; - background: #6c757d; - color: white; - border: none; - border-radius: 4px; - font-size: 1rem; - font-weight: 600; - cursor: not-allowed; - opacity: 0.6; - } - - .product-info { - background: #f8f9fa; - padding: 20px; - border-radius: 8px; - } - - .product-info h3 { - font-size: 1.1rem; - margin-bottom: 16px; - color: #333; - } - - .product-info dl { - display: grid; - grid-template-columns: 120px 1fr; - gap: 12px; - margin: 0; - } - - .product-info dt { - font-weight: 600; - color: #666; - } - - .product-info dd { - margin: 0; - color: #333; - } - - @media (max-width: 768px) { - .product-detail { - grid-template-columns: 1fr; - gap: 32px; - } - - .product-name { - font-size: 1.5rem; - } - - .product-price { - font-size: 1.5rem; - } - - .product-actions { - flex-direction: column; - } - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ProductDetailComponent implements OnInit { - private readonly route = inject(ActivatedRoute); - private readonly router = inject(Router); - private readonly productsService = inject(ProductsService); - - // State signals - readonly product = signal(null); - readonly loading = signal(false); - readonly error = signal(null); - - ngOnInit() { - this.loadProduct(); - } - - loadProduct() { - const productId = this.route.snapshot.paramMap.get('id'); - - if (!productId) { - this.error.set('Product ID not provided'); - return; - } - - this.loading.set(true); - this.error.set(null); - - this.productsService.getProductById(productId).subscribe({ - next: (product) => { - if (product) { - this.product.set(product); - } else { - this.error.set('Product not found'); - } - this.loading.set(false); - }, - error: (err) => { - this.error.set('Failed to load product details'); - this.loading.set(false); - console.error('Error loading product:', err); - }, - }); - } - - getStars(): boolean[] { - const product = this.product(); - if (!product) return []; - - const rating = product.rating; - const fullStars = Math.floor(rating); - const hasHalfStar = rating % 1 >= 0.5; - - return Array(5).fill(false).map((_, index) => { - if (index < fullStars) return true; - if (index === fullStars && hasHalfStar) return true; - return false; - }); - } - - addToCart() { - // This would typically call a cart service - console.log('Adding to cart:', this.product()?.id); - alert('Product added to cart!'); - } - - addToWishlist() { - // This would typically call a wishlist service - console.log('Adding to wishlist:', this.product()?.id); - alert('Product added to wishlist!'); - } -} \ No newline at end of file diff --git a/packages/shop/feature-product-detail/src/test-setup.ts b/packages/shop/feature-product-detail/src/test-setup.ts deleted file mode 100644 index 9157d5f..0000000 --- a/packages/shop/feature-product-detail/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import '@angular/compiler'; -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() -); diff --git a/packages/shop/feature-product-detail/tsconfig.json b/packages/shop/feature-product-detail/tsconfig.json deleted file mode 100644 index 8b4c4fd..0000000 --- a/packages/shop/feature-product-detail/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "module": "preserve", - "moduleResolution": "bundler", - "lib": ["dom", "es2022"] - } -} diff --git a/packages/shop/feature-product-detail/tsconfig.lib.json b/packages/shop/feature-product-detail/tsconfig.lib.json deleted file mode 100644 index a1f7296..0000000 --- a/packages/shop/feature-product-detail/tsconfig.lib.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "types": [] - }, - "exclude": ["src/**/*.spec.ts", "src/test-setup.ts"], - "include": ["src/**/*.ts"] -} \ No newline at end of file diff --git a/packages/shop/feature-product-detail/tsconfig.spec.json b/packages/shop/feature-product-detail/tsconfig.spec.json deleted file mode 100644 index 3a4ffcc..0000000 --- a/packages/shop/feature-product-detail/tsconfig.spec.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ], - "isolatedModules": true - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ], - "files": ["src/test-setup.ts"] -} diff --git a/packages/shop/feature-product-detail/vite.config.mts b/packages/shop/feature-product-detail/vite.config.mts deleted file mode 100644 index 53ca28e..0000000 --- a/packages/shop/feature-product-detail/vite.config.mts +++ /dev/null @@ -1,28 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import angular from '@analogjs/vite-plugin-angular'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/shop/feature-product-detail', - plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'feature-product-detail', - watch: false, - globals: true, - environment: 'jsdom', - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['src/test-setup.ts'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../../coverage/packages/shop/feature-product-detail', - provider: 'v8' as const, - }, - }, -})); diff --git a/packages/shop/feature-products/project.json b/packages/shop/feature-products/project.json deleted file mode 100644 index 6759672..0000000 --- a/packages/shop/feature-products/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "feature-products", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "sourceRoot": "packages/shop/feature-products/src", - "prefix": "shop", - "tags": ["scope:shop", "type:feature"], - "targets": {} -} diff --git a/packages/shop/feature-products/src/index.ts b/packages/shop/feature-products/src/index.ts deleted file mode 100644 index 9e883c8..0000000 --- a/packages/shop/feature-products/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/product-list/product-list.component'; -export * from './lib/feature-products.routes'; \ No newline at end of file diff --git a/packages/shop/feature-products/src/lib/feature-products.routes.ts b/packages/shop/feature-products/src/lib/feature-products.routes.ts deleted file mode 100644 index 63a4cf0..0000000 --- a/packages/shop/feature-products/src/lib/feature-products.routes.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Routes } from '@angular/router'; -import { ProductListComponent } from './product-list/product-list.component'; - -export const featureProductsRoutes: Routes = [ - { - path: '', - component: ProductListComponent, - }, -]; \ No newline at end of file diff --git a/packages/shop/feature-products/src/lib/product-list/product-list.component.spec.ts b/packages/shop/feature-products/src/lib/product-list/product-list.component.spec.ts deleted file mode 100644 index c255b35..0000000 --- a/packages/shop/feature-products/src/lib/product-list/product-list.component.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { Router } from '@angular/router'; -import { of } from 'rxjs'; -import { ProductListComponent } from './product-list.component'; -import { ProductsService } from '@org/shop/data'; -import { Product } from '@org/models'; -import { describe, it, beforeEach, expect, vi } from 'vitest'; - -describe('ProductListComponent', () => { - let component: ProductListComponent; - let fixture: ComponentFixture; - let mockProductsService: Partial; - let mockRouter: Partial; - - const mockProducts: Product[] = [ - { - id: '1', - name: 'Product 1', - description: 'Description 1', - price: 99.99, - imageUrl: 'https://example.com/1.jpg', - category: 'Electronics', - inStock: true, - rating: 4.5, - reviewCount: 100, - }, - { - id: '2', - name: 'Product 2', - description: 'Description 2', - price: 149.99, - imageUrl: 'https://example.com/2.jpg', - category: 'Clothing', - inStock: false, - rating: 4.0, - reviewCount: 50, - }, - ]; - - beforeEach(async () => { - mockProductsService = { - getProducts: vi.fn(), - getCategories: vi.fn(), - loading: vi.fn().mockReturnValue(false), - error: vi.fn().mockReturnValue(null), - }; - - mockRouter = { - navigate: vi.fn(), - }; - - await TestBed.configureTestingModule({ - imports: [ProductListComponent], - providers: [ - { provide: ProductsService, useValue: mockProductsService }, - { provide: Router, useValue: mockRouter }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(ProductListComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should load products and categories on init', () => { - mockProductsService.getProducts.mockReturnValue(of({ - items: mockProducts, - total: 2, - page: 1, - pageSize: 10, - totalPages: 1, - })); - mockProductsService.getCategories.mockReturnValue(of(['Electronics', 'Clothing'])); - - component.ngOnInit(); - - expect(mockProductsService.getProducts).toHaveBeenCalled(); - expect(mockProductsService.getCategories).toHaveBeenCalled(); - expect(component.products()).toEqual(mockProducts); - }); - - it('should navigate to product detail when product is selected', () => { - const product = mockProducts[0]; - - component.onProductSelect(product); - - expect(mockRouter.navigate).toHaveBeenCalledWith(['/products', product.id]); - }); - - it('should apply filters when search term changes', () => { - mockProductsService.getProducts.mockReturnValue(of({ - items: mockProducts, - total: 2, - page: 1, - pageSize: 10, - totalPages: 1, - })); - - component.searchTerm = 'Product 1'; - component.onSearchChange(); - - expect(mockProductsService.getProducts).toHaveBeenCalledWith( - expect.objectContaining({ - searchTerm: 'Product 1', - }), - 1, - 12 - ); - }); - - it('should apply filters when category changes', () => { - mockProductsService.getProducts.mockReturnValue(of({ - items: [mockProducts[0]], - total: 1, - page: 1, - pageSize: 10, - totalPages: 1, - })); - - component.selectedCategory = 'Electronics'; - component.onFilterChange(); - - expect(mockProductsService.getProducts).toHaveBeenCalledWith( - expect.objectContaining({ - category: 'Electronics', - }), - 1, - 12 - ); - }); -}); \ No newline at end of file diff --git a/packages/shop/feature-products/src/lib/product-list/product-list.component.ts b/packages/shop/feature-products/src/lib/product-list/product-list.component.ts deleted file mode 100644 index cea2ed6..0000000 --- a/packages/shop/feature-products/src/lib/product-list/product-list.component.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { Component, inject, signal, computed, OnInit, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; -import { ProductsService } from '@org/shop/data'; -import { Product, ProductFilter } from '@org/models'; -import { - ProductGridComponent, - LoadingSpinnerComponent, - ErrorMessageComponent, -} from '@org/shop/shared-ui'; - -@Component({ - selector: 'shop-product-list', - imports: [ - CommonModule, - FormsModule, - ProductGridComponent, - LoadingSpinnerComponent, - ErrorMessageComponent, - ], - template: ` -
- - -
- - -
- - - -
-
- - @if (loading()) { - - } @else if (error()) { - - } @else { -
- Showing {{ products().length }} of {{ totalProducts() }} products -
- - - - @if (hasMorePages()) { - - } - } -
- `, - styles: [` - .product-list-container { - max-width: 1200px; - margin: 0 auto; - padding: 24px; - } - - .page-header { - text-align: center; - margin-bottom: 48px; - } - - .page-header h1 { - font-size: 2.5rem; - margin-bottom: 8px; - color: #333; - } - - .page-header p { - font-size: 1.1rem; - color: #666; - } - - .filters-section { - background: #f8f9fa; - padding: 24px; - border-radius: 8px; - margin-bottom: 32px; - } - - .search-box { - margin-bottom: 16px; - } - - .search-input { - width: 100%; - padding: 12px 16px; - font-size: 1rem; - border: 1px solid #ddd; - border-radius: 4px; - transition: border-color 0.3s; - } - - .search-input:focus { - outline: none; - border-color: #3498db; - } - - .filter-controls { - display: flex; - gap: 16px; - align-items: center; - flex-wrap: wrap; - } - - .filter-select { - padding: 8px 16px; - font-size: 1rem; - border: 1px solid #ddd; - border-radius: 4px; - background: white; - cursor: pointer; - } - - .checkbox-label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 1rem; - } - - .checkbox-label input { - width: 18px; - height: 18px; - cursor: pointer; - } - - .results-info { - color: #666; - margin-bottom: 16px; - font-size: 0.95rem; - } - - .pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 16px; - margin-top: 48px; - padding-top: 24px; - border-top: 1px solid #e0e0e0; - } - - .page-info { - color: #666; - font-size: 1rem; - } - - .btn-secondary { - padding: 8px 16px; - background: #6c757d; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 1rem; - transition: background 0.3s; - } - - .btn-secondary:hover:not(:disabled) { - background: #5a6268; - } - - .btn-secondary:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - @media (max-width: 768px) { - .product-list-container { - padding: 16px; - } - - .page-header h1 { - font-size: 2rem; - } - - .filter-controls { - flex-direction: column; - align-items: stretch; - } - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ProductListComponent implements OnInit { - private readonly productsService = inject(ProductsService); - private readonly router = inject(Router); - - // State signals - readonly products = signal([]); - readonly totalProducts = signal(0); - readonly currentPage = signal(1); - readonly totalPages = signal(0); - readonly categories = signal([]); - readonly loading = signal(false); - readonly error = signal(null); - - // Filter state - searchTerm = ''; - selectedCategory = ''; - inStockOnly = false; - - // Computed values - readonly hasMorePages = computed(() => this.totalPages() > 1); - - ngOnInit() { - this.loadCategories(); - this.loadProducts(); - } - - loadCategories() { - this.productsService.getCategories().subscribe({ - next: (categories) => this.categories.set(categories), - error: (err) => console.error('Error loading categories:', err), - }); - } - - loadProducts() { - this.loading.set(true); - this.error.set(null); - - const filter: ProductFilter = {}; - - if (this.searchTerm) { - filter.searchTerm = this.searchTerm; - } - if (this.selectedCategory) { - filter.category = this.selectedCategory; - } - if (this.inStockOnly) { - filter.inStock = true; - } - - this.productsService.getProducts(filter, this.currentPage(), 12).subscribe({ - next: (response) => { - this.products.set(response.items); - this.totalProducts.set(response.total); - this.totalPages.set(response.totalPages); - this.loading.set(false); - }, - error: (err) => { - this.error.set('Failed to load products. Please try again.'); - this.loading.set(false); - console.error('Error loading products:', err); - }, - }); - } - - onSearchChange() { - this.currentPage.set(1); - this.loadProducts(); - } - - onFilterChange() { - this.currentPage.set(1); - this.loadProducts(); - } - - onProductSelect(product: Product) { - this.router.navigate(['/products', product.id]); - } - - nextPage() { - if (this.currentPage() < this.totalPages()) { - this.currentPage.update(page => page + 1); - this.loadProducts(); - } - } - - previousPage() { - if (this.currentPage() > 1) { - this.currentPage.update(page => page - 1); - this.loadProducts(); - } - } -} \ No newline at end of file diff --git a/packages/shop/feature-products/src/test-setup.ts b/packages/shop/feature-products/src/test-setup.ts deleted file mode 100644 index 9157d5f..0000000 --- a/packages/shop/feature-products/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import '@angular/compiler'; -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() -); diff --git a/packages/shop/feature-products/tsconfig.json b/packages/shop/feature-products/tsconfig.json deleted file mode 100644 index 8b4c4fd..0000000 --- a/packages/shop/feature-products/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ], - "compilerOptions": { - "module": "preserve", - "moduleResolution": "bundler", - "lib": ["dom", "es2022"] - } -} diff --git a/packages/shop/feature-products/tsconfig.lib.json b/packages/shop/feature-products/tsconfig.lib.json deleted file mode 100644 index a1f7296..0000000 --- a/packages/shop/feature-products/tsconfig.lib.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "types": [] - }, - "exclude": ["src/**/*.spec.ts", "src/test-setup.ts"], - "include": ["src/**/*.ts"] -} \ No newline at end of file diff --git a/packages/shop/feature-products/tsconfig.spec.json b/packages/shop/feature-products/tsconfig.spec.json deleted file mode 100644 index 3a4ffcc..0000000 --- a/packages/shop/feature-products/tsconfig.spec.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ], - "isolatedModules": true - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ], - "files": ["src/test-setup.ts"] -} diff --git a/packages/shop/feature-products/vite.config.mts b/packages/shop/feature-products/vite.config.mts deleted file mode 100644 index fed9b14..0000000 --- a/packages/shop/feature-products/vite.config.mts +++ /dev/null @@ -1,28 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import angular from '@analogjs/vite-plugin-angular'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/shop/feature-products', - plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'feature-products', - watch: false, - globals: true, - environment: 'jsdom', - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['src/test-setup.ts'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../../coverage/packages/shop/feature-products', - provider: 'v8' as const, - }, - }, -})); diff --git a/packages/shop/shared-ui/README.md b/packages/shop/shared-ui/README.md deleted file mode 100644 index 66b2964..0000000 --- a/packages/shop/shared-ui/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# shared-ui - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test shared-ui` to execute the unit tests. diff --git a/packages/shop/shared-ui/eslint.config.mjs b/packages/shop/shared-ui/eslint.config.mjs deleted file mode 100644 index 1e6c219..0000000 --- a/packages/shop/shared-ui/eslint.config.mjs +++ /dev/null @@ -1,34 +0,0 @@ -import nx from '@nx/eslint-plugin'; -import baseConfig from '../../../eslint.config.mjs'; - -export default [ - ...baseConfig, - ...nx.configs['flat/angular'], - ...nx.configs['flat/angular-template'], - { - files: ['**/*.ts'], - rules: { - '@angular-eslint/directive-selector': [ - 'error', - { - type: 'attribute', - prefix: 'shop', - style: 'camelCase', - }, - ], - '@angular-eslint/component-selector': [ - 'error', - { - type: 'element', - prefix: 'shop', - style: 'kebab-case', - }, - ], - }, - }, - { - files: ['**/*.html'], - // Override or add rules here - rules: {}, - }, -]; diff --git a/packages/shop/shared-ui/project.json b/packages/shop/shared-ui/project.json deleted file mode 100644 index 1ece8e5..0000000 --- a/packages/shop/shared-ui/project.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "shared-ui", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "packages/shop/shared-ui/src", - "prefix": "lib", - "projectType": "library", - "tags": ["scope:shop", "type:ui"], - "targets": {} -} diff --git a/packages/shop/shared-ui/src/index.ts b/packages/shop/shared-ui/src/index.ts deleted file mode 100644 index f42136d..0000000 --- a/packages/shop/shared-ui/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './lib/product-card/product-card.component'; -export * from './lib/product-grid/product-grid.component'; -export * from './lib/loading-spinner/loading-spinner.component'; -export * from './lib/error-message/error-message.component'; diff --git a/packages/shop/shared-ui/src/lib/error-message/error-message.component.ts b/packages/shop/shared-ui/src/lib/error-message/error-message.component.ts deleted file mode 100644 index b7975c0..0000000 --- a/packages/shop/shared-ui/src/lib/error-message/error-message.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, input, output, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'shop-error-message', - imports: [CommonModule], - template: ` -
-
⚠️
-

{{ title() || 'Oops! Something went wrong' }}

-

{{ message() || 'An unexpected error occurred. Please try again later.' }}

- @if (showRetry()) { - - } -
- `, - styles: [` - .error-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 48px; - text-align: center; - background: #fff; - border-radius: 8px; - border: 1px solid #e0e0e0; - } - - .error-icon { - font-size: 48px; - margin-bottom: 16px; - } - - h3 { - margin: 0 0 8px 0; - font-size: 1.5rem; - color: #333; - } - - p { - margin: 0 0 24px 0; - color: #666; - font-size: 1rem; - max-width: 400px; - } - - .retry-button { - background: #3498db; - color: white; - border: none; - padding: 12px 24px; - border-radius: 4px; - font-size: 1rem; - cursor: pointer; - transition: background 0.3s ease; - } - - .retry-button:hover { - background: #2980b9; - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ErrorMessageComponent { - readonly title = input(); - readonly message = input(); - readonly showRetry = input(true); - readonly retry = output(); -} \ No newline at end of file diff --git a/packages/shop/shared-ui/src/lib/loading-spinner/loading-spinner.component.ts b/packages/shop/shared-ui/src/lib/loading-spinner/loading-spinner.component.ts deleted file mode 100644 index 00a5448..0000000 --- a/packages/shop/shared-ui/src/lib/loading-spinner/loading-spinner.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -@Component({ - selector: 'shop-loading-spinner', - imports: [CommonModule], - template: ` -
-
-

Loading...

-
- `, - styles: [` - .spinner-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 48px; - } - - .spinner { - width: 40px; - height: 40px; - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - border-radius: 50%; - animation: spin 1s linear infinite; - } - - @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } - } - - p { - margin-top: 16px; - color: #666; - font-size: 1rem; - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LoadingSpinnerComponent {} \ No newline at end of file diff --git a/packages/shop/shared-ui/src/lib/product-card/product-card.component.spec.ts b/packages/shop/shared-ui/src/lib/product-card/product-card.component.spec.ts deleted file mode 100644 index 3b55378..0000000 --- a/packages/shop/shared-ui/src/lib/product-card/product-card.component.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { ProductCardComponent } from './product-card.component'; -import { Product } from '@org/models'; -import { describe, it, beforeEach, expect, vi } from 'vitest'; - -describe('ProductCardComponent', () => { - let component: ProductCardComponent; - let fixture: ComponentFixture; - - const mockProduct: Product = { - id: '1', - name: 'Test Product', - description: 'Test Description', - price: 99.99, - imageUrl: 'https://example.com/image.jpg', - category: 'Electronics', - inStock: true, - rating: 4.5, - reviewCount: 100, - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ProductCardComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(ProductCardComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('product', mockProduct); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should display product information', () => { - fixture.detectChanges(); - const compiled = fixture.nativeElement; - - expect(compiled.querySelector('.product-name').textContent).toContain('Test Product'); - expect(compiled.querySelector('.product-category').textContent).toContain('Electronics'); - expect(compiled.querySelector('.product-price').textContent).toContain('99.99'); - }); - - it('should emit productClick event when card is clicked', () => { - const clickSpy = vi.fn(); - component.productClick.subscribe(clickSpy); - - fixture.nativeElement.querySelector('.product-card').click(); - - expect(clickSpy).toHaveBeenCalledWith(mockProduct); - }); - - it('should display out of stock overlay when product is not in stock', () => { - const outOfStockProduct = { ...mockProduct, inStock: false }; - fixture.componentRef.setInput('product', outOfStockProduct); - fixture.detectChanges(); - - const compiled = fixture.nativeElement; - expect(compiled.querySelector('.out-of-stock-overlay')).toBeTruthy(); - expect(compiled.querySelector('.product-card').classList).toContain('out-of-stock'); - }); - - it('should calculate star ratings correctly', () => { - const stars = component.getStars(); - - expect(stars).toEqual([true, true, true, true, true]); - }); - - it('should handle product with low rating', () => { - const lowRatedProduct = { ...mockProduct, rating: 2.3 }; - fixture.componentRef.setInput('product', lowRatedProduct); - - const stars = component.getStars(); - - expect(stars).toEqual([true, true, false, false, false]); - }); -}); \ No newline at end of file diff --git a/packages/shop/shared-ui/src/lib/product-card/product-card.component.ts b/packages/shop/shared-ui/src/lib/product-card/product-card.component.ts deleted file mode 100644 index b7adb56..0000000 --- a/packages/shop/shared-ui/src/lib/product-card/product-card.component.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Component, input, output, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule, CurrencyPipe } from '@angular/common'; -import { Product } from '@org/models'; - -@Component({ - selector: 'shop-product-card', - imports: [CommonModule, CurrencyPipe], - template: ` -
-
- - @if (!product().inStock) { -
Out of Stock
- } -
-
-

{{ product().name }}

-

{{ product().category }}

-
- - @for (star of getStars(); track $index) { - ★ - } - - ({{ product().reviewCount }}) -
-
- {{ product().price | currency }} -
-
-
- `, - styles: [` - .product-card { - border: 1px solid #e0e0e0; - border-radius: 8px; - overflow: hidden; - cursor: pointer; - transition: all 0.3s ease; - background: white; - height: 100%; - display: flex; - flex-direction: column; - } - - .product-card:hover { - transform: translateY(-4px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); - } - - .product-card.out-of-stock { - opacity: 0.7; - } - - .product-image { - position: relative; - width: 100%; - padding-top: 100%; - overflow: hidden; - background: #f5f5f5; - } - - .product-image img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - } - - .out-of-stock-overlay { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - background: rgba(0, 0, 0, 0.7); - color: white; - padding: 8px 16px; - border-radius: 4px; - font-weight: bold; - } - - .product-info { - padding: 16px; - flex: 1; - display: flex; - flex-direction: column; - } - - .product-name { - margin: 0 0 8px 0; - font-size: 1.1rem; - font-weight: 600; - color: #333; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - } - - .product-category { - margin: 0 0 8px 0; - font-size: 0.9rem; - color: #666; - } - - .product-rating { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; - } - - .stars { - color: #ddd; - } - - .stars .filled { - color: #ffd700; - } - - .review-count { - font-size: 0.85rem; - color: #666; - } - - .product-price { - font-size: 1.25rem; - font-weight: bold; - color: #2c3e50; - margin-top: auto; - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ProductCardComponent { - readonly product = input.required(); - readonly productClick = output(); - - getStars(): boolean[] { - const rating = this.product().rating; - const fullStars = Math.floor(rating); - const hasHalfStar = rating % 1 >= 0.5; - - return Array(5).fill(false).map((_, index) => { - if (index < fullStars) return true; - if (index === fullStars && hasHalfStar) return true; - return false; - }); - } -} \ No newline at end of file diff --git a/packages/shop/shared-ui/src/lib/product-grid/product-grid.component.ts b/packages/shop/shared-ui/src/lib/product-grid/product-grid.component.ts deleted file mode 100644 index 1fdf4b8..0000000 --- a/packages/shop/shared-ui/src/lib/product-grid/product-grid.component.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, input, output, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Product } from '@org/models'; -import { ProductCardComponent } from '../product-card/product-card.component'; - -@Component({ - selector: 'shop-product-grid', - imports: [CommonModule, ProductCardComponent], - template: ` -
- @for (product of products(); track product.id) { - - } - @empty { -
-

No products found

-
- } -
- `, - styles: [` - .product-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 24px; - padding: 24px 0; - } - - .no-products { - grid-column: 1 / -1; - text-align: center; - padding: 48px; - color: #666; - font-size: 1.1rem; - } - - @media (max-width: 768px) { - .product-grid { - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); - gap: 16px; - } - } - `], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ProductGridComponent { - readonly products = input.required(); - readonly productSelect = output(); -} \ No newline at end of file diff --git a/packages/shop/shared-ui/src/test-setup.ts b/packages/shop/shared-ui/src/test-setup.ts deleted file mode 100644 index 9157d5f..0000000 --- a/packages/shop/shared-ui/src/test-setup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import '@angular/compiler'; -import '@analogjs/vitest-angular/setup-zone'; - -import { - BrowserTestingModule, - platformBrowserTesting, -} from '@angular/platform-browser/testing'; -import { getTestBed } from '@angular/core/testing'; - -getTestBed().initTestEnvironment( - BrowserTestingModule, - platformBrowserTesting() -); diff --git a/packages/shop/shared-ui/tsconfig.json b/packages/shop/shared-ui/tsconfig.json deleted file mode 100644 index 820e27c..0000000 --- a/packages/shop/shared-ui/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "target": "es2022", - "moduleResolution": "bundler", - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "module": "preserve", - "lib": ["dom", "es2022"] - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "typeCheckHostBindings": true, - "strictTemplates": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/shop/shared-ui/tsconfig.lib.json b/packages/shop/shared-ui/tsconfig.lib.json deleted file mode 100644 index 312ee86..0000000 --- a/packages/shop/shared-ui/tsconfig.lib.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "declarationMap": true, - "inlineSources": true, - "types": [] - }, - "exclude": [ - "src/**/*.spec.ts", - "src/test-setup.ts", - "jest.config.ts", - "src/**/*.test.ts", - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx" - ], - "include": ["src/**/*.ts"] -} diff --git a/packages/shop/shared-ui/tsconfig.spec.json b/packages/shop/shared-ui/tsconfig.spec.json deleted file mode 100644 index 3a4ffcc..0000000 --- a/packages/shop/shared-ui/tsconfig.spec.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "types": [ - "vitest/globals", - "vitest/importMeta", - "vite/client", - "node", - "vitest" - ], - "isolatedModules": true - }, - "include": [ - "vite.config.ts", - "vite.config.mts", - "vitest.config.ts", - "vitest.config.mts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.test.tsx", - "src/**/*.spec.tsx", - "src/**/*.test.js", - "src/**/*.spec.js", - "src/**/*.test.jsx", - "src/**/*.spec.jsx", - "src/**/*.d.ts" - ], - "files": ["src/test-setup.ts"] -} diff --git a/packages/shop/shared-ui/vite.config.mts b/packages/shop/shared-ui/vite.config.mts deleted file mode 100644 index 79c93df..0000000 --- a/packages/shop/shared-ui/vite.config.mts +++ /dev/null @@ -1,28 +0,0 @@ -/// -import { defineConfig } from 'vite'; -import angular from '@analogjs/vite-plugin-angular'; -import tsconfigPaths from 'vite-tsconfig-paths'; -import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; - -export default defineConfig(() => ({ - root: __dirname, - cacheDir: '../../../node_modules/.vite/packages/shop/shared-ui', - plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])], - // Uncomment this if you are using workers. - // worker: { - // plugins: [ nxViteTsPaths() ], - // }, - test: { - name: 'shared-ui', - watch: false, - globals: true, - environment: 'jsdom', - include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], - setupFiles: ['src/test-setup.ts'], - reporters: ['default'], - coverage: { - reportsDirectory: '../../../coverage/packages/shop/shared-ui', - provider: 'v8' as const, - }, - }, -})); diff --git a/tsconfig.base.json b/tsconfig.base.json index 5e0637b..d4054df 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -10,21 +10,20 @@ "importHelpers": true, "target": "es2015", "module": "esnext", - "lib": ["es2020", "dom"], + "lib": [ + "es2020", + "dom" + ], "skipLibCheck": true, "skipDefaultLibCheck": true, "paths": { - "@org/api/products": ["./packages/api/products/src/index.ts"], - "@org/models": ["./packages/shared/models/src/index.ts"], - "@org/shop/data": ["./packages/shop/data/src/index.ts"], - "@org/shop/feature-product-detail": [ - "./packages/shop/feature-product-detail/src/index.ts" - ], - "@org/shop/feature-products": [ - "./packages/shop/feature-products/src/index.ts" - ], - "@org/shop/shared-ui": ["./packages/shop/shared-ui/src/index.ts"] + "@pi/ui": [ + "./libs/ui/src/index.ts" + ] } }, - "exclude": ["node_modules", "tmp"] + "exclude": [ + "node_modules", + "tmp" + ] }