Directory structure: └── techinsper-insper-design-system/ ├── README.md ├── angular.json ├── azure-pipelines.yml ├── Dockerfile ├── nginx.conf ├── package.json ├── tsconfig.json ├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .npmrc ├── .postcssrc.json ├── .yarnrc.yml ├── projects/ │ ├── insper/ │ │ └── design-system/ │ │ ├── README.md │ │ ├── ng-package.json │ │ ├── package.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.lib.prod.json │ │ ├── tsconfig.schematics.json │ │ ├── tsconfig.spec.json │ │ ├── .eslintrc.json │ │ ├── schematics/ │ │ │ ├── collection.json │ │ │ └── ng-add/ │ │ │ └── index.ts │ │ └── src/ │ │ ├── public-api.ts │ │ ├── assets/ │ │ │ └── Acumin/ │ │ │ ├── Acumin-Pro-Extra-Condensed.woff │ │ │ └── Acumin-Pro-Extra-Condensed.woff2 │ │ └── lib/ │ │ ├── components/ │ │ │ ├── components.module.ts │ │ │ ├── accordion/ │ │ │ │ ├── accordion.component.css │ │ │ │ ├── accordion.component.html │ │ │ │ ├── accordion.component.spec.ts │ │ │ │ └── accordion.component.ts │ │ │ ├── avatar/ │ │ │ │ ├── avatar.component.css │ │ │ │ ├── avatar.component.html │ │ │ │ ├── avatar.component.spec.ts │ │ │ │ ├── avatar.component.ts │ │ │ │ └── utils/ │ │ │ │ └── testing.ts │ │ │ ├── breadcrumb/ │ │ │ │ ├── breadcrumb.component.css │ │ │ │ ├── breadcrumb.component.html │ │ │ │ ├── breadcrumb.component.spec.ts │ │ │ │ └── breadcrumb.component.ts │ │ │ ├── button/ │ │ │ │ ├── button.component.css │ │ │ │ ├── button.component.html │ │ │ │ ├── button.component.spec.ts │ │ │ │ └── button.component.ts │ │ │ ├── card/ │ │ │ │ ├── card.component.css │ │ │ │ ├── card.component.html │ │ │ │ ├── card.component.spec.ts │ │ │ │ └── card.component.ts │ │ │ ├── chip/ │ │ │ │ ├── chip.component.css │ │ │ │ ├── chip.component.html │ │ │ │ ├── chip.component.spec.ts │ │ │ │ └── chip.component.ts │ │ │ ├── container/ │ │ │ │ ├── container.component.css │ │ │ │ ├── container.component.spec.ts │ │ │ │ └── container.component.ts │ │ │ ├── divider/ │ │ │ │ ├── divider.component.css │ │ │ │ ├── divider.component.html │ │ │ │ ├── divider.component.spec.ts │ │ │ │ └── divider.component.ts │ │ │ ├── form-field/ │ │ │ │ ├── form-field.component.css │ │ │ │ ├── form-field.component.html │ │ │ │ ├── form-field.component.spec.ts │ │ │ │ └── form-field.component.ts │ │ │ ├── grid/ │ │ │ │ ├── grid.component.css │ │ │ │ ├── grid.component.spec.ts │ │ │ │ └── grid.component.ts │ │ │ ├── grid-item/ │ │ │ │ ├── grid-item.component.css │ │ │ │ └── grid-item.component.ts │ │ │ ├── icon/ │ │ │ │ ├── icon.component.css │ │ │ │ ├── icon.component.spec.ts │ │ │ │ └── icon.component.ts │ │ │ ├── input-checkbox/ │ │ │ │ ├── input-checkbox.component.css │ │ │ │ ├── input-checkbox.component.spec.ts │ │ │ │ └── input-checkbox.component.ts │ │ │ ├── input-text/ │ │ │ │ ├── input-text.component.css │ │ │ │ ├── input-text.component.spec.ts │ │ │ │ └── input-text.component.ts │ │ │ ├── modal/ │ │ │ │ ├── modal.component.css │ │ │ │ ├── modal.component.html │ │ │ │ ├── modal.component.spec.ts │ │ │ │ └── modal.component.ts │ │ │ ├── search/ │ │ │ │ ├── search.component.css │ │ │ │ ├── search.component.html │ │ │ │ ├── search.component.spec.ts │ │ │ │ └── search.component.ts │ │ │ ├── stack/ │ │ │ │ ├── stack.component.css │ │ │ │ ├── stack.component.html │ │ │ │ ├── stack.component.spec.ts │ │ │ │ └── stack.component.ts │ │ │ ├── tab/ │ │ │ │ ├── tab.component.css │ │ │ │ ├── tab.component.html │ │ │ │ ├── tab.component.spec.ts │ │ │ │ └── tab.component.ts │ │ │ ├── table/ │ │ │ │ ├── table.component.css │ │ │ │ ├── table.component.html │ │ │ │ ├── table.component.spec.ts │ │ │ │ └── table.component.ts │ │ │ ├── toast/ │ │ │ │ ├── toast.component.css │ │ │ │ ├── toast.component.html │ │ │ │ ├── toast.component.spec.ts │ │ │ │ └── toast.component.ts │ │ │ ├── toggle/ │ │ │ │ ├── toggle.component.css │ │ │ │ ├── toggle.component.html │ │ │ │ ├── toggle.component.spec.ts │ │ │ │ └── toggle.component.ts │ │ │ ├── tooltip/ │ │ │ │ ├── tooltip.component.css │ │ │ │ ├── tooltip.component.html │ │ │ │ ├── tooltip.component.spec.ts │ │ │ │ └── tooltip.component.ts │ │ │ └── typography/ │ │ │ ├── typography.component.css │ │ │ ├── typography.component.html │ │ │ ├── typography.component.spec.ts │ │ │ └── typography.component.ts │ │ ├── services/ │ │ │ └── toast/ │ │ │ ├── toast.service.spec.ts │ │ │ └── toast.service.ts │ │ └── styles/ │ │ ├── styles.css │ │ └── theme/ │ │ ├── index.css │ │ ├── index.ts │ │ └── tokens/ │ │ ├── breakpoints.css │ │ ├── breakpoints.ts │ │ ├── colors.css │ │ ├── colors.ts │ │ ├── container.css │ │ ├── container.ts │ │ ├── size.css │ │ ├── size.ts │ │ ├── spacing.css │ │ ├── spacing.ts │ │ ├── typography.css │ │ └── typography.ts │ └── preview/ │ ├── package.json │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── src/ │ │ ├── index.html │ │ ├── main.ts │ │ ├── styles.css │ │ ├── app/ │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ ├── app.routes.ts │ │ │ └── button-preview/ │ │ │ ├── button-preview.component.css │ │ │ ├── button-preview.component.html │ │ │ ├── button-preview.component.spec.ts │ │ │ └── button-preview.component.ts │ │ ├── assets/ │ │ │ └── .gitkeep │ │ ├── stories/ │ │ │ ├── avatar.stories.ts │ │ │ ├── container.stories.ts │ │ │ ├── form-field.stories.ts │ │ │ ├── grid.stories.ts │ │ │ ├── input-checkbox.stories.ts │ │ │ ├── introduction.mdx │ │ │ ├── modal.stories.ts │ │ │ ├── stack.stories.ts │ │ │ ├── tab.stories.ts │ │ │ ├── table.stories.ts │ │ │ ├── toast.stories.ts │ │ │ ├── toggle.stories.ts │ │ │ ├── tooltip.stories.ts │ │ │ ├── accordion/ │ │ │ │ ├── accordion.mdx │ │ │ │ └── accordion.stories.ts │ │ │ ├── breadcrumb/ │ │ │ │ ├── breadcrumb.mdx │ │ │ │ └── breadcrumb.stories.ts │ │ │ ├── button/ │ │ │ │ ├── button.mdx │ │ │ │ └── button.stories.ts │ │ │ ├── chip/ │ │ │ │ ├── chip.mdx │ │ │ │ └── chip.stories.ts │ │ │ ├── divider/ │ │ │ │ ├── divider.mdx │ │ │ │ └── divider.stories.ts │ │ │ ├── icon/ │ │ │ │ ├── icon.mdx │ │ │ │ └── icon.stories.ts │ │ │ ├── search/ │ │ │ │ ├── search.mdx │ │ │ │ └── search.stories.ts │ │ │ └── typography/ │ │ │ ├── typography.mdx │ │ │ └── typography.stories.ts │ │ └── types/ │ │ └── WithContent.ts │ └── .storybook/ │ ├── main.ts │ ├── preview.ts │ ├── tsconfig.doc.json │ ├── tsconfig.json │ └── typings.d.ts ├── .github/ │ └── workflows/ │ └── publisher.yml └── .husky/ ├── pre-commit └── pre-push ================================================ FILE: README.md ================================================ ## Tools This projects was built with the following technologies: - Node 22 - Angular v17 - Tailwind CSS v3.4 - Storybook v8.6 ## Development server Run `yarn start` Now you can access http://localhost:6006 in your browser and check the components storybook ## Creating a new component ```bash cd /projects/insper/design-system ng g c ``` Remember to ALWAYS export new components from `/projects/insper/design-system/src/public-api.ts` ## Add fonts Download the font, create a folder inside /src/assets with the font's name: `projects/insper/design-system/src/assets/Montserrat` Then add all the files, like ttf or any others. In the project folder, go to the global style file and add the `@font-face` using the installed font Now, you need to add the font to Tailwind's @theme ## Maintenance We recommend using an extension to hide comments, due to lots of JSDoc's comments. You can install `Hide Comments` if using VS Code. ## Build Library Run `yarn build` to build the project. The build artifacts will be stored in the `dist/` directory. If you're gonna publish it, you need to upgrade library version, you can do it by changing `projects/insper/design-system/package.json` "version" ## Publish Library Did you build the library? If you didn't, go back to "Build Library" step ```bash cd dist/insper/design-system npm login --registry npm publish --registry ``` ## Build Storybook Run `yarn build:storybook` ## Serving Storybook static files ```bash # Build a new image docker build -t design-system/storybook . # Run the image docker run -d -p 8080:80 design-system/storybook ``` ## Adding the library to another Angular project If you are using NPM, create a `.npmrc` file ```bash #.npmrc @insper:registry= registry=https://registry.npmjs.org/ ``` If using Yarn, create a `.yarnrc` file ```bash npmScopes: insper: npmRegistryServer: "private-registerer" npmAlwaysAuth: false npmAuthToken: "" unsafeHttpWhitelist: - "localhost" ``` Now you can execute the following command: ```bash ng add @techinsper/insper-design-system ``` ================================================ FILE: angular.json ================================================ { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "version": 1, "newProjectRoot": "projects", "cli": { "schematicCollections": [ "angular-eslint" ] }, "projects": { "@techinsper/insper-design-system": { "projectType": "library", "root": "projects/insper/design-system", "sourceRoot": "projects/insper/design-system/src", "schematics": { "@schematics/angular:component": { "path": "projects/insper/design-system/src/lib/components" } }, "prefix": "ids", "architect": { "build": { "builder": "@angular-devkit/build-angular:ng-packagr", "options": { "project": "projects/insper/design-system/ng-package.json" }, "configurations": { "production": { "tsConfig": "projects/insper/design-system/tsconfig.lib.prod.json" }, "development": { "tsConfig": "projects/insper/design-system/tsconfig.lib.json" } }, "defaultConfiguration": "production" }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "tsConfig": "projects/insper/design-system/tsconfig.spec.json", "polyfills": [ "zone.js", "zone.js/testing" ] } }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { "lintFilePatterns": [ "projects/insper/design-system/**/*.ts", "projects/insper/design-system/**/*.html" ] } } } }, "preview": { "projectType": "application", "schematics": {}, "root": "projects/preview", "sourceRoot": "projects/preview/src", "prefix": "app", "architect": { "build": { "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/preview", "index": "projects/preview/src/index.html", "browser": "projects/preview/src/main.ts", "polyfills": [ "zone.js" ], "tsConfig": "projects/preview/tsconfig.app.json", "assets": [ "projects/preview/src/favicon.ico", "projects/preview/src/assets" ], "styles": [ "projects/preview/src/styles.css", "@techinsper/insper-design-system/src/lib/styles/styles.css" ], "scripts": [] }, "configurations": { "production": { "budgets": [ { "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }, { "type": "anyComponentStyle", "maximumWarning": "2kb", "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { "optimization": false, "extractLicenses": false, "sourceMap": true } }, "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { "buildTarget": "preview:build:production" }, "development": { "buildTarget": "preview:build:development" } }, "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { "buildTarget": "preview:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { "polyfills": [ "zone.js", "zone.js/testing" ], "tsConfig": "projects/preview/tsconfig.spec.json", "assets": [ "projects/preview/src/favicon.ico", "projects/preview/src/assets" ], "styles": [ "projects/preview/src/styles.css", "@techinsper/insper-design-system/src/lib/styles/styles.css" ], "scripts": [] } }, "storybook": { "builder": "@storybook/angular:start-storybook", "options": { "configDir": "projects/preview/.storybook", "browserTarget": "preview:build", "compodoc": true, "compodocArgs": [ "-e", "json", "--disablePrivate", "--disableInternal", "-d", "projects/preview" ], "port": 6006 } }, "build-storybook": { "builder": "@storybook/angular:build-storybook", "options": { "configDir": "projects/preview/.storybook", "browserTarget": "preview:build", "compodoc": true, "compodocArgs": [ "-e", "json", "--disablePrivate", "--disableInternal", "-d", "projects/preview" ], "outputDir": "dist/storybook/preview" } } } } } } ================================================ FILE: azure-pipelines.yml ================================================ pr: branches: include: - main resources: - repo: self variables: dockerRegistryServiceConnection: '9aefe228-12f2-48ee-825c-6f4a5c808b81' imageRepository: 'storybook' containerRegistry: 'devinspertech.azurecr.io' dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile' tag: '$(Build.BuildNumber)' vmImageName: 'ubuntu-latest' stages: - stage: Build displayName: Build and push stage jobs: - job: Build displayName: Build Docker image and push pool: vmImage: $(vmImageName) steps: # - task: JavaToolInstaller@0 # inputs: # versionSpec: '17' # jdkArchitectureOption: 'x64' # jdkSourceOption: 'PreInstalled' # - task: SonarQubePrepare@4 # inputs: # SonarQube: 'SonarQube' # scannerMode: 'CLI' # configMode: 'manual' # cliProjectKey: 'Insper-Design-System' # cliProjectName: 'Insper Design System' # cliSources: '.' # - task: SonarQubeAnalyze@4 # inputs: # jdkversion: 'JAVA_HOME_17_X64' # - task: SonarQubePublish@4 # inputs: # pollingTimeoutSec: '300' - task: Docker@2 displayName: 'Build and push Docker image' inputs: command: buildAndPush repository: $(imageRepository) dockerfile: $(dockerfilePath) containerRegistry: $(dockerRegistryServiceConnection) tags: | $(tag) buildContext: '$(Build.SourcesDirectory)' ================================================ FILE: Dockerfile ================================================ # Stage 1: Build the Angular application and Storybook FROM node:18 AS builder WORKDIR /app # Copy package.json and workspace configuration COPY package.json yarn.lock ./ COPY projects/preview/package.json ./projects/preview/ COPY projects/insper/design-system/package.json ./projects/insper/design-system/ # Enable Corepack to use the yarn version specified in package.json RUN corepack enable # Copy the rest of the application code (needed for workspace dependencies) COPY . . # Install dependencies (allow lockfile updates during Docker build) RUN yarn install # Build Storybook # The output directory is defined in angular.json: projects.preview.architect.build-storybook.options.outputDir RUN yarn build:storybook # Stage 2: Serve the static files with Nginx FROM nginx:alpine # Copy the Storybook build output from the builder stage COPY --from=builder /app/dist/storybook/preview /usr/share/nginx/html # Copy custom Nginx configuration COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ================================================ FILE: nginx.conf ================================================ server { listen 80; server_name localhost; root /usr/share/nginx/html; index index.html; location / { try_files $uri /index.html; } } ================================================ FILE: package.json ================================================ { "name": "ds-insper", "version": "0.0.0", "scripts": { "ng": "ng", "start": "run-p watch start:storybook", "start:storybook": "ng run preview:storybook", "build": "run-s build:tailwind build:lib build:schematics", "build:lib": "ng build @techinsper/insper-design-system", "build:schematics": "cd projects/insper/design-system && yarn build", "build:storybook": "ng run preview:build-storybook", "build:tailwind": "npx @tailwindcss/cli -i ./projects/insper/design-system/src/lib/styles/styles.css -o ./projects/insper/design-system/src/lib/styles/ids-theme.css", "publish": "yarn build && cd dist/insper/design-system && npm publish --registry=https://npm.pkg.github.com", "watch": "ng build @techinsper/insper-design-system --watch", "test": "ng test", "test:ci": "ng test @techinsper/insper-design-system --no-watch --no-progress --browsers=ChromeHeadless", "prepare": "husky", "lint": "ng lint", "postinstall": "yarn prepare" }, "private": true, "workspaces": { "packages": [ "projects/insper/design-system/", "projects/preview" ] }, "dependencies": { "@angular/animations": "~17.3.0", "@angular/common": "~17.3.0", "@angular/compiler": "~17.3.0", "@angular/core": "~17.3.0", "@angular/forms": "~17.3.0", "@angular/platform-browser": "~17.3.0", "@angular/platform-browser-dynamic": "~17.3.0", "@angular/router": "~17.3.0", "@tailwindcss/postcss": "~4.1.7", "postcss": "~8.5.3", "postcss-loader": "~8.1.1", "rxjs": "~7.8.0", "tailwindcss": "~4.1.7", "tslib": "~2.3.0", "zone.js": "~0.14.3" }, "devDependencies": { "@angular-devkit/build-angular": "~17.3.17", "@angular-eslint/builder": "17.5.3", "@angular-eslint/eslint-plugin": "17.5.3", "@angular-eslint/eslint-plugin-template": "17.5.3", "@angular-eslint/schematics": "17.5.3", "@angular-eslint/template-parser": "17.5.3", "@angular/cli": "~17.3.17", "@angular/compiler-cli": "~17.3.0", "@compodoc/compodoc": "1.1.26", "@storybook/addon-docs": "8.6.12", "@storybook/addon-essentials": "8.6.12", "@storybook/addon-interactions": "8.6.12", "@storybook/addon-onboarding": "8.6.12", "@storybook/angular": "8.6.12", "@storybook/blocks": "8.6.12", "@storybook/test": "8.6.12", "@tanstack/angular-table": "8.21.3", "@types/jasmine": "~5.1.0", "@typescript-eslint/eslint-plugin": "7.11.0", "@typescript-eslint/parser": "7.11.0", "angular-eslint": "19.4.0", "eslint": "~8.57.0", "husky": "~9.1.7", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "ng-packagr": "~17.3.0", "npm-run-all": "~4.1.5", "storybook": "8.6.12", "typescript": "~5.4.2", "typescript-eslint": "8.32.0" }, "packageManager": "yarn@4.9.1" } ================================================ FILE: tsconfig.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "compileOnSave": false, "compilerOptions": { "paths": { "@techinsper/insper-design-system": [ "./dist/insper/design-system" ] }, "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, "sourceMap": true, "declaration": false, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, "lib": [ "ES2022", "dom" ] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true, "strictTemplates": true } } ================================================ FILE: .dockerignore ================================================ node_modules dist .git .gitignore Dockerfile *.log *.tmp ================================================ FILE: .editorconfig ================================================ # Editor configuration, see https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true [*.ts] quote_type = single [*.md] max_line_length = off trim_trailing_whitespace = false ================================================ FILE: .eslintrc.json ================================================ { "root": true, "ignorePatterns": [ "projects/**/*" ], "overrides": [ { "files": [ "*.ts" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates" ], "rules": { "@angular-eslint/directive-selector": [ "error", { "prefix": "ids", "style": "camelCase", "type": ["attribute", "element"] } ], "@angular-eslint/directive-class-suffix": "off", "@angular-eslint/component-selector": [ "error", { "prefix": "ids" } ], "@angular-eslint/prefer-standalone": "error", "@angular-eslint/no-output-on-prefix": "error", "@angular-eslint/no-input-rename": "error", "@angular-eslint/use-injectable-provided-in": "error", "@angular-eslint/no-host-metadata-property": "off", "@angular-eslint/template/click-events-have-key-events": "off", "@typescript-eslint/max-params": [ "error", { "max": 3 } ], "semi": ["error", "always"], "quotes": ["error", "single"], "comma-dangle": [ "error", { "arrays": "always-multiline", "objects": "always-multiline", "imports": "always-multiline", "exports": "always-multiline", "functions": "ignore" } ], "space-in-parens": "error", "no-multi-spaces": "error", "no-multiple-empty-lines": [ "error", { "max": 1 } ], "no-console": "warn" } }, { "files": [ "*.html" ], "extends": [ "plugin:@angular-eslint/template/recommended", "plugin:@angular-eslint/template/accessibility" ], "rules": { } } ] } ================================================ FILE: .npmrc ================================================ @techinsper:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} ================================================ FILE: .postcssrc.json ================================================ { "plugins": { "@tailwindcss/postcss": {} } } ================================================ FILE: .yarnrc.yml ================================================ yarnPath: .yarn/releases/yarn-4.9.1.cjs nodeLinker: node-modules enableGlobalCache: true enableInlineBuilds: true compressionLevel: mixed ================================================ FILE: projects/insper/design-system/README.md ================================================ # DesignSystem This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.3.0. ## Code scaffolding Run `ng generate component component-name --project design-system` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project design-system`. > Note: Don't forget to add `--project design-system` or else it will be added to the default project in your `angular.json` file. ## Build Run `ng build design-system` to build the project. The build artifacts will be stored in the `dist/` directory. ## Publishing After building your library with `ng build design-system`, go to the dist folder `cd dist/design-system` and run `npm publish`. ## Running unit tests Run `ng test design-system` to execute the unit tests via [Karma](https://karma-runner.github.io). ## Further help To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. ================================================ FILE: projects/insper/design-system/ng-package.json ================================================ { "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", "dest": "../../../dist/insper/design-system", "lib": { "entryFile": "src/public-api.ts" }, "assets": [ "./src/lib/styles/ids-theme.css", "./src/lib/styles/theme/**/*.css", "./src/assets/**/*", "./schematics/**/*.json", "./schematics/**/*.js" ] } ================================================ FILE: projects/insper/design-system/package.json ================================================ { "name": "@techinsper/insper-design-system", "version": "0.21.0", "scripts": { "build": "npx run-s build:tsc build:copy", "build:tsc": "npx tsc -p tsconfig.schematics.json", "build:copy": "npx copyfiles schematics/*/schema.json schematics/*/files/** schematics/collection.json ../../../dist/insper/design-system" }, "peerDependencies": { "@angular/common": "^17.3.0", "@angular/core": "^17.3.0", "@tanstack/angular-table": "8.21.3" }, "dependencies": { "tslib": "^2.3.0" }, "sideEffects": false, "schematics": "./schematics/collection.json", "ng-add": { "save": "dependencies" } } ================================================ FILE: projects/insper/design-system/tsconfig.lib.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "../../../out-tsc/lib", "declaration": true, "declarationMap": true, "inlineSources": true, "types": [] }, "exclude": [ "**/*.spec.ts" ] } ================================================ FILE: projects/insper/design-system/tsconfig.lib.prod.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "./tsconfig.lib.json", "compilerOptions": { "declarationMap": false }, "angularCompilerOptions": { "compilationMode": "partial" } } ================================================ FILE: projects/insper/design-system/tsconfig.schematics.json ================================================ { "compilerOptions": { "baseUrl": ".", "lib": [ "es2018", "dom" ], "declaration": true, "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, "noFallthroughCasesInSwitch": true, "noImplicitAny": true, "noImplicitThis": true, "noUnusedParameters": true, "noUnusedLocals": true, "rootDir": "schematics", "outDir": "../../../dist/insper/design-system/schematics", "skipDefaultLibCheck": true, "skipLibCheck": true, "sourceMap": true, "strictNullChecks": true, "target": "es6", "types": [ "jasmine", "node" ] }, "include": [ "schematics/**/*" ], "exclude": [ "schematics/*/files/**/*" ] } ================================================ FILE: projects/insper/design-system/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "../../../out-tsc/spec", "types": [ "jasmine" ] }, "include": [ "**/*.spec.ts", "**/*.d.ts" ] } ================================================ FILE: projects/insper/design-system/.eslintrc.json ================================================ { "extends": "../../../.eslintrc.json", "ignorePatterns": [ "!**/*" ], "overrides": [ { "files": [ "*.ts" ], "rules": {} }, { "files": [ "*.html" ], "rules": {} } ] } ================================================ FILE: projects/insper/design-system/schematics/collection.json ================================================ { "$schema": "../../../../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { "description": "Adds Design System styles to the project.", "factory": "./ng-add/index#ngAdd" } } } ================================================ FILE: projects/insper/design-system/schematics/ng-add/index.ts ================================================ import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; export function ngAdd(): Rule { return (tree: Tree, context: SchematicContext) => { const workspaceConfig = tree.read('/angular.json'); if (!workspaceConfig) { throw new Error('Could not find Angular workspace configuration'); } const workspace = JSON.parse(workspaceConfig.toString()); const stylePath = 'node_modules/@techinsper/insper-design-system/src/lib/styles/ids-theme.css'; // Itera sobre todos os projetos do workspace //eslint-disable-next-line Object.entries(workspace.projects).forEach(([projectName, project]: [string, any]) => { // Verifica se o projeto tem configuração de build if (!project.architect?.build?.options) { context.logger.warn(`Project ${projectName} doesn't have build configuration, skipping...`); return; } project.architect.build.options.styles = project.architect.build.options.styles ?? []; // Adiciona o arquivo de estilo aos styles globais if (!project.architect.build.options.styles.includes(stylePath)) { project.architect.build.options.styles.push(stylePath); context.logger.info(`Added styles to project: ${projectName}`); } }); // Salva as alterações no angular.json tree.overwrite('/angular.json', JSON.stringify(workspace, null, 2)); context.logger.info('Updated global styles in angular.json for all applicable projects'); return tree; }; } ================================================ FILE: projects/insper/design-system/src/public-api.ts ================================================ // Components export * from './lib/components/accordion/accordion.component'; export * from './lib/components/avatar/avatar.component'; export * from './lib/components/breadcrumb/breadcrumb.component'; export * from './lib/components/button/button.component'; export * from './lib/components/card/card.component'; export * from './lib/components/chip/chip.component'; export * from './lib/components/container/container.component'; export * from './lib/components/divider/divider.component'; export * from './lib/components/form-field/form-field.component'; export * from './lib/components/grid-item/grid-item.component'; export * from './lib/components/grid/grid.component'; export * from './lib/components/icon/icon.component'; export * from './lib/components/input-checkbox/input-checkbox.component'; export * from './lib/components/input-text/input-text.component'; export * from './lib/components/modal/modal.component'; export * from './lib/components/search/search.component'; export * from './lib/components/stack/stack.component'; export * from './lib/components/tab/tab.component'; export * from './lib/components/table/table.component'; export * from './lib/components/toast/toast.component'; export * from './lib/components/toggle/toggle.component'; export * from './lib/components/tooltip/tooltip.component'; export * from './lib/components/typography/typography.component'; // Services export * from './lib/services/toast/toast.service'; // Styles export * from './lib/styles/theme/index'; // Base Module export * from './lib/components/components.module'; ================================================ FILE: projects/insper/design-system/src/assets/Acumin/Acumin-Pro-Extra-Condensed.woff ================================================ [Binary file] ================================================ FILE: projects/insper/design-system/src/assets/Acumin/Acumin-Pro-Extra-Condensed.woff2 ================================================ [Binary file] ================================================ FILE: projects/insper/design-system/src/lib/components/components.module.ts ================================================ import { NgModule } from '@angular/core'; import { AccordionModule } from './accordion/accordion.component'; import { AvatarModule } from './avatar/avatar.component'; import { BreadcrumbModule } from './breadcrumb/breadcrumb.component'; import { ButtonModule } from './button/button.component'; import { CardModule } from './card/card.component'; import { ChipModule } from './chip/chip.component'; import { ContainerModule } from './container/container.component'; import { DividerModule } from './divider/divider.component'; import { FormFieldModule } from './form-field/form-field.component'; import { GridItemModule } from './grid-item/grid-item.component'; import { GridModule } from './grid/grid.component'; import { IconModule } from './icon/icon.component'; import { InputCheckboxModule } from './input-checkbox/input-checkbox.component'; import { InputTextModule } from './input-text/input-text.component'; import { ModalModule } from './modal/modal.component'; import { SearchModule } from './search/search.component'; import { StackModule } from './stack/stack.component'; import { TabModule } from './tab/tab.component'; import { TableModule } from './table/table.component'; import { ToastModule } from './toast/toast.component'; import { ToggleModule } from './toggle/toggle.component'; import { TooltipModule } from './tooltip/tooltip.component'; import { TypographyModule } from './typography/typography.component'; @NgModule({ exports: [ TypographyModule, TooltipModule, ToggleModule, ToastModule, TableModule, TabModule, StackModule, ModalModule, InputCheckboxModule, InputTextModule, IconModule, GridModule, GridItemModule, FormFieldModule, ContainerModule, ChipModule, CardModule, ButtonModule, BreadcrumbModule, AvatarModule, AccordionModule, DividerModule, SearchModule, ], }) export class InsperDesignSystemModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/accordion/accordion.component.css ================================================ .ids-accordion-container { @apply flex flex-col max-w-2xl bg-gray-50 dark:bg-gray-900 text-gray-700 dark:text-gray-300 px-400 py-600 rounded-[12px]; } .ids-accordion-summary { @apply flex items-center justify-between cursor-pointer text-gray-900 dark:text-gray-100; } .ids-accordion-summary i { @apply text-gray-500; } .ids-accordion-summary-content { @apply inline-flex gap-300 items-center; } .ids-accordion-expand-icon { @apply transition-transform duration-300 ease-in-out; } .ids-accordion-expand-icon--expanded { @apply rotate-180; } .ids-accordion-details { @apply overflow-hidden max-h-0 transition-all duration-200 ease-in-out; } /*Added margin-top because should be a "gap" between summary and details, but gap render its 'height' even if hidden. */ .ids-accordion-details--expanded { @apply max-h-full mt-400 overflow-visible; } ================================================ FILE: projects/insper/design-system/src/lib/components/accordion/accordion.component.html ================================================

accordion works!

================================================ FILE: projects/insper/design-system/src/lib/components/accordion/accordion.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { AccordionComponent, AccordionDetailsComponent, AccordionSummaryComponent } from './accordion.component'; @Component({ standalone: true, selector: 'host-component', imports: [ AccordionComponent, AccordionDetailsComponent, AccordionSummaryComponent, ], template: ` Bandas Queen Metallica Guns n' Roses `, }) class HostComponent { icon?: string; expandIcon?: string; } describe('AccordionComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display details when summary is clicked', async () => { const accordionElement = fixture.debugElement.query(By.css('ids-accordion')).nativeElement; const summaryElement = accordionElement.querySelector('.ids-accordion-summary') as HTMLElement; expect(summaryElement).toBeTruthy(); const detailsElement = accordionElement.querySelector('.ids-accordion-details') as HTMLElement; expect(detailsElement).toBeTruthy(); expect(detailsElement.classList).not.toContain('ids-accordion-details--expanded'); summaryElement.click(); fixture.detectChanges(); expect(detailsElement.classList).toContain('ids-accordion-details--expanded'); }); it('should render icon and expandIcon when passed', () => { component.icon = 'star'; component.expandIcon = 'sunglasses'; fixture.detectChanges(); const summaryElement: HTMLElement = fixture.debugElement.query(By.css('.ids-accordion-summary')).nativeElement; const [iconEl, expandIconEl] = Array.from(summaryElement.querySelectorAll('i')) as HTMLElement[]; expect(iconEl).toBeTruthy(); expect(iconEl.classList).toContain('ids-icon'); expect(iconEl.classList).toMatch('star'); expect(expandIconEl).toBeTruthy(); expect(expandIconEl.classList).toContain('ids-icon'); expect(expandIconEl.classList).toMatch('sunglasses'); }); it('should render with open state when expanded is true', () => {}); }); ================================================ FILE: projects/insper/design-system/src/lib/components/accordion/accordion.component.ts ================================================ import { CommonModule } from '@angular/common'; import { Component, ContentChild, input, NgModule, signal, ViewEncapsulation, type AfterContentInit } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { IconComponent } from '../icon/icon.component'; import { TypographyComponent } from '../typography/typography.component'; @Component({ selector: 'ids-accordion-summary', standalone: true, imports: [IconComponent, CommonModule, TypographyComponent], encapsulation: ViewEncapsulation.None, template: `
@if(icon()) { }
`, }) export class AccordionSummaryComponent { /** * The Phosphor icon name to be used as the expand icon. * @default 'chevron-down' */ icon = input(); /** * The Phosphor icon name to be used as the expand icon. * @default 'chevron-down' */ expandIcon = input('arrow-down'); /** * BehaviorSubject to track the expanded state of the accordion. * * @default false */ expanded$ = new BehaviorSubject(false); protected toggle() { this.expanded$.next(!this.expanded$.getValue()); } } @Component({ selector: 'ids-accordion-details', standalone: true, encapsulation: ViewEncapsulation.None, template: `
`, }) export class AccordionDetailsComponent { expanded = signal(false); } /** * Accordion component that allows for collapsible sections with a summary and details. * @example * ```html * * * Section Title * * *

This is the content of the section.

*
*
*/ @Component({ selector: 'ids-accordion', standalone: true, imports: [], template: `
`, encapsulation: ViewEncapsulation.None, }) export class AccordionComponent implements AfterContentInit { @ContentChild(AccordionSummaryComponent) protected summary!: AccordionSummaryComponent; @ContentChild(AccordionDetailsComponent) protected details!: AccordionDetailsComponent; /** * Property to control the initial expanded state of the accordion. * * @default false */ expanded = input(false); ngAfterContentInit() { if (!this.summary || !this.details) { throw new Error('Accordion must have both ids-accordion-summary and ids-accordion-details components.'); } this.summary.expanded$.subscribe((expanded) => { this.details.expanded.set(expanded); }); this.summary.expanded$.next(this.expanded()); } } @NgModule({ imports: [AccordionComponent, AccordionSummaryComponent, AccordionDetailsComponent], exports: [AccordionComponent, AccordionSummaryComponent, AccordionDetailsComponent], }) export class AccordionModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/avatar/avatar.component.css ================================================ .ids-avatar { @apply inline-flex justify-center items-center bg-gray-300 rounded-200; } .ids-avatar--rounded { @apply rounded-full overflow-hidden; } .ids-avatar-size--lg { @apply w-14 h-14; } .ids-avatar-size--md { @apply w-12 h-12; } .ids-avatar-size--sm { @apply w-10 h-10; } .ids-avatar-image { @apply w-full h-full object-cover; } ================================================ FILE: projects/insper/design-system/src/lib/components/avatar/avatar.component.html ================================================ Avatar {{ label()?.charAt(0)?.toUpperCase() || "Label can't be undefined"}} ================================================ FILE: projects/insper/design-system/src/lib/components/avatar/avatar.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { AvatarComponent, type AvatarSize } from './avatar.component'; import { imageBase64 } from './utils/testing'; @Component({ standalone: true, selector: 'host-component', imports: [AvatarComponent], template: ` `, }) class HostComponent { image: string = ''; icon: string = ''; label: string = 'Label'; size: AvatarSize = 'md'; rounded: boolean = true; } describe('AvatarComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display an image when provided', () => { component.image = imageBase64; fixture.detectChanges(); const avatarEl: HTMLElement = fixture.debugElement.query(By.css('ids-avatar')).nativeElement; const imgEl = avatarEl.querySelector('.ids-avatar-image')!; expect(imgEl).toBeTruthy(); expect(imgEl.getAttribute('src')).toBe(imageBase64); expect(imgEl.getAttribute('alt')).toBe('Avatar'); }); it('should display an icon when provided', () => { component.icon = 'sunglasses'; fixture.detectChanges(); const avatarEl: HTMLElement = fixture.debugElement.query(By.css('ids-avatar')).nativeElement; const iconEl: HTMLElement = avatarEl.querySelector('[idsIcon]')!; expect(iconEl).toBeTruthy(); expect(iconEl.classList).toContain('ids-icon'); expect(iconEl.classList).toContain(`ph-${component.icon}`); }); it('should display a label when provided', () => { const label = 'Test Label'; component.label = label; fixture.detectChanges(); const avatarEl: HTMLElement = fixture.debugElement.query(By.css('ids-avatar')).nativeElement; expect(avatarEl).toBeTruthy(); expect(avatarEl.textContent).toContain(label.charAt(0).toUpperCase()); }); it('should throw an error if no image, icon, or label is provided', () => { const fixture = TestBed.createComponent(HostComponent); const component = fixture.componentInstance; component.image = ''; component.icon = ''; component.label = ''; expect(() => fixture.detectChanges()).toThrowError(); }); it('should allow only one of image, icon, or label to be provided', () => { const fixture = TestBed.createComponent(HostComponent); const component = fixture.componentInstance; component.image = imageBase64; component.icon = 'sunglasses'; component.label = 'Label'; expect(() => fixture.detectChanges()).toThrowError(); }); it('should apply the correct size class', () => { component.size = 'md'; fixture.detectChanges(); const avatarEl: HTMLElement = fixture.debugElement.query(By.css('ids-avatar')).nativeElement; expect(avatarEl.classList).toContain(`ids-avatar-size--${component.size}`); component.size = 'lg'; fixture.detectChanges(); expect(avatarEl.classList).toContain(`ids-avatar-size--${component.size}`); expect(avatarEl.classList).not.toContain('ids-avatar-size--md'); }); it('should apply rounded class when rounded is true', () => { component.rounded = true; fixture.detectChanges(); const avatarEl: HTMLElement = fixture.debugElement.query(By.css('ids-avatar')).nativeElement; expect(avatarEl.classList).toContain('ids-avatar--rounded'); component.rounded = false; fixture.detectChanges(); expect(avatarEl.classList).not.toContain('ids-avatar--rounded'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/avatar/avatar.component.ts ================================================ import { NgTemplateOutlet } from '@angular/common'; import { Component, computed, input, NgModule, TemplateRef, viewChild, ViewEncapsulation, type OnInit } from '@angular/core'; import { IconComponent } from '../icon/icon.component'; import { TypographyComponent } from '../typography/typography.component'; export type AvatarSize = 'sm' | 'md' | 'lg'; /** * AvatarComponent is a component that displays an avatar image, icon, or label. * * If no image, icon, or label is provided, it will throw an error. You should always provide only one of these inputs. */ @Component({ selector: 'ids-avatar', standalone: true, imports: [TypographyComponent, IconComponent, NgTemplateOutlet], templateUrl: './avatar.component.html', encapsulation: ViewEncapsulation.None, host: { '[class]':'avatarClass()', }, }) export class AvatarComponent implements OnInit { private imageTemplate = viewChild>('imageTemplate'); private iconTemplate = viewChild>('iconTemplate'); private labelTemplate = viewChild>('labelTemplate'); ngOnInit(): void { const inputVariants = [this.image(), this.icon(), this.label()].filter(Boolean); if(!inputVariants.length) { throw Error('AvatarComponent: At least one of "image", "icon", or "label" inputs must be provided.'); } if(inputVariants.length > 1) { throw Error('AvatarComponent: Only one of "image", "icon", or "label" inputs can be provided at a time.'); } } /** * The text label to display in the avatar. * @default undefined */ label = input(); /** * The image to display in the avatar. * It works like an `` tag, so it can be a URL or a Base64 string. * @default undefined */ image = input(); /** * The icon to display in the avatar. * It should be a valid icon name from Phosphor Icons. * @default undefined * @see https://phosphoricons.com/ */ icon = input(); /** * The size of the avatar. * @default 'md' */ size = input('md'); /** * Whether the avatar should have rounded corners. * @default true */ rounded = input(true); protected labelSize = computed(() => { switch (this.size()) { case 'sm': return 'lg'; case 'md': return 'xl'; default: return '2xl'; } }); protected avatarClass = computed(() => ({ 'ids-avatar': true, 'ids-avatar--rounded': this.rounded(), [`ids-avatar-size--${this.size()}`]: true, })); protected getAvatarTemplate() { if (this.image()) { return this.imageTemplate(); } if (this.icon()) { return this.iconTemplate(); } return this.labelTemplate(); } } @NgModule({ imports: [AvatarComponent], exports: [AvatarComponent], }) export class AvatarModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/avatar/utils/testing.ts ================================================ export const imageBase64 = 'data:image/jpeg;base64,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'; ================================================ FILE: projects/insper/design-system/src/lib/components/breadcrumb/breadcrumb.component.css ================================================ .ids-breadcrumb-container { @apply flex flex-wrap gap-200; } .ids-breadcrumb-item { @apply inline-flex items-center gap-100; } .ids-breadcrumb-separator { @apply text-gray-500; } .ids-breadcrumb-link { @apply inline-flex items-center gap-200 text-gray-500 dark:text-gray-400; } ================================================ FILE: projects/insper/design-system/src/lib/components/breadcrumb/breadcrumb.component.html ================================================ @if (!first) { } @if(item.iconName) { } @if(item.label) { {{ item.label }} } ================================================ FILE: projects/insper/design-system/src/lib/components/breadcrumb/breadcrumb.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { provideRouter, Router, type Routes } from '@angular/router'; import { BreadcrumbComponent, type BreadcrumbItem } from './breadcrumb.component'; @Component({ standalone: true, }) class DummyComponent {} const routes: Routes = [ { path: '', component: DummyComponent, }, { path: 'about', component: DummyComponent, }, { path: 'contact', component: DummyComponent, }, ]; @Component({ standalone: true, template: '', imports: [BreadcrumbComponent], }) class HostComponent { items: BreadcrumbItem[] = [ { label: 'Home', path: '/', queryParams: {} }, { label: 'About', path: '/about', queryParams: { name: 'john', age: 30 } }, { label: 'Contact', path: '/contact' }, { label: 'Google', externalUrl: 'https://www.google.com' }, ]; } describe('BreadcrumbComponent', () => { let component: HostComponent; let fixture: ComponentFixture; let router: Router; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], providers: [provideRouter(routes)], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; router = TestBed.inject(Router); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should render breadcrumb items', () => { const linksEl = fixture.debugElement.queryAll(By.css('.ids-breadcrumb-link')); expect(linksEl.length).toBe(component.items.length); linksEl.forEach((link, index) => { expect(link.nativeElement.textContent).toContain(component.items[index].label); expect(link.nativeElement.getAttribute('href')).toContain(component.items[index].path ?? component.items[index].externalUrl); }); }); it('should navigate to breadcrumb defined path', async () => { const linksEl = fixture.debugElement.queryAll(By.css('.ids-breadcrumb-link')); const aboutPath = `${component.items[1].path}?${new URLSearchParams(component.items[1].queryParams).toString()}`; const contactPath = component.items[2].path; linksEl[1].nativeElement.click(); await fixture.whenStable(); expect(router.url).toBe(aboutPath); router.navigateByUrl('/'); await fixture.whenStable(); expect(router.url).toBe('/'); linksEl[2].nativeElement.click(); await fixture.whenStable(); expect(router.url).toBe(contactPath!); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/breadcrumb/breadcrumb.component.ts ================================================ import { NgTemplateOutlet } from '@angular/common'; import { Component, input, NgModule, ViewEncapsulation } from '@angular/core'; import { RouterLink, type Params } from '@angular/router'; import { IconComponent } from '../icon/icon.component'; import { TypographyComponent } from '../typography/typography.component'; /** * You have to use at least one of the properties `label` or `iconName` in each item. * They can be combined. * You can use `path` to navigate within the application or `externalUrl` to link to an external site. * If you use `path`, you can also pass `queryParams` to include additional parameters in the route. */ export type BreadcrumbItem = ( | { label: string; iconName?: string } | { label?: string; iconName: string } ) & ( | { path: string; queryParams?: Params; externalUrl?: never } | { path?: never; queryParams?: never; externalUrl: string } ) @Component({ selector: 'ids-breadcrumb', standalone: true, imports: [ RouterLink, TypographyComponent, NgTemplateOutlet, IconComponent, ], templateUrl: './breadcrumb.component.html', encapsulation: ViewEncapsulation.None, }) export class BreadcrumbComponent { /** * Array of breadcrumb items to display. * Each item should have `label` and either `path` or `externalUrl` defined. You can also use `queryParams` to pass additional parameters for the route (if using `path`). * * @example * ```typescript * itemsWithPath = [ * { label: 'Home', path: '/' }, * { label: 'About', path: '/about', queryParams: { name: 'john', age: 30 } }, * ]; * * itemsWithExternalUrl = [ * { label: 'Google', externalUrl: 'https://www.google.com' }, * { label: 'Facebook', externalUrl: 'https://www.facebook.com' }, * ]; * ``` */ items = input.required(); } @NgModule({ imports: [BreadcrumbComponent], exports: [BreadcrumbComponent], }) export class BreadcrumbModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/button/button.component.css ================================================ .ids-btn { @apply box-border inline-flex gap-100 px-300 items-center justify-center rounded-200 cursor-pointer transition-all duration-200 ease-in-out; &:focus-visible { @apply outline-none; } &:disabled { @apply opacity-40; } } .ids-btn--fullwidth { @apply w-full; } /* Label */ .ids-btn-label-container { @apply px-100; } .ids-btn-loading { @apply animate-spin; } /* ------ Variants and colors --------- */ /* Fill */ .ids-btn-style-fill--neutral { @apply bg-gray-950 text-white-1000; &:hover { @apply bg-gray-800; } &:focus { @apply bg-gray-700 ring-2 ring-gray-950 ring-offset-2; } &:active { @apply bg-gray-950 text-white-1000; } } .ids-btn-style-fill--neutral.ids-btn--inverted { @apply bg-white-1000 text-gray-950; &:hover { @apply bg-white-800; } &:focus { @apply bg-white-800 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-white-1000 text-gray-950; } } .ids-btn-style-fill--danger { @apply bg-red-600 text-white-1000; &:hover { @apply bg-red-700; } &:focus { @apply bg-red-700 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-red-800; } } .ids-btn-style-fill--danger.ids-btn--inverted { @apply bg-red-600 text-white-1000; &:hover { @apply bg-red-700; } &:focus { @apply bg-red-700 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-red-800; } } /* Tonal */ .ids-btn-style-tonal--neutral { @apply bg-gray-100 text-gray-950; &:hover { @apply bg-gray-200; } &:focus { @apply bg-gray-200 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-gray-300; } } .ids-btn-style-tonal--neutral.ids-btn--inverted { @apply bg-white-100 text-white-1000; &:hover { @apply bg-white-200; } &:focus { @apply bg-white-200 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-white-300; } } .ids-btn-style-tonal--danger { @apply bg-gray-100 text-red-700; &:hover { @apply bg-red-700 text-white-1000; } &:focus { @apply bg-red-700 text-white-1000 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-red-800; } } .ids-btn-style-tonal--danger.ids-btn--inverted { @apply bg-white-100 text-red-400; &:hover { @apply bg-red-700 text-white-1000; } &:focus { @apply bg-red-700 text-white-1000 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-red-800; } } /* Outline */ .ids-btn-style-outline--neutral { @apply border-1 border-gray-950 text-gray-950; &:hover { @apply bg-gray-100 text-gray-950; } &:focus { @apply bg-gray-100 text-gray-950 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-gray-200; } } .ids-btn-style-outline--neutral.ids-btn--inverted { @apply border-1 border-white-1000 text-white-1000; &:hover { @apply bg-white-200 text-white-1000; } &:focus { @apply bg-white-200 text-white-1000 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-white-300; } } .ids-btn-style-outline--danger { @apply border-1 border-red-700 text-red-700; &:hover { @apply bg-red-700 text-white-1000; } &:focus { @apply bg-red-700 text-white-1000 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-red-800; } } .ids-btn-style-outline--danger.ids-btn--inverted { @apply border-1 border-red-400 text-red-400; &:hover { @apply border-red-700 bg-red-700 text-white-1000; } &:focus { @apply border-red-700 bg-red-700 text-white-1000 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-red-800; } } /* Ghost */ .ids-btn-style-ghost--neutral { @apply bg-transparent text-gray-950; &:hover { @apply bg-gray-100 text-gray-950; } &:focus { @apply bg-gray-100 text-gray-950 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-gray-200; } } .ids-btn-style-ghost--neutral.ids-btn--inverted { @apply bg-transparent text-white-1000; &:hover { @apply bg-white-100 text-white-1000; } &:focus { @apply bg-white-100 text-white-1000 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-white-200; } } .ids-btn-style-ghost--danger { @apply bg-transparent text-red-700; &:hover { @apply bg-red-700 text-white-1000; } &:focus { @apply bg-red-700 text-white-1000 ring-2 ring-gray-950 ring-offset-2 ring-offset-white-1000; } &:active { @apply bg-red-800; } } .ids-btn-style-ghost--danger.ids-btn--inverted { @apply bg-transparent text-red-400; &:hover { @apply bg-red-700 text-white-1000; } &:focus { @apply bg-red-700 text-white-1000 ring-2 ring-white-1000 ring-offset-2 ring-offset-gray-950; } &:active { @apply bg-red-800; } } /* ---------- Shapes and Sizes --------------- */ /* Rectangle */ .ids-btn-shape--rectangle.ids-btn-size--sm { @apply h-800 px-200; } .ids-btn-shape--rectangle.ids-btn-size--md { @apply h-1000 px-300; } .ids-btn-shape--rectangle.ids-btn-size--lg { @apply h-1200 px-400; } .ids-btn-shape--rectangle.ids-btn-size--xl { @apply h-1400 px-500; } /* Square */ .ids-btn-shape--square.ids-btn-size--sm { @apply h-800 w-800; } .ids-btn-shape--square.ids-btn-size--md { @apply h-1000 w-1000; } .ids-btn-shape--square.ids-btn-size--lg { @apply h-1200 w-1200; } .ids-btn-shape--square.ids-btn-size--xl { @apply h-1400 w-1400; } /* Circle */ .ids-btn-shape--circle.ids-btn-size--sm { @apply rounded-full h-800 w-800; } .ids-btn-shape--circle.ids-btn-size--md { @apply rounded-full h-1000 w-1000; } .ids-btn-shape--circle.ids-btn-size--lg { @apply rounded-full h-1200 w-1200; } .ids-btn-shape--circle.ids-btn-size--xl { @apply rounded-full h-1400 w-1400; } ================================================ FILE: projects/insper/design-system/src/lib/components/button/button.component.html ================================================ @if(loading()) { } @else { } @if(leadingIcon()) { } @if(icon()) { } @else { } @if(trailingIcon()) { } ================================================ FILE: projects/insper/design-system/src/lib/components/button/button.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ButtonComponent } from './button.component'; @Component({ imports: [ButtonComponent], template: ` `, standalone: true, }) class HostComponent { disabled: boolean = false; loading: boolean = false; fullwidth: boolean = false; inverted: boolean = false; variant: 'fill' | 'tonal' | 'outline' | 'ghost' = 'fill'; color: 'neutral' | 'danger' = 'neutral'; shape: 'rectangle' | 'circle' | 'square' = 'rectangle'; leadingIcon: string | null = null; icon: string | null = null; trailingIcon: string | null = null; size: 'sm' | 'md' | 'lg' | 'xl' = 'md'; content: string = 'Button'; onClick(event: MouseEvent) { return event; } onKeydown(event: KeyboardEvent) { return event; } } @Component({ standalone: true, template: ` `, }) class ErrorHostComponent { icon = 'plus'; shape = 'rectangle'; } describe('ButtonComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should be loading', () => { component.loading = true; fixture.detectChanges(); const loading = fixture.debugElement.query(By.css('.ids-btn-loading')); expect(loading).toBeDefined(); }); it('should be fullwidth when property is true', () => { component.fullwidth = true; fixture.detectChanges(); const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn--fullwidth'); }); it('should invert colors when inverted is true', () => { component.inverted = true; fixture.detectChanges(); const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn--inverted'); }); it('should allow change shape', () => { const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn-shape--rectangle'); component.shape = 'circle'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-shape--circle'); expect(buttonEl.classList).not.toContain('ids-btn-shape--rectangle'); component.shape = 'square'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-shape--square'); expect(buttonEl.classList).not.toContain('ids-btn-shape--circle'); }); it('should not render icon with rectangle shape', () => { expect(() => { TestBed.createComponent(ErrorHostComponent); }).toThrowError(); }); it('should render icon with circle or square shape', () => { component.icon = 'plus'; component.shape = 'circle'; fixture.detectChanges(); const circleButtonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; const circleIconEl = circleButtonEl.querySelector('[idsIcon]'); expect(circleIconEl).toBeTruthy(); component.shape = 'square'; fixture.detectChanges(); const squareButtonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; const squareIconEl = squareButtonEl.querySelector('[idsIcon]'); expect(squareIconEl).toBeTruthy(); }); it('should allow change variants', () => { const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn-style-fill--neutral'); component.variant = 'tonal'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-style-tonal--neutral'); expect(buttonEl.classList).not.toContain('ids-btn-style-fill--neutral'); component.variant = 'outline'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-style-outline--neutral'); expect(buttonEl.classList).not.toContain('ids-btn-style-tonal--neutral'); component.variant = 'ghost'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-style-ghost--neutral'); expect(buttonEl.classList).not.toContain('ids-btn-style-outline--neutral'); }); it('should allow change shapes', () => { const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn-shape--rectangle'); component.shape = 'circle'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-shape--circle'); expect(buttonEl.classList).not.toContain('ids-btn-shape--rectangle'); component.shape = 'square'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-shape--square'); expect(buttonEl.classList).not.toContain('ids-btn-shape--circle'); }); it('should allow change colors', () => { const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn-style-fill--neutral'); component.color = 'danger'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-style-fill--danger'); expect(buttonEl.classList).not.toContain('ids-btn-style-fill--neutral'); }); it('should allow change sizes', () => { const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.classList).toContain('ids-btn-size--md'); component.size = 'sm'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-size--sm'); expect(buttonEl.classList).not.toContain('ids-btn-size--md'); component.size = 'lg'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-size--lg'); expect(buttonEl.classList).not.toContain('ids-btn-size--sm'); component.size = 'xl'; fixture.detectChanges(); expect(buttonEl.classList).toContain('ids-btn-size--xl'); expect(buttonEl.classList).not.toContain('ids-btn-size--lg'); }); it('should render projected content', () => { component.content = 'Button with projected content'; fixture.detectChanges(); const buttonEl = fixture.debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(buttonEl.textContent).toContain(component.content); }); it('should emit event on click', () => { spyOn(component, 'onClick'); const buttonEl = fixture.debugElement.query(By.css('button')); buttonEl.triggerEventHandler('click', new MouseEvent('click')); fixture.detectChanges(); expect(component.onClick).toHaveBeenCalledWith(jasmine.any(MouseEvent)); }); it('should emit event on Enter keydown', () => { spyOn(component, 'onKeydown'); const buttonEl = fixture.debugElement.query(By.css('button')); buttonEl.triggerEventHandler('keydown', new KeyboardEvent('keydown', { key: 'Enter' })); fixture.detectChanges(); expect(component.onKeydown).toHaveBeenCalledWith(jasmine.any(KeyboardEvent)); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/button/button.component.ts ================================================ import { CommonModule } from '@angular/common'; import { Component, computed, input, NgModule, ViewEncapsulation, type OnInit } from '@angular/core'; import { IconComponent } from '../icon/icon.component'; import { TypographyComponent } from '../typography/typography.component'; type ButtonSize = 'sm' | 'md' | 'lg' | 'xl'; type ButtonVariant = 'fill' |'tonal' | 'outline' | 'ghost'; type ButtonColor = 'neutral' | 'danger' type ButtonShape = 'rectangle' | 'circle' | 'square'; /** * Button component for the Insper Design System. * * You can use this component along with the `idsButton` directive on a button or anchor element. * ```html * * * * Button * * ``` */ @Component({ selector: 'button[idsButton], a[idsButton]', standalone: true, templateUrl: './button.component.html', imports: [TypographyComponent, IconComponent, CommonModule], encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()', }, }) export class ButtonComponent implements OnInit{ ngOnInit() { if(this.icon() && this.shape() === 'rectangle') { throw new Error('The `icon` property should only be used with shape `circle` or `square`.'); } } /** * Whether the button is in a loading state. * * This will disable the button and show a loading indicator. * @default false */ loading = input(); /** * The variant style of the button. * @default 'fill' */ variant = input('fill'); /** * The variant color of the button. * * This is used to determine the color of the button. * @default 'neutral' */ color = input('neutral'); /** * Whether the button colors are inverted or not. * * @default false */ inverted = input(false); /** * The shape of the button. * * This can be `rectangle`, `circle`, or `square`. * @default 'rectangle' */ shape = input('rectangle'); /** * The leading icon to display in the button. * * Should be a valid phosphor icon name. * @default undefined */ leadingIcon = input(); /** * The icon to display in the button. * * Should be a valid phosphor icon name and use shape `circle` or `square`. * @default undefined */ icon = input(); /** * The trailing icon to display in the button. * * Should be a valid phosphor icon name. * @default undefined */ trailingIcon = input(); /** * The size of the button. * @default 'md' */ size = input('md'); /** * Whether the button should take the full width of its container. * * This will apply `width: 100%` to the button. * @default false */ fullwidth = input(false); protected iconSize = computed(() => { // The icon size is derivated from the button size. // According to the design system docs: // - `sm` -> `16` // - `md` -> `20` // - `lg` -> `24` // - `xl` -> `28` if(this.size() === 'md' || this.size() === 'lg') return '20'; if(this.size() === 'xl') return '24'; return '16'; }); protected classes = computed(() => ({ ['ids-btn']: true, [`ids-btn-style-${this.variant()}--${this.color()}`]: true, [`ids-btn-size--${this.size()}`]: true, [`ids-btn-shape--${this.shape()}`]: true, ['ids-btn--inverted']: this.inverted(), ['ids-btn--fullwidth']: this.fullwidth(), })); } @NgModule({ imports: [ButtonComponent], exports: [ButtonComponent], }) export class ButtonModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/card/card.component.css ================================================ .ids-card-container { @apply flex flex-col gap-8 border-2 border-dark-brand-600 rounded-lg shadow-md p-4; } .ids-card-section { @apply flex flex-col gap-4; } ================================================ FILE: projects/insper/design-system/src/lib/components/card/card.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/card/card.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CardComponent } from './card.component'; describe('CardComponent', () => { let component: CardComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CardComponent], }) .compileComponents(); fixture = TestBed.createComponent(CardComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/card/card.component.ts ================================================ import { Component, Directive, input, NgModule, ViewEncapsulation } from '@angular/core'; @Directive({ selector: '[idsCardAlignment]', standalone: true, host: { '[class.items-start]': 'alignment() === "flex-start"', '[class.items-center]': 'alignment() === "center"', '[class.items-end]': 'alignment() === "flex-end"', }, }) export class AlignmentDirective { alignment = input<'center' | 'flex-start' | 'flex-end'>('flex-start'); } @Component({ selector: 'ids-card-header', standalone: true, hostDirectives: [{ directive: AlignmentDirective, inputs: ['alignment'], }], host: { class: 'ids-card-section', }, template: '', encapsulation: ViewEncapsulation.None, }) export class CardHeaderComponent {} @Component({ selector: 'ids-card-content', standalone: true, hostDirectives: [{ directive: AlignmentDirective, inputs: ['alignment'], }], host: { class: 'ids-card-section', }, template: '', encapsulation: ViewEncapsulation.None, }) export class CardContentComponent {} @Component({ selector: 'ids-card-footer', standalone: true, hostDirectives: [{ directive: AlignmentDirective, inputs: ['alignment'], }], host: { class: 'ids-card-section', }, template: '', encapsulation: ViewEncapsulation.None, }) export class CardFooterComponent {} @Component({ selector: 'ids-card', standalone: true, imports: [], templateUrl: './card.component.html', encapsulation: ViewEncapsulation.None, }) export class CardComponent {} @NgModule({ imports: [CardComponent, CardHeaderComponent, CardContentComponent, CardFooterComponent, AlignmentDirective], exports: [CardComponent, CardHeaderComponent, CardContentComponent, CardFooterComponent, AlignmentDirective], }) export class CardModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/chip/chip.component.css ================================================ .ids-chip { @apply h-800 inline-flex items-center rounded-full px-300 font-sans text-sm text-gray-900 dark:text-gray-100 bg-gray-200 dark:bg-gray-800 transition-all duration-200 ease-in-out cursor-pointer; } .ids-chip:focus, .ids-chip:focus-visible { @apply outline-none ring-2 ring-gray-700 ring-offset-2 ring-offset-white; } .ids-chip--disabled { @apply opacity-50 cursor-default; } .ids-chip--disabled:focus, .ids-chip--disabled:focus-visible { @apply outline-none ring-0 ring-offset-0; } .ids-chip-content { @apply px-200 font-medium; } .ids-chip--selected { @apply bg-gray-900 dark:bg-white-1000 text-gray-50 dark:text-gray-950; } .ids-chip-delete { @apply cursor-pointer align-middle rounded-full; } ================================================ FILE: projects/insper/design-system/src/lib/components/chip/chip.component.html ================================================ @if(!isDestroyed()) { @if(icon()) { } {{ label()}} @if(deletable()) { } } ================================================ FILE: projects/insper/design-system/src/lib/components/chip/chip.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ChipComponent } from './chip.component'; @Component({ standalone: true, imports: [ChipComponent], template: ` `, }) class HostComponent { label = 'Chip'; selected = false; disabled = false; icon = ''; deletable = false; deleteIcon = 'x-circle'; onClick() {} onKeydown() {} } describe('ChipComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should render the chip with label', () => { const chipEl = fixture.debugElement.query(By.css('ids-chip')).nativeElement as HTMLElement; expect(chipEl).toBeTruthy(); expect(chipEl.textContent).toContain(component.label); }); it('should render with icon when icon is provided', () => { component.icon = 'check'; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('ids-chip')); const iconEl = chipEl.query(By.css('i')); expect(iconEl).toBeTruthy(); expect(iconEl.nativeElement.classList).toMatch(component.icon); }); it('should change styles when active is true', () => { component.selected = true; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('.ids-chip')); expect(chipEl.nativeElement.classList).toMatch('ids-chip--selected'); }); it('should handle click events', () => { spyOn(component, 'onClick'); const chipEl = fixture.debugElement.query(By.css('ids-chip')); chipEl.triggerEventHandler('click'); expect(component.onClick).toHaveBeenCalled(); }); it('should handle keydown events', () => { spyOn(component, 'onKeydown'); const chipEl = fixture.debugElement.query(By.css('ids-chip')); chipEl.triggerEventHandler('keydown', new KeyboardEvent('keydown', { key: 'Enter' })); expect(component.onKeydown).toHaveBeenCalled(); }); it('should render with deletable icon when deletable is true', () => { component.deletable = true; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('ids-chip')); const deleteIcon = chipEl.query(By.css('.ids-chip-delete')); expect(deleteIcon).toBeTruthy(); expect(deleteIcon.nativeElement.classList).toMatch(component.deleteIcon); }); it('should not render delete icon when deletable is false', () => { component.deletable = false; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('ids-chip')); const deleteIcon = chipEl.query(By.css('.ids-chip-delete')); expect(deleteIcon).toBeFalsy(); }); // The component is not destroyed on delete icon click as it just removes the chip from the view, // but the ng-container remains in the DOM. it('should destroy component when click on delete icon', () => { component.deletable = true; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('ids-chip')); const deleteIcon = chipEl.query(By.css('.ids-chip-delete')); expect(deleteIcon).toBeTruthy(); deleteIcon.triggerEventHandler('click', new MouseEvent('click')); fixture.detectChanges(); const updatedchipEl = fixture.debugElement.query(By.css('ids-chip')).nativeElement as HTMLElement; expect(updatedchipEl.textContent).toBeFalsy(); }); it('should destroy component when keydown on delete icon', async () => { component.deletable = true; fixture.detectChanges(); const chipEl = fixture.debugElement.query(By.css('ids-chip')); const deleteIcon = chipEl.query(By.css('.ids-chip-delete')).nativeElement as HTMLElement; expect(deleteIcon).toBeTruthy(); deleteIcon.focus(); fixture.detectChanges(); const keydownEvent = new KeyboardEvent('keydown', { key: 'Enter' }); deleteIcon.dispatchEvent(keydownEvent); fixture.detectChanges(); const updatedchipEl = fixture.debugElement.query(By.css('ids-chip')).nativeElement as HTMLElement; expect(updatedchipEl.textContent).toBeFalsy(); }); it('should not emit click event when disabled', () => { component.disabled = true; fixture.detectChanges(); spyOn(component, 'onClick'); const chipEl = fixture.debugElement.query(By.css('.ids-chip')); chipEl.triggerEventHandler('click', new MouseEvent('click')); expect(component.onClick).not.toHaveBeenCalled(); }); it('should not emit keydown event when disabled', () => { component.disabled = true; fixture.detectChanges(); spyOn(component, 'onKeydown'); const chipEl = fixture.debugElement.query(By.css('.ids-chip')); chipEl.triggerEventHandler('keydown', new KeyboardEvent('keydown', { key: 'Enter' })); expect(component.onKeydown).not.toHaveBeenCalled(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/chip/chip.component.ts ================================================ import { Component, computed, input, NgModule, output, signal, ViewEncapsulation } from '@angular/core'; import { IconComponent } from '../icon/icon.component'; import { TypographyComponent } from '../typography/typography.component'; @Component({ selector: 'ids-chip', standalone: true, imports: [IconComponent, TypographyComponent], templateUrl: './chip.component.html', encapsulation: ViewEncapsulation.None, }) export class ChipComponent { protected isDestroyed = signal(false); /** * The icon to be displayed on the chip. * This should be the name of the icon from Phosphor Icons Library. * * @see https://phosphoricons.com/ */ deleteIcon = input('x-circle'); /** * If `true`, the chip will have a delete icon and will be clickable. * The delete icon will be displayed on the right side of the chip. */ deletable = input(false); /** * The label to be displayed on the chip. * This is a required input. */ label = input.required(); /** * The icon to be displayed on the left side of the chip. * This should be the name of the icon from Phosphor Icons Library. * * @see https://phosphoricons.com/ */ icon = input(); /** * If `true`, the chip will have a different style. * This will apply a background color and text color to the chip. */ selected = input(false); /** * If `true`, the chip will be disabled. * This will prevent any interaction with the chip. * * @default false */ disabled = input(false); click = output(); keydown = output(); protected containerClasses = computed(() => ({ 'ids-chip': true, 'ids-chip--selected': this.selected(), 'ids-chip--disabled': this.disabled(), })); protected handleClick(event: MouseEvent) { if (this.disabled()) return; event.stopPropagation(); event.stopImmediatePropagation(); this.click.emit(event); } protected handleKeydown(event: Event) { if (this.disabled()) return; event.stopPropagation(); event.stopImmediatePropagation(); this.keydown.emit(event); } protected handleDeleteInteraction(event: MouseEvent | Event) { if (this.disabled()) return; event.stopPropagation(); event.stopImmediatePropagation(); this.isDestroyed.set(true); } } @NgModule({ imports: [ChipComponent], exports: [ChipComponent], }) export class ChipModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/container/container.component.css ================================================ .ids-container { @apply block max-w-2xl p-400 md:p-800 mx-auto; } ================================================ FILE: projects/insper/design-system/src/lib/components/container/container.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ContainerComponent } from './container.component'; describe('ContainerComponent', () => { let component: ContainerComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ContainerComponent], }) .compileComponents(); fixture = TestBed.createComponent(ContainerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render with default size and not centralized', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/container/container.component.ts ================================================ import { Component, NgModule, ViewEncapsulation } from '@angular/core'; /** * Generic container. * * Should be used as directive, like: * ```html *
...
* ``` */ @Component({ selector: '[idsContainer]', standalone: true, imports: [], template: '', host: { class: 'ids-container', }, encapsulation: ViewEncapsulation.None, }) export class ContainerComponent {} @NgModule({ imports: [ContainerComponent], exports: [ContainerComponent], }) export class ContainerModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/divider/divider.component.css ================================================ .ids-divider { @apply block h-[1px] border-gray-200 dark:border-gray-800 my-800; } .ids-divider-vertical { @apply inline-block h-full w-[1px] bg-gray-200 dark:bg-gray-800 mx-800; } ================================================ FILE: projects/insper/design-system/src/lib/components/divider/divider.component.html ================================================ @if(vertical()) { } @else {
} ================================================ FILE: projects/insper/design-system/src/lib/components/divider/divider.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { DividerComponent } from './divider.component'; @Component({ selector: 'host-component', template: '', standalone: true, imports: [DividerComponent], }) class HostComponent { vertical = false; } describe('DividerComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render with default props', () => { const dividerEl: HTMLHRElement = fixture.debugElement.query(By.css('.ids-divider')).nativeElement; expect(dividerEl.tagName).toBe('HR'); expect(dividerEl.classList).toContain('ids-divider'); expect(dividerEl.getAttribute('aria-orientation')).toBe('horizontal'); }); it('should render vertical divider when vertical input is true', () => { component.vertical = true; fixture.detectChanges(); const dividerEl: HTMLDivElement = fixture.debugElement.query(By.css('.ids-divider-vertical')).nativeElement; expect(dividerEl.tagName).toBe('DIV'); expect(dividerEl.getAttribute('role')).toBe('separator'); expect(dividerEl.classList).toContain('ids-divider-vertical'); expect(dividerEl.getAttribute('aria-orientation')).toBe('vertical'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/divider/divider.component.ts ================================================ import { Component, input, NgModule, ViewEncapsulation } from '@angular/core'; @Component({ selector: 'ids-divider', standalone: true, imports: [], templateUrl: './divider.component.html', encapsulation: ViewEncapsulation.None, }) export class DividerComponent { /** * If true, the divider will be vertical. * * @default false */ vertical = input(false); } @NgModule({ imports: [DividerComponent], exports: [DividerComponent], }) export class DividerModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/form-field/form-field.component.css ================================================ .ids-form-field { @apply w-full flex justify-between items-center justify-self-start gap-400 p-300 rounded-200 border-1 border-gray-300 dark:border-gray-700 text-gray-500 bg-white-1000 dark:bg-gray-950 transition-all duration-300 ease-in-out overflow-visible; } .ids-input-text-wrapper { @apply w-full flex gap-200 items-center; } .ids-form-field:has(input:focus), .ids-form-field:has(input:focus-visible) { @apply ring-2 ring-gray-700 ring-offset-2 ring-offset-white; } .ids-form-field:has(input:disabled) { @apply opacity-50; } ================================================ FILE: projects/insper/design-system/src/lib/components/form-field/form-field.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/form-field/form-field.component.spec.ts ================================================ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { IconComponent } from '../icon/icon.component'; import { InputTextComponent } from '../input-text/input-text.component'; import { FormFieldComponent } from './form-field.component'; @Component({ standalone: true, selector: 'ids-host-component', imports: [FormFieldComponent, InputTextComponent, IconComponent], template: ` `, }) class HostComponent {} describe('FormItemComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render with default props', () => { const formFieldEl: HTMLElement = fixture.debugElement.query(By.css('ids-form-field')).nativeElement; expect(component).toBeTruthy(); expect(formFieldEl).toBeTruthy(); expect(formFieldEl.classList).toContain('ids-form-field'); expect(formFieldEl.querySelector('input')).toBeTruthy(); }); it('should render with leading and trailing icons when slots are used', () => { fixture.detectChanges(); const formFieldEl: HTMLElement = fixture.debugElement.query(By.css('ids-form-field')).nativeElement; const iconsEl = Array.from(formFieldEl.querySelectorAll('i[idsIcon]')); iconsEl.forEach(icon => { expect(icon).toBeTruthy(); expect(icon.classList).toContain('ids-icon'); }); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/form-field/form-field.component.ts ================================================ import { Component, NgModule, ViewEncapsulation } from '@angular/core'; /** * Form item component that wraps an input element. * * @example * ```html * * * * ``` * * It can also be used with leading and trailing icons, as it supports the `slot="start"` and `slot="end"` attributes. * ```html * * * * * * ``` */ @Component({ selector: 'ids-form-field', standalone: true, imports: [], templateUrl: './form-field.component.html', encapsulation: ViewEncapsulation.None, host: { class: 'ids-form-field', }, }) export class FormFieldComponent {} @NgModule({ imports: [FormFieldComponent], exports: [FormFieldComponent], }) export class FormFieldModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/grid/grid.component.css ================================================ .ids-grid { @apply grid grid-cols-12 auto-rows-auto gap-400 md:gap-800; } /* Alignment */ .ids-grid-align--start { @apply items-start; } .ids-grid-align--center { @apply items-center; } .ids-grid-align--end { @apply items-end; } .ids-grid-align--stretch { @apply items-stretch; } /* Justification */ .ids-grid-justify--start { @apply justify-items-start; } .ids-grid-justify--center { @apply justify-items-center; } .ids-grid-justify--end { @apply justify-items-end; } .ids-grid-justify--stretch { @apply justify-items-stretch; } ================================================ FILE: projects/insper/design-system/src/lib/components/grid/grid.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { GridItemComponent } from '../grid-item/grid-item.component'; import { GridComponent, type GridAlign, type GridJustify } from './grid.component'; @Component({ standalone: true, selector: 'host-component', imports: [GridComponent, GridItemComponent], template: ` Item 1 Item 2 Item 3 `, }) class HostComponent { alignItem: GridAlign = 'stretch'; justifyItem: GridJustify = 'stretch'; container: boolean = false; xs: number = 4; sm?: number; md?: number; lg?: number; xl?: number; xxl?: number; } describe('GridComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render with default props', () => { const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); expect(component).toBeTruthy(); expect(gridEl.classList).toContain('ids-grid'); expect(gridEl.classList).toContain('ids-grid-align--stretch'); expect(gridEl.classList).toContain('ids-grid-justify--stretch'); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--xs-colspan-4'); expect(item.textContent).toContain('Item'); }); }); it('should allow change grid alignment and justification', () => { component.alignItem = 'end'; component.justifyItem = 'end'; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; expect(gridEl.classList).toContain('ids-grid-align--end'); expect(gridEl.classList).toContain('ids-grid-justify--end'); expect(gridEl.classList).not.toContain('ids-grid-align--stretch'); expect(gridEl.classList).not.toContain('ids-grid-justify--stretch'); component.alignItem = 'start'; component.justifyItem = 'start'; fixture.detectChanges(); expect(gridEl.classList).toContain('ids-grid-align--start'); expect(gridEl.classList).toContain('ids-grid-justify--start'); expect(gridEl.classList).not.toContain('ids-grid-align--end'); expect(gridEl.classList).not.toContain('ids-grid-justify--end'); }); it('should allow set colspan for sm breakpoint', () => { component.sm = 4; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--xs-colspan-4'); }); }); it('should allow set colspan for md breakpoint', () => { component.md = 4; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--md-colspan-4'); }); }); it('should allow set colspan for lg breakpoint', () => { component.lg = 4; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--lg-colspan-4'); }); }); it('should allow set colspan for xl breakpoint', () => { component.xl = 4; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--xl-colspan-4'); }); }); it('should allow set colspan for xxl breakpoint', () => { component.xxl = 4; fixture.detectChanges(); const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); gridItemsEl.forEach((item) => { expect(item.classList).toContain('ids-grid-item--xxl-colspan-4'); }); }); it('should throw error if none colspan is defined', () => { const fixture = TestBed.createComponent(HostComponent); const component = fixture.componentInstance; component.xs = 0; component.sm = 0; component.md = 0; component.lg = 0; component.xl = 0; component.xxl = 0; expect(() => fixture.detectChanges()).toThrowError(); }); it('should allow use grid item as a container', () => { // Set the first item as a container component.container = true; const gridEl: HTMLElement = fixture.debugElement.query(By.css('ids-grid')).nativeElement; const gridItemsEl = Array.from(gridEl.querySelectorAll('ids-grid-item')); // Check if the first item is a container const firstItem = gridItemsEl[0]; fixture.detectChanges(); expect(firstItem.classList).toContain('ids-grid-item--container'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/grid/grid.component.ts ================================================ import { Component, computed, input, NgModule, ViewEncapsulation } from '@angular/core'; export type GridAlign = 'start' | 'center' | 'end' | 'stretch'; export type GridJustify = 'start' | 'center' | 'end' | 'stretch'; /** * This component should be used to create a grid layout. * * Use it along with `ids-grid-item` components to create a grid structure. */ @Component({ selector: 'ids-grid', standalone: true, imports: [], template: '', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()', }, }) export class GridComponent { /** * The alignment of the grid item along Y axis * * @default stretch */ alignItem = input('stretch'); /** * The alignment of the grid item along X axis * * @default stretch */ justifyItem = input('stretch'); classes = computed(() => ({ 'ids-grid': true, [`ids-grid-align--${this.alignItem()}`]: true, [`ids-grid-justify--${this.justifyItem()}`]: true, })); } @NgModule({ imports: [GridComponent], exports: [GridComponent], }) export class GridModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/grid-item/grid-item.component.css ================================================ .ids-grid-item--container { @apply grid grid-cols-12; } /* Column Span */ .ids-grid-item--colspan-1 { @apply col-span-1; } .ids-grid-item--colspan-2 { @apply col-span-2; } .ids-grid-item--colspan-3 { @apply col-span-3; } .ids-grid-item--colspan-4 { @apply col-span-4; } .ids-grid-item--colspan-5 { @apply col-span-5; } .ids-grid-item--colspan-6 { @apply col-span-6; } .ids-grid-item--colspan-7 { @apply col-span-7; } .ids-grid-item--colspan-8 { @apply col-span-8; } .ids-grid-item--colspan-9 { @apply col-span-9; } .ids-grid-item--colspan-10 { @apply col-span-10; } .ids-grid-item--colspan-11 { @apply col-span-11; } .ids-grid-item--colspan-12 { @apply col-span-12; } /* Extra small colspan */ .ids-grid-item--xs-colspan-1 { @apply col-span-1; } .ids-grid-item--xs-colspan-2 { @apply col-span-2; } .ids-grid-item--xs-colspan-3 { @apply col-span-3; } .ids-grid-item--xs-colspan-4 { @apply col-span-4; } .ids-grid-item--xs-colspan-5 { @apply col-span-5; } .ids-grid-item--xs-colspan-6 { @apply col-span-6; } .ids-grid-item--xs-colspan-7 { @apply col-span-7; } .ids-grid-item--xs-colspan-8 { @apply col-span-8; } .ids-grid-item--xs-colspan-9 { @apply col-span-9; } .ids-grid-item--xs-colspan-10 { @apply col-span-10; } .ids-grid-item--xs-colspan-11 { @apply col-span-11; } .ids-grid-item--xs-colspan-12 { @apply col-span-12; } /* Small colspan */ .ids-grid-item--sm-colspan-1 { @apply sm:col-span-1; } .ids-grid-item--sm-colspan-2 { @apply sm:col-span-2; } .ids-grid-item--sm-colspan-3 { @apply sm:col-span-3; } .ids-grid-item--sm-colspan-4 { @apply sm:col-span-4; } .ids-grid-item--sm-colspan-5 { @apply sm:col-span-5; } .ids-grid-item--sm-colspan-6 { @apply sm:col-span-6; } .ids-grid-item--sm-colspan-7 { @apply sm:col-span-7; } .ids-grid-item--sm-colspan-8 { @apply sm:col-span-8; } .ids-grid-item--sm-colspan-9 { @apply sm:col-span-9; } .ids-grid-item--sm-colspan-10 { @apply sm:col-span-10; } .ids-grid-item--sm-colspan-11 { @apply sm:col-span-11; } .ids-grid-item--sm-colspan-12 { @apply sm:col-span-12; } /* Medium colspan */ .ids-grid-item--md-colspan-1 { @apply md:col-span-1; } .ids-grid-item--md-colspan-2 { @apply md:col-span-2; } .ids-grid-item--md-colspan-3 { @apply md:col-span-3; } .ids-grid-item--md-colspan-4 { @apply md:col-span-4; } .ids-grid-item--md-colspan-5 { @apply md:col-span-5; } .ids-grid-item--md-colspan-6 { @apply md:col-span-6; } .ids-grid-item--md-colspan-7 { @apply md:col-span-7; } .ids-grid-item--md-colspan-8 { @apply md:col-span-8; } .ids-grid-item--md-colspan-9 { @apply md:col-span-9; } .ids-grid-item--md-colspan-10 { @apply md:col-span-10; } .ids-grid-item--md-colspan-11 { @apply md:col-span-11; } .ids-grid-item--md-colspan-12 { @apply md:col-span-12; } /* Large colspan */ .ids-grid-item--lg-colspan-1 { @apply lg:col-span-1; } .ids-grid-item--lg-colspan-2 { @apply lg:col-span-2; } .ids-grid-item--lg-colspan-3 { @apply lg:col-span-3; } .ids-grid-item--lg-colspan-4 { @apply lg:col-span-4; } .ids-grid-item--lg-colspan-5 { @apply lg:col-span-5; } .ids-grid-item--lg-colspan-6 { @apply lg:col-span-6; } .ids-grid-item--lg-colspan-7 { @apply lg:col-span-7; } .ids-grid-item--lg-colspan-8 { @apply lg:col-span-8; } .ids-grid-item--lg-colspan-9 { @apply lg:col-span-9; } .ids-grid-item--lg-colspan-10 { @apply lg:col-span-10; } .ids-grid-item--lg-colspan-11 { @apply lg:col-span-11; } .ids-grid-item--lg-colspan-12 { @apply lg:col-span-12; } /* Extra large colspan */ .ids-grid-item--xl-colspan-1 { @apply xl:col-span-1; } .ids-grid-item--xl-colspan-2 { @apply xl:col-span-2; } .ids-grid-item--xl-colspan-3 { @apply xl:col-span-3; } .ids-grid-item--xl-colspan-4 { @apply xl:col-span-4; } .ids-grid-item--xl-colspan-5 { @apply xl:col-span-5; } .ids-grid-item--xl-colspan-6 { @apply xl:col-span-6; } .ids-grid-item--xl-colspan-7 { @apply xl:col-span-7; } .ids-grid-item--xl-colspan-8 { @apply xl:col-span-8; } .ids-grid-item--xl-colspan-9 { @apply xl:col-span-9; } .ids-grid-item--xl-colspan-10 { @apply xl:col-span-10; } .ids-grid-item--xl-colspan-11 { @apply xl:col-span-11; } .ids-grid-item--xl-colspan-12 { @apply xl:col-span-12; } /* 2 Extra Large colspan */ .ids-grid-item--xxl-colspan-1 { @apply 2xl:col-span-1; } .ids-grid-item--xxl-colspan-2 { @apply 2xl:col-span-2; } .ids-grid-item--xxl-colspan-3 { @apply 2xl:col-span-3; } .ids-grid-item--xxl-colspan-4 { @apply 2xl:col-span-4; } .ids-grid-item--xxl-colspan-5 { @apply 2xl:col-span-5; } .ids-grid-item--xxl-colspan-6 { @apply 2xl:col-span-6; } .ids-grid-item--xxl-colspan-7 { @apply 2xl:col-span-7; } .ids-grid-item--xxl-colspan-8 { @apply 2xl:col-span-8; } .ids-grid-item--xxl-colspan-9 { @apply 2xl:col-span-9; } .ids-grid-item--xxl-colspan-10 { @apply 2xl:col-span-10; } .ids-grid-item--xxl-colspan-11 { @apply 2xl:col-span-11; } .ids-grid-item--xxl-colspan-12 { @apply 2xl:col-span-12; } ================================================ FILE: projects/insper/design-system/src/lib/components/grid-item/grid-item.component.ts ================================================ import { Component, computed, input, NgModule, ViewEncapsulation, type AfterViewInit } from '@angular/core'; /** * This component should be used to create a grid item within a grid layout. * * Remember to use at least one of the `xs`, `sm`, `md`, `lg`, `xl`, or `xxl` properties to define the column span. * If none of these properties are set, an error will be thrown. */ @Component({ selector: 'ids-grid-item', standalone: true, imports: [], template: '', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()', }, }) export class GridItemComponent implements AfterViewInit { ngAfterViewInit() { const hasColspan = [ this.xs(), this.sm(), this.md(), this.lg(), this.xl(), this.xxl(), ].some(colspan => colspan && colspan > 0 && colspan <= 12); if(!hasColspan) { throw Error('At least one kind of colspan must be defined. Options: xs, sm, md, lg, xl, or xxl. Each should be a number between 1 and 12.'); } } /** * The number of columns the grid item should span in extra small screens. * This should be a number between 1 and 12. * @default undefined */ xs = input(); /** * The number of columns the grid item should span in small screens. * This should be a number between 1 and 12. * @default undefined */ sm = input(); /** * The number of columns the grid item should span in medium screens. * This should be a number between 1 and 12. * @default undefined */ md = input(); /** * The number of columns the grid item should span in large screens. * This should be a number between 1 and 12. * @default undefined */ lg = input(); /** * The number of columns the grid item should span in extra large screens. * This should be a number between 1 and 12. * @default undefined */ xl = input(); /** * The number of columns the grid item should span in 2 extra large screens. * This should be a number between 1 and 12. * @default undefined */ xxl = input(); /** * If true, the grid item will be a container for sub-items. * @default false */ container = input(false); classes = computed(() => ({ 'ids-grid-item--container': this.container(), [`ids-grid-item--xs-colspan-${this.xs()}`]: Boolean(this.xs()), [`ids-grid-item--sm-colspan-${this.sm()}`]: Boolean(this.sm()), [`ids-grid-item--md-colspan-${this.md()}`]: Boolean(this.md()), [`ids-grid-item--lg-colspan-${this.lg()}`]: Boolean(this.lg()), [`ids-grid-item--xl-colspan-${this.xl()}`]: Boolean(this.xl()), [`ids-grid-item--xxl-colspan-${this.xxl()}`]: Boolean(this.xxl()), })); } @NgModule({ imports: [GridItemComponent], exports: [GridItemComponent], }) export class GridItemModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/icon/icon.component.css ================================================ .ids-icon { @apply align-middle text-center; } .ids-icon--16 { @apply text-[16px]; } .ids-icon--20 { @apply text-[20px]; } .ids-icon--24 { @apply text-[24px]; } .ids-icon--32 { @apply text-[32px]; } ================================================ FILE: projects/insper/design-system/src/lib/components/icon/icon.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { IconComponent, type IconSize, type IconWeight } from './icon.component'; @Component({ standalone: true, selector: 'host-component', imports: [IconComponent], template: '', }) class HostComponent { size?: IconSize; weight?: IconWeight; iconName = 'home'; } describe('IconComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should render the icon with the correct class', () => { const iconElement = fixture.debugElement.query(By.css('i')).nativeElement as HTMLElement; expect(iconElement).toBeTruthy(); expect(iconElement.classList.contains('ids-icon')).toBeTrue(); expect(iconElement.classList.contains(`ids-icon--${component.size}`)).toBeTrue(); expect(iconElement.classList.contains('ph')).toBeTrue(); expect(iconElement.classList.contains(`ph-${component.iconName}`)).toBeTrue(); }); it('should allow changing size', () => { const iconElement = fixture.debugElement.query(By.css('i')).nativeElement as HTMLElement; component.size = '32'; fixture.detectChanges(); expect(iconElement).toBeTruthy(); expect(iconElement.classList.contains(`ids-icon--${component.size}`)).toBeTrue(); }); it('should allow changing weight', () => { const iconElement = fixture.debugElement.query(By.css('i')).nativeElement as HTMLElement; component.weight = 'fill'; fixture.detectChanges(); expect(iconElement).toBeTruthy(); expect(iconElement.classList.contains(`ph-${component.weight}`)).toBeTrue(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/icon/icon.component.ts ================================================ import { Component, computed, input, NgModule, ViewEncapsulation } from '@angular/core'; export type IconWeight = 'regular' | 'fill'; export type IconSize = '16' | '20' | '24' | '32'; /** * Icon component should be used with attribute selector on an `` or `` element. */ @Component({ selector: 'i[idsIcon], span[idsIcon]', standalone: true, imports: [], template: '', encapsulation: ViewEncapsulation.None, host: { '[class]': 'class()', }, }) export class IconComponent { /** * This property is the name of the icon in Phospor Icons Library * @see https://phosphoricons.com/ */ iconName = input.required(); /** * This defines the size of the icon * - `16px` * - `20px` * - `24px` * - `32px` * * @default '20' */ size = input('20'); /** * Defines the icon weight: * - `regular` * - `fill` * * @default 'regular' */ weight = input('regular'); protected class = computed(() => [ 'ids-icon', `ids-icon--${this.size()}`, `ph-${this.iconName()}`, this.weight() === 'fill' ? 'ph-fill' : 'ph', ]); } @NgModule({ imports: [IconComponent], exports: [IconComponent], }) export class IconModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/input-checkbox/input-checkbox.component.css ================================================ .ids-checkbox { @apply cursor-pointer appearance-none w-[1em] h-[1em] border-[0.1em] border-black rounded-[0.15em] bg-white grid place-content-center m-[0.25em]; } .ids-checkbox::before { @apply content-[''] w-[0.50em] h-[0.50em] scale-0 transition-transform duration-150 ease-in-out bg-dark-brand-500; } .ids-checkbox:checked::before { @apply scale-100; } .ids-checkbox:focus { @apply outline-dark-brand-700 outline-2; } .ids-checkbox:disabled { @apply cursor-not-allowed opacity-50; } ================================================ FILE: projects/insper/design-system/src/lib/components/input-checkbox/input-checkbox.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { InputCheckboxComponent } from './input-checkbox.component'; @Component({ standalone: true, imports: [InputCheckboxComponent, FormsModule], template: ` `, }) class HostComponent { value = signal(false); } describe('InputCheckboxComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should change checked state when clicked', () => { const checkboxElement = fixture.debugElement.query(By.css('input[type="checkbox"][idsInputCheckbox]')).nativeElement as HTMLInputElement; expect(checkboxElement.checked).toBeFalse(); expect(component.value()).toBeFalse(); checkboxElement.click(); fixture.detectChanges(); expect(checkboxElement.checked).toBeTrue(); expect(component.value()).toBeTrue(); checkboxElement.click(); fixture.detectChanges(); expect(checkboxElement.checked).toBeFalse(); expect(component.value()).toBeFalse(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/input-checkbox/input-checkbox.component.ts ================================================ import { Component, NgModule, ViewEncapsulation } from '@angular/core'; import { FormsModule } from '@angular/forms'; @Component({ selector: 'input[idsInputCheckbox]', standalone: true, imports: [FormsModule], template: '', host: { '[class.ids-checkbox]': 'true', '[attr.type]': '"checkbox"', }, encapsulation: ViewEncapsulation.None, }) export class InputCheckboxComponent {} @NgModule({ imports: [InputCheckboxComponent], exports: [InputCheckboxComponent], }) export class InputCheckboxModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/input-text/input-text.component.css ================================================ .ids-input-text { @apply w-full outline-none font-sans font-regular text-body-400 overflow-hidden text-ellipsis; } .ids-input-text:not([value=""]) { @apply text-gray-900 dark:text-gray-400; } .ids-input-text::placeholder { @apply text-gray-500 dark:text-gray-400; } ================================================ FILE: projects/insper/design-system/src/lib/components/input-text/input-text.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { InputTextComponent } from './input-text.component'; @Component({ selector: 'host-component', template: '', standalone: true, imports: [InputTextComponent], }) class HostComponent {} describe('InputComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should render with default props', () => { const inputEl: HTMLInputElement = fixture.debugElement.query(By.css('input[idsInputText]')).nativeElement; expect(component).toBeTruthy(); expect(inputEl).toBeTruthy(); expect(inputEl.classList).toContain('ids-input-text'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/input-text/input-text.component.ts ================================================ import { Component, NgModule, ViewEncapsulation } from '@angular/core'; export type InputTextVariant = 'brand' | 'secondary' | 'dashed'; /** * Input text should be used with attribute selector on an `` element. * * @example * ```html * * ``` */ @Component({ selector: 'input[idsInputText]', standalone: true, imports: [], template: '', encapsulation: ViewEncapsulation.None, host: { class: 'ids-input-text', }, }) export class InputTextComponent {} @NgModule({ imports: [InputTextComponent], exports: [InputTextComponent], }) export class InputTextModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/modal/modal.component.css ================================================ .ids-modal-overlay { @apply absolute w-full h-full top-0 left-0 z-50 flex items-center justify-center bg-black/50; } .ids-modal { @apply bg-white rounded-lg shadow-lg p-6 w-full max-w-lg; } .ids-modal-header { @apply flex items-center justify-between mb-4; } .ids-modal-header-title { @apply text-xl font-semibold; } .ids-modal--closing { @apply opacity-0 transition-opacity duration-300; } ================================================ FILE: projects/insper/design-system/src/lib/components/modal/modal.component.html ================================================ @if(visibility()) {

{{ title() }}

} ================================================ FILE: projects/insper/design-system/src/lib/components/modal/modal.component.spec.ts ================================================ import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ModalComponent } from './modal.component'; @Component({ selector: 'host-component', imports: [ModalComponent], template: `
Outside Div to click outside
{{content}} `, standalone: true, }) class HostComponent { content = 'This is a modal'; title = 'Modal Title'; visibility = false; closeOnClickOutside = false; } describe('ModalComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should not render content when visibility is false', () => { component.visibility = false; fixture.detectChanges(); const modalElement = fixture.debugElement.query(By.css('ids-modal')).nativeElement as HTMLElement; expect(modalElement.textContent).not.toContain(component.content); }); it('should render content when visibility is true', () => { component.visibility = true; fixture.detectChanges(); const modalElement = fixture.debugElement.query(By.css('ids-modal')).nativeElement as HTMLElement; expect(modalElement).toBeTruthy(); expect(modalElement.textContent).toContain(component.title); expect(modalElement.textContent).toContain(component.content); }); it('should close modal on click outside when closeOnClickOutside is true', fakeAsync(() => { component.visibility = true; component.closeOnClickOutside = true; fixture.detectChanges(); const outsideDiv = fixture.debugElement.query(By.css('.outside-div')).nativeElement as HTMLElement; // Ticks because the close state is set after a delay due to the animation outsideDiv.click(); tick(500); fixture.detectChanges(); const modalElement = fixture.debugElement.query(By.css('ids-modal')).nativeElement as HTMLElement; expect(modalElement.textContent).not.toContain(component.content); expect(component.visibility).toBeFalse(); })); }); ================================================ FILE: projects/insper/design-system/src/lib/components/modal/modal.component.ts ================================================ import { Component, effect, HostListener, input, model, NgModule, viewChild, ViewEncapsulation, type ElementRef } from '@angular/core'; import { IconComponent } from '../icon/icon.component'; /** * You can use this component in two different ways: * 1. Using two-way binding with `[(visibility)]` to control the modal visibility. * 2. Using one-way binding with `[visibility]` and listening to the `(visibilityChange)` event to control the modal visibility. * @example * ```html * * * * * * * * * ``` */ @Component({ selector: 'ids-modal', standalone: true, imports: [IconComponent], templateUrl: './modal.component.html', encapsulation: ViewEncapsulation.None, }) export class ModalComponent { protected modalContainerRef = viewChild>('modalContainerRef'); protected modalRef = viewChild>('modalRef'); /** The modal title */ title = input.required(); /** Allow closing the modal by clicking outside of it. */ closeOnClickOutside = input(false); /** * The modal visibility state. Should be used like a model: * @example * [(visibility)]="visibility" */ visibility = model(false); constructor() { effect(() => { if(this.visibility()) { this.modalContainerRef()?.nativeElement.classList.remove('ids-modal--closing'); } }); } protected close() { this.modalContainerRef()?.nativeElement.classList.add('ids-modal--closing'); // Timeout to allow closing animation to finish before setting visibility to false setTimeout(() => { this.visibility.set(false); }, 300); } @HostListener('document:keydown.escape', ['$event']) protected onPressEscape() { this.close(); } @HostListener('document:click', ['$event']) protected onClickOutside(event: MouseEvent) { if(!this.closeOnClickOutside()) return; if (!this.modalRef()?.nativeElement.contains(event.target as HTMLElement)) { this.close(); } } } @NgModule({ imports: [ModalComponent], exports: [ModalComponent], }) export class ModalModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/search/search.component.css ================================================ .ids-search-x { @apply cursor-pointer; } ================================================ FILE: projects/insper/design-system/src/lib/components/search/search.component.html ================================================ ================================================ FILE: projects/insper/design-system/src/lib/components/search/search.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchComponent } from './search.component'; describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SearchComponent], }) .compileComponents(); fixture = TestBed.createComponent(SearchComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/search/search.component.ts ================================================ import { Component, ElementRef, forwardRef, inject, input, model, NgModule, Renderer2, signal, viewChild, ViewEncapsulation } from '@angular/core'; import { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'; import { FormFieldComponent } from '../form-field/form-field.component'; import { IconComponent } from '../icon/icon.component'; import { InputTextComponent } from '../input-text/input-text.component'; /** * Search component that wraps an input element with leading and trailing icons. * This component implements `ControlValueAccessor` to support Angular forms. * You can use it with: * - `[(ngModel)]` -> for two-way data binding and form control * - `[(value)]` -> for two-way data binding with signals */ @Component({ selector: 'ids-search', standalone: true, imports: [FormFieldComponent, IconComponent, InputTextComponent], templateUrl: './search.component.html', encapsulation: ViewEncapsulation.None, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SearchComponent), multi: true, }, ], }) export class SearchComponent implements ControlValueAccessor { /** * The HTML `id` attribute for the input element. */ id = input(''); /** * The placeholder text for the input element. */ placeholder = input(''); /** * If true, the search input will be disabled. * * @default false */ disabled = input(false); /** * The HTML `autocomplete` attribute for the input element. */ autocomplete = input('off'); /** * If true, the search input will be required. * * @default false */ required = input(false); /** * The label for the search input, used for accessibility. */ ariaLabel = input(''); /** * The value of the search input. * * Should be used as two-way binding: * ```html * * ``` */ value = model(''); protected inputEl = viewChild>('inputElement'); private renderer = inject(Renderer2); //eslint-disable-next-line private onChange = (value: any) => {}; private onTouched = () => {}; //eslint-disable-next-line writeValue(value: any): void { if(!this.inputEl()) return; this.renderer.setProperty(this.inputEl()!.nativeElement!, 'value', value ?? ''); this.value.set(value ?? ''); } //eslint-disable-next-line registerOnChange(fn: any): void { this.onChange = fn; } //eslint-disable-next-line registerOnTouched(fn: any): void { this.onTouched = fn; } setDisabledState(disabled: boolean): void { if(!this.inputEl()) return; this.renderer.setProperty(this.inputEl()!.nativeElement!, 'disabled', disabled); } protected onInputChange(event: Event): void { const value = (event.target as HTMLInputElement).value ?? ''; this.value.set(value); this.onChange(value); } hidden = signal(true); protected onInputBlur(): void { this.onTouched(); } protected clear(): void { this.writeValue(''); this.value.set(''); this.onChange(''); if(this.inputEl()) { this.renderer.setProperty(this.inputEl()!.nativeElement, 'value', ''); } } } @NgModule({ imports: [SearchComponent], exports: [SearchComponent], }) export class SearchModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/stack/stack.component.css ================================================ .ids-stack { @apply flex; } .ids-stack--column { @apply flex-col; } .ids-stack--justify-start { @apply justify-start; } .ids-stack--justify-center { @apply justify-center; } .ids-stack--justify-end { @apply justify-end; } .ids-stack--justify-between { @apply justify-between; } .ids-stack--justify-around { @apply justify-around; } .ids-stack--align-start { @apply items-start; } .ids-stack--align-center { @apply items-center; } .ids-stack--align-end { @apply items-end; } .ids-stack--gap-1 { @apply gap-1; } .ids-stack--gap-2 { @apply gap-2; } .ids-stack--gap-3 { @apply gap-3; } .ids-stack--gap-4 { @apply gap-4; } .ids-stack--gap-5 { @apply gap-5; } .ids-stack--gap-6 { @apply gap-6; } .ids-stack--gap-7 { @apply gap-7; } .ids-stack--gap-8 { @apply gap-8; } ================================================ FILE: projects/insper/design-system/src/lib/components/stack/stack.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/stack/stack.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { StackComponent, type StackAlign, type StackDirection, type StackGap, type StackJustify } from './stack.component'; @Component({ selector: 'host-stack', standalone: true, imports: [StackComponent], template: `
Item 1
Item 2
Item 3
`, }) class HostComponent { direction: StackDirection = 'row'; gap: StackGap = 1; justifyContent: StackJustify = 'start'; alignItems: StackAlign = 'start'; } describe('StackComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should allow change direction', () => { const stackElement = fixture.debugElement.query(By.css('.ids-stack')).nativeElement as HTMLDivElement; expect(stackElement.classList).not.toContain('ids-stack--column'); component.direction = 'column'; fixture.detectChanges(); expect(stackElement.classList).toContain('ids-stack--column'); }); it('should allow change gap', () => { const stackElement = fixture.debugElement.query(By.css('.ids-stack')).nativeElement as HTMLDivElement; expect(stackElement.classList).not.toContain('ids-stack--gap-2'); component.gap = 2; fixture.detectChanges(); expect(stackElement.classList).toContain('ids-stack--gap-2'); }); it('should allow change justifyContent', () => { const stackElement = fixture.debugElement.query(By.css('.ids-stack')).nativeElement as HTMLDivElement; expect(stackElement.classList).not.toContain('ids-stack--justify-center'); component.justifyContent = 'center'; fixture.detectChanges(); expect(stackElement.classList).toContain('ids-stack--justify-center'); }); it('should allow change alignItems', () => { const stackElement = fixture.debugElement.query(By.css('.ids-stack')).nativeElement as HTMLDivElement; expect(stackElement.classList).not.toContain('ids-stack--align-center'); component.alignItems = 'center'; fixture.detectChanges(); expect(stackElement.classList).toContain('ids-stack--align-center'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/stack/stack.component.ts ================================================ import { NgClass } from '@angular/common'; import { Component, computed, input, NgModule, ViewEncapsulation } from '@angular/core'; export type StackDirection = 'row' | 'column'; export type StackGap = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; export type StackJustify = 'start' | 'center' | 'end' | 'between' | 'around'; export type StackAlign = 'start' | 'center' | 'end'; @Component({ selector: 'ids-stack', standalone: true, imports: [NgClass], templateUrl: './stack.component.html', encapsulation: ViewEncapsulation.None, }) export class StackComponent { direction = input('row'); gap = input(1); justifyContent = input('start'); alignItems = input('start'); width = input(); height = input(); class = computed(() => ({ 'ids-stack': true, 'ids-stack--column': this.direction() === 'column', [`ids-stack--justify-${this.justifyContent()}`]: true, [`ids-stack--align-${this.alignItems()}`]: true, [`ids-stack--gap-${this.gap()}`]: true, })); } @NgModule({ imports: [StackComponent], exports: [StackComponent], }) export class StackModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/tab/tab.component.css ================================================ .ids-tab-group { @apply relative flex gap-4 bg-dark-brand-400 text-white p-4; } .ids-tab { @apply block p-4; } .ids-tab--label { @apply font-bold text-white; } .ids-tab--active { @apply border-b-2 border-dark-brand-700; } .ids-tab-indicator { @apply absolute bottom-0 h-1 bg-dark-brand-700; transition: left 250ms ease, width 250ms ease; } ================================================ FILE: projects/insper/design-system/src/lib/components/tab/tab.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/tab/tab.component.spec.ts ================================================ import { TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { TabComponent, TabGroupComponent } from './tab.component'; @Component({ standalone: true, imports: [TabGroupComponent, TabComponent], template: ` @for(tab of tabs; track tab; let i = $index) { {{ tab.content }} } `, }) class ControlledTabsComponent { selectedTab = 0; tabs = [ { label: 'Tab 1', content: 'Content of Tab 1' }, { label: 'Tab 2', content: 'Content of Tab 2' }, { label: 'Tab 3', content: 'Content of Tab 3' }, ]; } @Component({ standalone: true, imports: [TabGroupComponent, TabComponent], template: ` Tab 1 Tab 2 Tab 3 `, }) class UncontrolledTabsComponent {} const setupControlledTabs = async () => { await TestBed.configureTestingModule({ imports: [ControlledTabsComponent], }).compileComponents(); const fixture = TestBed.createComponent(ControlledTabsComponent); const component = fixture.componentInstance; fixture.detectChanges(); return { fixture, component }; }; const setupUncontrolledTabs = async () => { await TestBed.configureTestingModule({ imports: [UncontrolledTabsComponent], }).compileComponents(); const fixture = TestBed.createComponent(UncontrolledTabsComponent); const component = fixture.componentInstance; fixture.detectChanges(); return { fixture, component }; }; describe('TabComponent', async () => { it('should allow change tabs when binding model', async () => { const { fixture, component } = await setupControlledTabs(); const tabElements = fixture.debugElement.queryAll(By.css('ids-tab')).map(de => de.nativeElement); expect(tabElements.length).toBe(component.tabs.length); expect(tabElements[0].hidden).toBeFalse(); expect(tabElements[1].hidden).toBeTrue(); expect(tabElements[2].hidden).toBeTrue(); component.selectedTab = 1; fixture.detectChanges(); expect(tabElements[0].hidden).toBeTrue(); expect(tabElements[1].hidden).toBeFalse(); expect(tabElements[2].hidden).toBeTrue(); component.selectedTab = 2; fixture.detectChanges(); expect(tabElements[0].hidden).toBeTrue(); expect(tabElements[1].hidden).toBeTrue(); expect(tabElements[2].hidden).toBeFalse(); }); it('should allow change tab with no state control', async () => { const {fixture} = await setupUncontrolledTabs(); const tabLabels = fixture.debugElement.queryAll(By.css('.ids-tab--label')).map(de => de.nativeElement); expect(tabLabels.length).toBe(3); const tabElements = fixture.debugElement.queryAll(By.css('ids-tab')).map(de => de.nativeElement); expect(tabElements[0].hidden).toBeFalse(); expect(tabElements[1].hidden).toBeTrue(); expect(tabElements[2].hidden).toBeTrue(); tabLabels[1].click(); fixture.detectChanges(); expect(tabElements[0].hidden).toBeTrue(); expect(tabElements[1].hidden).toBeFalse(); expect(tabElements[2].hidden).toBeTrue(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/tab/tab.component.ts ================================================ import { Component, ContentChildren, effect, ElementRef, input, model, NgModule, signal, ViewChildren, ViewEncapsulation, type AfterViewInit, type QueryList } from '@angular/core'; @Component({ selector: 'ids-tab', standalone: true, imports: [], host: { class: 'ids-tab', '[hidden]': '!selected()', '[attr.role]': '"tab"', }, template: '', encapsulation: ViewEncapsulation.None, }) export class TabComponent { selected = model(false); label = input(); } @Component({ selector: 'ids-tab-group', standalone: true, imports: [], templateUrl: './tab.component.html', encapsulation: ViewEncapsulation.None, host: { '[attr.role]': '"tablist"', }, }) export class TabGroupComponent implements AfterViewInit { @ContentChildren(TabComponent, {descendants: false}) protected tabs!: QueryList; @ViewChildren('tabLabels') protected tabLabels!: QueryList>; /** * Tab index atualmente selecionado. * @default 0 * * @note * Se quiser usar o componente de forma controlada, * é necessário usar a model para vincular o índice selecionado. * * @example * * Tab 1 * Tab 2 * Tab 3 * */ selectedTab = model(0); protected indicatorLeft = signal(0); protected indicatorWidth = signal(0); constructor() { effect(() => { this.updateTabs(); this.updateLabelIndicator(); }, { allowSignalWrites: true }); } ngAfterViewInit(): void { this.updateTabs(); this.updateLabelIndicator(); } protected onTabChange(tabIndex: number) { this.selectedTab.set(tabIndex); } private updateTabs() { this.tabs.forEach((tab, index) => { tab.selected.set(index === this.selectedTab()); }); } private updateLabelIndicator() { const idx = this.selectedTab(); const el = this.tabLabels.toArray()[idx]?.nativeElement; if (!el) return; this.indicatorLeft.set(el.offsetLeft); this.indicatorWidth.set(el.offsetWidth); } } @NgModule({ imports: [TabComponent, TabGroupComponent], exports: [TabComponent, TabGroupComponent], }) export class TabModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/table/table.component.css ================================================ .ids-table { @apply border-1 border-dark-brand-500 rounded-xs shadow-xs; } .ids-table th, td { @apply p-1; } .ids-table-row { @apply bg-dark-brand-300; &:nth-child(odd) { @apply bg-dark-brand-100; } } .ids-table-header { @apply bg-dark-brand-600 text-white; } .ids-table-checkbox { @apply appearance-none w-[1em] h-[1em] border-[0.1em] border-black rounded-[0.15em] bg-white grid place-content-center m-[0.25em]; } .ids-table-checkbox::before { @apply content-[''] w-[0.50em] h-[0.50em] scale-0 transition-transform duration-150 ease-in-out bg-dark-brand-500; } .ids-table-checkbox:checked::before { @apply scale-100; } .ids-table-icons { @apply fill-white; } ================================================ FILE: projects/insper/design-system/src/lib/components/table/table.component.html ================================================ @for(headerGroup of table.getHeaderGroups(); track headerGroup.id) { @for(header of headerGroup.headers; track header.id) { @if(!header.isPlaceholder) { } } } @for(row of table.getRowModel().rows; track row.id) { @for(cell of row.getVisibleCells(); track cell.id) { } } @if(hasFooter()) { @for(footerGroup of table.getFooterGroups(); track footerGroup.id) { @for(footer of footerGroup.headers; track footer.id) { } } }
{{ header }} @if(header.column.getCanSort()) { @switch(header.column.getIsSorted()) { @case('asc') { } @case('desc') { } @default { } } }
{{ cell }}
{{ footer }}
================================================ FILE: projects/insper/design-system/src/lib/components/table/table.component.spec.ts ================================================ import { Component, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TableComponent } from './table.component'; @Component({ standalone: true, imports: [TableComponent], template: ` `, }) class HostComponent { data = signal<{name: string, age: number, id: number}[]>([ {name: 'John Doe', age: 30, id: 1}, {name: 'Jane Smith', age: 25, id: 2}, {name: 'Mike Johnson', age: 40, id: 3}, ]); columns = [ { header: 'ID', accessorKey: 'id', enableSorting: true, }, { header: 'Name', accessorKey: 'name', }, { header: 'Age', accessorKey: 'age', }, ]; enableSelection = false; enableSorting = false; } describe('TableTesteComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TableComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should render with proper bindings', () => { const tableElement = fixture.debugElement.query(By.css('ids-table')).nativeElement as HTMLElement; expect(tableElement.textContent).toContain('John Doe'); component.data.update(prev => { prev[0].name = 'Updated Name'; return [...prev]; }); fixture.detectChanges(); expect(tableElement.textContent).toContain('Updated Name'); }); it('should allow to selection all rows', () => { component.enableSelection = true; fixture.detectChanges(); const tableElement = fixture.debugElement.query(By.css('ids-table')).nativeElement as HTMLElement; const toggleAll = tableElement.querySelector('.ids-table-checkbox') as HTMLInputElement; expect(toggleAll).toBeTruthy(); expect(toggleAll.checked).toBeFalse(); toggleAll.click(); fixture.detectChanges(); expect(toggleAll.checked).toBeTrue(); toggleAll.click(); fixture.detectChanges(); expect(toggleAll.checked).toBeFalse(); }); it('should allow to select specif row', () => { component.enableSelection = true; fixture.detectChanges(); const tableElement = fixture.debugElement.query(By.css('ids-table')).nativeElement as HTMLTableElement; const rowsCheckboxes = Array.from(tableElement.querySelectorAll('input[type="checkbox"]')) as HTMLInputElement[]; expect(rowsCheckboxes.length).toBeGreaterThan(0); // Skip the first checkbox which is for selecting all rows rowsCheckboxes[1].click(); fixture.detectChanges(); expect(rowsCheckboxes[1].checked).toBeTrue(); rowsCheckboxes[2].click(); fixture.detectChanges(); expect(rowsCheckboxes[2].checked).toBeTrue(); }); it('should allow sorting', () => { component.enableSorting = true; fixture.detectChanges(); const tableElement = fixture.debugElement.query(By.css('ids-table')).nativeElement as HTMLElement; const headers = Array.from(tableElement.querySelectorAll('.ids-table-header')) as HTMLElement[]; expect(headers.length).toBeGreaterThan(0); // Click on the first header to sort by ID headers[0].click(); fixture.detectChanges(); const sortedRows = Array.from(tableElement.querySelectorAll('.ids-table-row')) as HTMLElement[]; expect(sortedRows[0].textContent).toContain('3'); expect(sortedRows[1].textContent).toContain('2'); expect(sortedRows[2].textContent).toContain('1'); // Click again to reverse the order headers[0].click(); fixture.detectChanges(); const reversedRows = Array.from(tableElement.querySelectorAll('.ids-table-row')) as HTMLElement[]; expect(reversedRows[0].textContent).toContain('1'); expect(reversedRows[1].textContent).toContain('2'); expect(reversedRows[2].textContent).toContain('3'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/table/table.component.ts ================================================ import { NgTemplateOutlet } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input, NgModule, signal, ViewEncapsulation, type Signal } from '@angular/core'; import { ColumnDef, createAngularTable, flexRenderComponent, FlexRenderDirective, getCoreRowModel, getSortedRowModel, injectFlexRenderContext, type CellContext, type HeaderContext, type RowSelectionState, type SortingState, type Table } from '@tanstack/angular-table'; @Component({ template: ` `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) class TableHeadSelectionComponent { context = injectFlexRenderContext>(); } @Component({ template: ` `, standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, }) class TableRowSelectionComponent { context = injectFlexRenderContext>(); } export type IDSTableColumn = ColumnDef type TableState = Table & Signal> @Component({ selector: 'ids-table', standalone: true, imports: [FlexRenderDirective, NgTemplateOutlet], templateUrl: './table.component.html', encapsulation: ViewEncapsulation.None, }) export class TableComponent { data = input.required(); columns = input.required[]>(); enableSelection = input(false); enableSorting = input(false); rowSelection = signal({}); sorting = signal([]); hasFooter = computed(() => this.table .getFooterGroups() .flatMap((group) => group.headers.map((header) => header.column.columnDef.footer)) .filter(Boolean) .length > 0 ); baseColumns = computed(() => [ ...( this.enableSelection() ? [ { id: 'select', header: () => flexRenderComponent(TableHeadSelectionComponent), cell: () => flexRenderComponent(TableRowSelectionComponent), }, ] : [] ), ...this.columns(), ]); protected table: TableState = createAngularTable(() => ({ data: this.data(), columns: this.baseColumns(), getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), state: { rowSelection: this.rowSelection(), sorting: this.sorting(), }, enableRowSelection: this.enableSelection(), enableSorting: this.enableSorting(), onSortingChange: (updatedOrValue) => { this.sorting.set( typeof updatedOrValue === 'function' ? updatedOrValue(this.sorting()) : updatedOrValue ); }, onRowSelectionChange: (updaterOrValue) => { this.rowSelection.set( typeof updaterOrValue === 'function' ? updaterOrValue(this.rowSelection()) : updaterOrValue ); }, })); } @NgModule({ imports: [TableComponent], exports: [TableComponent], }) export class TableModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/toast/toast.component.css ================================================ .ids-toast-container { @apply fixed flex flex-col gap-2 top-1 right-1 z-10 max-h-2/3 max-w-[400px] font-serif; } .ids-toast { @apply flex flex-col h-[4em] p-2 rounded transition-all duration-300 ease-in-out translate-x-[calc(100%+30px)] translate-y-[calc(100%+30px)]; } .ids-toast--visible { @apply translate-x-0 translate-y-0; } .ids-toast-header { @apply w-full inline-flex gap-2 items-center justify-between font-bold; } .ids-toast-status--success { @apply bg-green-400 text-black; } .ids-toast-status--error { @apply bg-red-400 text-black; } .ids-toast-status--info { @apply bg-blue-400 text-black; } .ids-toast-status--warning { @apply bg-yellow-400 text-black; } .ids-toast-content { @apply text-lg text-wrap; } .ids-toast-close { @apply cursor-pointer self-start; } ================================================ FILE: projects/insper/design-system/src/lib/components/toast/toast.component.html ================================================
@for(toast of toasts(); track toast.id) {
{{toast.title}}
{{toast.message}}
}
================================================ FILE: projects/insper/design-system/src/lib/components/toast/toast.component.spec.ts ================================================ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ToastOptions, ToastService } from '../../../public-api'; import { ToastComponent } from './toast.component'; type ToastShowOptions = Omit; const toasts: Record = { success: { title: 'Success Toast', message: 'This is a test toast message.', variant: 'success', duration: 2000, }, error: { title: 'Error Toast', message: 'This is a test toast message.', variant: 'error', duration: 2000, }, info: { title: 'Info Toast', message: 'This is a test toast message.', variant: 'info', duration: 2000, }, warning: { title: 'Warning Toast', message: 'This is a test toast message.', variant: 'warning', duration: 2000, }, }; describe('ToastComponent', () => { let component: ToastComponent; let fixture: ComponentFixture; let service: ToastService; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ToastComponent], }) .compileComponents(); fixture = TestBed.createComponent(ToastComponent); component = fixture.componentInstance; service = TestBed.inject(ToastService); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should show toasts from the service', () => { service.showToast(toasts.success); service.showToast(toasts.error); fixture.detectChanges(); const toastElements = Array.from(fixture.debugElement.queryAll(By.css('.ids-toast'))); expect(toastElements.length).toBe(2); // Index 1 because the last toast is always the one being added expect(toastElements[1].nativeElement.textContent).toContain(toasts.success.title); expect(toastElements[1].nativeElement.classList).toContain('ids-toast-status--success'); expect(toastElements[0].nativeElement.textContent).toContain(toasts.error.title); expect(toastElements[0].nativeElement.classList).toContain('ids-toast-status--error'); }); it('should close a toast when close button is clicked', fakeAsync(() => { service.showToast(toasts.info); fixture.detectChanges(); const toastElement = fixture.debugElement.query(By.css('.ids-toast')); const closeButton = toastElement.query(By.css('.ids-toast-close')); expect(toastElement).toBeTruthy(); expect(closeButton).toBeTruthy(); closeButton.nativeElement.click(); // Toast service emulates a delay for the fade-out animation, so we need to flush macrotasks flush(); fixture.detectChanges(); const updatedToastElements = fixture.debugElement.queryAll(By.css('.ids-toast')); expect(updatedToastElements.length).toBe(0); })); it('should close when duration expires', fakeAsync(() => { service.showToast(toasts.warning); fixture.detectChanges(); const toastElement = fixture.debugElement.query(By.css('.ids-toast')); expect(toastElement).toBeTruthy(); // Simulate the passage of time for the toast duration tick(toasts.warning.duration + 100); fixture.detectChanges(); const updatedToastElements = fixture.debugElement.queryAll(By.css('.ids-toast')); expect(updatedToastElements.length).toBe(0); })); }); ================================================ FILE: projects/insper/design-system/src/lib/components/toast/toast.component.ts ================================================ import { Component, inject, NgModule, signal, ViewEncapsulation, type OnDestroy, type OnInit } from '@angular/core'; import type { Subscription } from 'rxjs'; import { ToastService, type ToastOptions } from '../../services/toast/toast.service'; @Component({ selector: 'ids-toast', standalone: true, imports: [], templateUrl: './toast.component.html', encapsulation: ViewEncapsulation.None, }) export class ToastComponent implements OnInit, OnDestroy { protected toasts = signal([]); private toastService = inject(ToastService); private subscription: Subscription | undefined; ngOnInit() { this.subscription = this.toastService.toast$.subscribe((toasts) => { this.toasts.set(toasts); }); } ngOnDestroy() { this.subscription?.unsubscribe(); } protected closeToast(id: string) { this.toastService.hideToast(id); } protected getVariantClass(variant: ToastOptions['variant']) { return `ids-toast ids-toast-status--${variant}`; } } @NgModule({ imports: [ToastComponent], exports: [ToastComponent], }) export class ToastModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/toggle/toggle.component.css ================================================ /* Container */ .ids-toggle-container { @apply inline-flex items-center p-1 bg-gray-400 rounded-full cursor-pointer transition-all duration-300 ease-out; } .ids-toggle-container--active { @apply bg-dark-brand-200; } .ids-toggle-container--sm { @apply w-[32px] h-[18px]; } .ids-toggle-container--md { @apply w-[40px] h-[22px]; } .ids-toggle-container--lg { @apply w-[48px] h-[26px]; } /* Toggle switcher */ .ids-toggle-switcher { @apply bg-gray-700 rounded-full transition-all duration-300 ease-out; } .ids-toggle-switcher--sm { @apply w-[12px] h-[12px]; } .ids-toggle-switcher--md { @apply w-[16px] h-[16px]; } .ids-toggle-switcher--lg { @apply w-[20px] h-[20px]; } .ids-toggle-switcher--active { @apply translate-x-[100%] bg-dark-brand-700; } ================================================ FILE: projects/insper/design-system/src/lib/components/toggle/toggle.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/toggle/toggle.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, signal } from '@angular/core'; import { By } from '@angular/platform-browser'; import { ToggleComponent, type ToggleSize } from './toggle.component'; @Component({ standalone: true, imports: [ToggleComponent], template: ` `, }) class HostComponent { size: ToggleSize = 'md'; toggled = signal(false); name = 'toggle-example'; } describe('ToggleComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should toggle state when clicked', () => { const toggleElement = fixture.debugElement.query(By.css('.ids-toggle-container')); toggleElement.triggerEventHandler('click'); fixture.detectChanges(); expect(component.toggled()).toBeTrue(); }); it('should toggle state when space key is pressed', () => { const toggleElement = fixture.debugElement.query(By.css('.ids-toggle-container')); toggleElement.triggerEventHandler('keydown.enter'); fixture.detectChanges(); expect(component.toggled()).toBeTrue(); }); it('should use name attribute when provided', () => { const toggleElement = fixture.debugElement.query(By.css('.ids-toggle-container')); const inputElement = toggleElement.query(By.css('input')).nativeElement as HTMLInputElement; expect(inputElement.name).toBe(component.name); }); it('should apply correct classes based on size', () => { const toggleElement = fixture.debugElement.query(By.css('.ids-toggle-container')).nativeElement as HTMLElement; expect(toggleElement.classList).toContain('ids-toggle-container--md'); component.size = 'lg'; fixture.detectChanges(); expect(toggleElement.classList).toContain('ids-toggle-container--lg'); expect(toggleElement.classList).not.toContain('ids-toggle-container--md'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/toggle/toggle.component.ts ================================================ import { Component, computed, input, model, NgModule, ViewChild, ViewEncapsulation, type ElementRef } from '@angular/core'; import { FormsModule } from '@angular/forms'; export type ToggleSize = 'sm' | 'md' | 'lg'; @Component({ selector: 'ids-toggle', standalone: true, imports: [FormsModule], templateUrl: './toggle.component.html', encapsulation: ViewEncapsulation.None, }) export class ToggleComponent { @ViewChild('toggleContainer') toggleContainer!: ElementRef; @ViewChild('toggleCircle') toggleCircle!: ElementRef; /** The name of the toggle, used for form submission */ name = input(); /** The size of the toggle, can be * - `sm` (small) * - `md` (medium) * - `lg` (large) */ size = input('md'); /** The value of the toggle, should be used with two-way binding */ toggled = model(false); protected toggle() { this.toggled.update((toggled) => { this.toggleContainer.nativeElement.classList.toggle('ids-toggle-container--active', !toggled); this.toggleCircle.nativeElement.classList.toggle('ids-toggle-switcher--active', !toggled); return !toggled; }); } protected containerClass = computed(() => ({ 'ids-toggle-container': true, 'ids-toggle-container--active': this.toggled(), [`ids-toggle-container--${this.size()}`]: true, })); protected switcherClass = computed(() => ({ 'ids-toggle-switcher': true, 'ids-toggle-switcher--active': this.toggled(), [`ids-toggle-switcher--${this.size()}`]: true, })); } @NgModule({ imports: [ToggleComponent], exports: [ToggleComponent], }) export class ToggleModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/tooltip/tooltip.component.css ================================================ .ids-tooltip { @apply relative inline-block; } /* Show tooltip on hover */ .ids-tooltip:hover::before, .ids-tooltip:hover::after { @apply visible; } /* Tooltip arrow */ .ids-tooltip::before { @apply absolute invisible content-[''] top-[-6px] left-1/2 translate-x-[-50%] z-10 border-8 border-solid border-l-[rgba(0,0,0,0.7)] border-t-transparent border-b-transparent border-r-transparent; } /* Tooltip content */ .ids-tooltip::after { @apply absolute invisible content-[attr(tooltipText)] p-2 top-[-6px] left-1/2 translate-x-[-50%] translate-y-[-100%] bg-[rgba(0,0,0,0.7)] text-center text-nowrap z-10 rounded-sm text-white; } /* Tooltip positions */ [tooltipPosition="left"]::before { @apply left-0 top-1/2 translate-y-[-50%] } [tooltipPosition="left"]::after { @apply left-0 top-1/2 ml-[-8px] translate-x-[-100%] translate-y-[-50%]; } [tooltipPosition="right"]::before { @apply left-full top-1/2 translate-y-[-50%] rotate-180; } [tooltipPosition="right"]::after { @apply left-full top-1/2 ml-[8px] translate-x-0 translate-y-[-50%]; } [tooltipPosition="top"]::before { @apply left-1/2 rotate-90; } [tooltipPosition="top"]::after { @apply left-1/2; } [tooltipPosition="bottom"]::before { @apply top-full mt-[6px] translate-x-[-50%] translate-y-[-100%] rotate-270; } [tooltipPosition="bottom"]::after { @apply top-full mt-[6px] translate-x-[-50%] translate-y-0; } ================================================ FILE: projects/insper/design-system/src/lib/components/tooltip/tooltip.component.html ================================================
================================================ FILE: projects/insper/design-system/src/lib/components/tooltip/tooltip.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { TooltipComponent, type TooltipPosition } from './tooltip.component'; @Component({ standalone: true, imports: [TooltipComponent], template: ` Hover over me `, }) class HostComponent { tooltipText = 'My tooltip text'; tooltipPosition: TooltipPosition = 'top'; } describe('TooltipComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/tooltip/tooltip.component.ts ================================================ import { Component, input, NgModule, ViewEncapsulation } from '@angular/core'; export type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'; /** * Tooltip component that displays additional information when hovering over his children. * * @example * ```html * * Hover over me * * * ``` */ @Component({ selector: 'ids-tooltip', standalone: true, imports: [], templateUrl: './tooltip.component.html', encapsulation: ViewEncapsulation.None, }) export class TooltipComponent { /** * Text to be displayed in the tooltip. */ tooltipText = input.required(); /** * Position of the tooltip relative to the host element. * Possible values: * - `top` * - `bottom` * - `left` * - `right` */ tooltipPosition = input('top'); } @NgModule({ imports: [TooltipComponent], exports: [TooltipComponent], }) export class TooltipModule {} ================================================ FILE: projects/insper/design-system/src/lib/components/typography/typography.component.css ================================================ /* Ultra */ .ids-typography-ultra-2400 { @apply font-sans text-ultra-2400 font-medium text-ellipsis; } .ids-typography-ultra-2400--str { @apply font-sans text-ultra-2400 font-bold text-ellipsis; } .ids-typography-ultra-2400--exp { @apply font-serif text-ultra-2400 text-ellipsis; } .ids-typography-ultra-2000 { @apply font-sans text-ultra-2000 font-medium text-ellipsis; } .ids-typography-ultra-2000--str { @apply font-sans text-ultra-2000 font-bold text-ellipsis; } .ids-typography-ultra-2000--exp { @apply font-serif text-ultra-2000 text-ellipsis; } .ids-typography-ultra-1800 { @apply font-sans text-ultra-1800 font-medium text-ellipsis; } .ids-typography-ultra-1800--str { @apply font-sans text-ultra-1800 font-bold text-ellipsis; } .ids-typography-ultra-1800--exp { @apply font-serif text-ultra-1800 text-ellipsis; } /* Display */ .ids-typography-display-1600 { @apply font-sans text-display-1600 font-medium text-ellipsis; } .ids-typography-display-1600--str { @apply font-sans text-display-1600 font-bold text-ellipsis; } .ids-typography-display-1600--exp { @apply font-serif text-display-1600 text-ellipsis; } .ids-typography-display-1400 { @apply font-sans text-display-1400 font-medium text-ellipsis; } .ids-typography-display-1400--str { @apply font-sans text-display-1400 font-bold text-ellipsis; } .ids-typography-display-1400--exp { @apply font-serif text-display-1400 text-ellipsis; } .ids-typography-display-1200 { @apply font-sans text-display-1200 font-medium text-ellipsis; } .ids-typography-display-1200--str { @apply font-sans text-display-1200 font-bold text-ellipsis; } .ids-typography-display-1200--exp { @apply font-serif text-display-1200 text-ellipsis; } /* Headline */ .ids-typography-headline-1000 { @apply font-sans text-headline-1000 font-medium text-ellipsis; } .ids-typography-headline-1000--str { @apply font-sans text-headline-1000 font-bold text-ellipsis; } .ids-typography-headline-900 { @apply font-sans text-headline-900 font-medium text-ellipsis; } .ids-typography-headline-900--str { @apply font-sans text-headline-900 font-bold text-ellipsis; } .ids-typography-headline-800 { @apply font-sans text-headline-800 font-medium text-ellipsis; } .ids-typography-headline-800--str { @apply font-sans text-headline-800 font-bold text-ellipsis; } /* Title */ .ids-typography-title-700 { @apply font-sans text-title-700 font-medium text-ellipsis; } .ids-typography-title-700--str { @apply font-sans text-title-700 font-bold text-ellipsis; } .ids-typography-title-600 { @apply font-sans text-title-600 font-medium text-ellipsis; } .ids-typography-title-600--str { @apply font-sans text-title-600 font-bold text-ellipsis; } .ids-typography-title-500 { @apply font-sans text-title-500 font-medium text-ellipsis; } .ids-typography-title-500--str { @apply font-sans text-title-500 font-bold text-ellipsis; } /* Lead */ .ids-typography-lead-700 { @apply font-sans text-lead-700 font-regular text-ellipsis; } .ids-typography-lead-700--str { @apply font-sans text-lead-700 font-bold text-ellipsis; } .ids-typography-lead-600 { @apply font-sans text-lead-600 font-regular; } .ids-typography-lead-600--str { @apply font-sans text-lead-600 font-bold; } .ids-typography-lead-500 { @apply font-sans text-lead-500 font-regular; } .ids-typography-lead-500--str { @apply font-sans text-lead-500 font-bold; } /* Body */ .ids-typography-body-450 { @apply font-sans text-body-450 font-regular; } .ids-typography-body-450--str { @apply font-sans text-body-450 font-semibold; } .ids-typography-body-400 { @apply font-sans text-body-400 font-regular; } .ids-typography-body-400--str { @apply font-sans text-body-400 font-semibold; } .ids-typography-body-350 { @apply font-sans text-body-350 font-regular; } .ids-typography-body-350--str { @apply font-sans text-body-350 font-semibold; } /* Caption */ .ids-typography-caption-300 { @apply font-sans text-caption-300 font-regular; } .ids-typography-caption-300--str { @apply font-sans text-caption-300 font-semibold; } /* Support */ .ids-typography-support-1000 { @apply font-sans-condensed text-support-1000 text-ellipsis; } .ids-typography-support-1000--upper { @apply font-sans-condensed text-support-1000 uppercase text-ellipsis; } .ids-typography-support-800 { @apply font-sans-condensed text-support-800 text-ellipsis; } .ids-typography-support-800--upper { @apply font-sans-condensed text-support-800 uppercase text-ellipsis; } .ids-typography-support-600 { @apply font-sans-condensed text-support-600 text-ellipsis; } .ids-typography-support-600--upper { @apply font-sans-condensed text-support-600 uppercase text-ellipsis; } .ids-typography--underline { @apply underline decoration-auto underline-offset-3; } ================================================ FILE: projects/insper/design-system/src/lib/components/typography/typography.component.html ================================================ @switch (this.element()) { @case ('h1') {

} @case ('h2') {

} @case ('h3') {

} @case ('h4') {

} @case ('h5') {
} @case ('h6') {
} @case ('p') {

} @case ('span') { } }
================================================ FILE: projects/insper/design-system/src/lib/components/typography/typography.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component } from '@angular/core'; import { By } from '@angular/platform-browser'; import { TypographyComponent, type TypographyVariant } from './typography.component'; @Component({ imports: [TypographyComponent], template: `

Text

`, standalone: true, }) class HostComponent { variant: TypographyVariant = 'body-400'; underline: boolean = false; } describe('TypographyComponent', () => { let component: HostComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HostComponent], }) .compileComponents(); fixture = TestBed.createComponent(HostComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should apply the correct variant class', () => { component.variant = 'display-1400'; fixture.detectChanges(); const element = fixture.debugElement.query(By.css('p')).nativeElement; expect(element.classList).toContain('ids-typography-display-1400'); component.variant = 'lead-500'; fixture.detectChanges(); expect(element.classList).toContain('ids-typography-lead-500'); expect(element.classList).not.toContain('ids-typography-display-1400'); component.variant = 'caption-300'; fixture.detectChanges(); expect(element.classList).toContain('ids-typography-caption-300'); expect(element.classList).not.toContain('ids-typography-lead-500'); }); it('should apply underline transformation when underline is true', () => { component.underline = true; fixture.detectChanges(); const element = fixture.nativeElement.querySelector('p') as HTMLParagraphElement; expect(element.classList).toContain('ids-typography--underline'); }); }); ================================================ FILE: projects/insper/design-system/src/lib/components/typography/typography.component.ts ================================================ import { Component, computed, input, NgModule, ViewEncapsulation } from '@angular/core'; export const IDS_TYPOGRAPHY_VARIANTS = [ 'ultra-2400', 'ultra-2400--str', 'ultra-2400--exp', 'ultra-2000', 'ultra-2000--str', 'ultra-2000--exp', 'ultra-1800', 'ultra-1800--str', 'ultra-1800--exp', 'display-1600', 'display-1600--str', 'display-1600--exp', 'display-1400', 'display-1400--str', 'display-1400--exp', 'display-1200', 'display-1200--str', 'display-1200--exp', 'headline-1000', 'headline-1000--str', 'headline-900', 'headline-900--str', 'headline-800', 'headline-800--str', 'title-700', 'title-700--str', 'title-600', 'title-600--str', 'title-500', 'title-500--str', 'lead-700', 'lead-700--str', 'lead-600', 'lead-600--str', 'lead-500', 'lead-500--str', 'body-450', 'body-450--str', 'body-400', 'body-400--str', 'body-350', 'body-350--str', 'caption-300', 'caption-300--str', 'support-1000', 'support-1000--upper', 'support-800', 'support-800--upper', 'support-600', 'support-600--upper', ] as const; export type TypographyVariant = typeof IDS_TYPOGRAPHY_VARIANTS[number]; @Component({ selector: '[idsTypography]', standalone: true, imports: [], template: '', encapsulation: ViewEncapsulation.None, host: { '[class]': 'classes()', }, }) export class TypographyComponent { /** * The typography style variant to apply. * @default 'body-400' */ idsTypography = input('body-400'); /** * Apply underline transformation to the text. * If true, text will be underlined using CSS. * @note This does not change the actual text content, only its visual representation. * @default false */ underline = input(false); protected classes = computed(() => ({ [`ids-typography-${this.idsTypography()}`]: true, 'ids-typography--underline': this.underline(), })); } @NgModule({ imports: [TypographyComponent], exports: [TypographyComponent], }) export class TypographyModule {} ================================================ FILE: projects/insper/design-system/src/lib/services/toast/toast.service.spec.ts ================================================ import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { ToastService, type ToastOptions } from './toast.service'; describe('ToastService', () => { let service: ToastService; const toast: Omit = { title: 'Test Toast', message: 'This is a test toast message.', variant: 'info', duration: 1000, }; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(ToastService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should show a toast', () => { let toasts: ToastOptions[] = []; service.toast$.subscribe(t => {toasts = t;}); service.showToast(toast); expect(toasts.length).toBeGreaterThan(0); expect(toasts[0].title).toBe(toast.title); }); it('should hide toast after duration', fakeAsync(() => { let toasts: ToastOptions[] = []; service.toast$.subscribe(t => {toasts = t;}); service.showToast(toast); tick(toast.duration + 100); expect(toasts.length).toBe(0); })); }); ================================================ FILE: projects/insper/design-system/src/lib/services/toast/toast.service.ts ================================================ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; export type ToastOptions = { id: string; createdAt: number; title: string; message: string; variant: 'info' | 'success' | 'warning' | 'error'; duration: number; visible: boolean; } @Injectable({ providedIn: 'root', }) export class ToastService { private toastsSubject = new BehaviorSubject([]); toast$ = this.toastsSubject.asObservable(); showToast(toast: Omit) { const newToast: ToastOptions = { ...toast, id: this.generateToastId(), createdAt: Date.now(), visible: false, }; this.toastsSubject.next([ newToast, ...this.toastsSubject.value, ]); // Delay to allow the fade-in animation to complete setTimeout(() => { this.toastsSubject.next(this.toastsSubject.value.map(t => t.id === newToast.id ? { ...t, visible: true } : t )); }, 100); setTimeout(() => { this.hideToast(newToast.id); }, toast.duration ?? 2000); } hideToast(id: string) { this.toastsSubject.next(this.toastsSubject.value.map(toast => { if (toast.id === id) { return { ...toast, visible: false }; } return toast; })); // Delay to allow the fade-out animation to complete setTimeout(() => { this.toastsSubject.next(this.toastsSubject.value.filter(toast => toast.id !== id)); }, 100); } hideAll() { this.toastsSubject.next([]); } private generateToastId(): string { return `toast-${Date.now()}-${Math.floor(Math.random() * 1000)}`; } } ================================================ FILE: projects/insper/design-system/src/lib/styles/styles.css ================================================ /* Base imports */ @import 'tailwindcss'; @import '../styles/theme/index.css'; @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); /* Components' styles */ @import '../components/button/button.component.css'; @import '../components/card/card.component.css'; @import '../components/input-text/input-text.component.css'; @import '../components/stack/stack.component.css'; @import '../components/tab/tab.component.css'; @import '../components/table/table.component.css'; @import '../components/toast/toast.component.css'; @import '../components/icon/icon.component.css'; @import '../components/chip/chip.component.css'; @import '../components/modal/modal.component.css'; @import '../components/tooltip/tooltip.component.css'; @import '../components/typography/typography.component.css'; @import '../components/accordion/accordion.component.css'; @import '../components/toggle/toggle.component.css'; @import '../components/input-checkbox/input-checkbox.component.css'; @import '../components/form-field/form-field.component.css'; @import '../components/container/container.component.css'; @import '../components/grid/grid.component.css'; @import '../components/grid-item/grid-item.component.css'; @import '../components/breadcrumb/breadcrumb.component.css'; @import '../components/avatar/avatar.component.css'; @import '../components/divider/divider.component.css'; @import '../components/search/search.component.css'; @theme { /* Dark Colors */ --color-dark-brand-100: var(--dark-color-brand-100); --color-dark-brand-200: var(--dark-color-brand-200); --color-dark-brand-300: var(--dark-color-brand-300); --color-dark-brand-400: var(--dark-color-brand-400); --color-dark-brand-500: var(--dark-color-brand-500); --color-dark-brand-600: var(--dark-color-brand-600); --color-dark-brand-700: var(--dark-color-brand-700); --color-dark-primary-100: var(--dark-color-primary-100); --color-dark-primary-200: var(--dark-color-primary-200); --color-dark-primary-300: var(--dark-color-primary-300); --color-dark-primary-400: var(--dark-color-primary-400); --color-dark-primary-500: var(--dark-color-primary-500); --color-dark-primary-600: var(--dark-color-primary-600); --color-dark-primary-700: var(--dark-color-primary-700); /* Light Colors */ --color-light-brand-100: var(--light-color-brand-100); --color-light-brand-200: var(--light-color-brand-200); --color-light-brand-300: var(--light-color-brand-300); --color-light-brand-400: var(--light-color-brand-400); --color-light-brand-500: var(--light-color-brand-500); --color-light-brand-600: var(--light-color-brand-600); --color-light-brand-700: var(--light-color-brand-700); --color-light-primary-100: var(--light-color-primary-100); --color-light-primary-200: var(--light-color-primary-200); --color-light-primary-300: var(--light-color-primary-300); --color-light-primary-400: var(--light-color-primary-400); --color-light-primary-500: var(--light-color-primary-500); --color-light-primary-600: var(--light-color-primary-600); --color-light-primary-700: var(--light-color-primary-700); --spacing-1: var(--spacing-1); --spacing-2: var(--spacing-2); --spacing-3: var(--spacing-3); --spacing-4: var(--spacing-4); --spacing-5: var(--spacing-5); --spacing-6: var(--spacing-6); --spacing-7: var(--spacing-7); --spacing-8: var(--spacing-8); } /* The default border color has changed to `currentcolor` in Tailwind CSS v4, so we've added these compatibility styles to make sure everything still looks the same as it did with Tailwind CSS v3. If we ever want to remove these styles, we need to add an explicit border color utility to any element that depends on these defaults. */ @layer base { *, ::after, ::before, ::backdrop, ::file-selector-button { border-color: var(--color-gray-200, currentcolor); } } @font-face { font-family: 'GTUltra'; font-style: normal; font-weight: 700; font-display: swap; src: url(../../assets/GTUltra/GTUltra-Fine-Bold.woff) format("woff"); } @font-face { font-family: 'Inter'; font-style: normal; font-weight: 100 900; font-display: swap; src: url(../../assets/Inter/InterVariable.woff2) format("woff2"); } @font-face { font-family: 'Inter'; font-style: italic; font-weight: 100 900; font-display: swap; src: url(../../assets/Inter/InterVariable-Italic.woff2) format("woff2"); } @font-face { font-family: 'Roboto'; font-style: normal; font-weight: 100 700; font-display: swap; src: url(../../assets/Roboto/RobotoMonoVariable.ttf) format("truetype"); } @font-face { font-family: 'Roboto'; font-style: italic; font-weight: 100 700; font-display: swap; src: url(../../assets/Roboto/RobotoMonoVariable-Italic.ttf) format("truetype"); } @font-face { font-family: 'Acumin-Pro-Extra-Condensed'; font-style: normal; font-weight: 600; font-display: auto; font-stretch: normal; src: url(../../assets/Acumin/Acumin-Pro-Extra-Condensed.woff2) format("woff2"), url(../../assets/Acumin/Acumin-Pro-Extra-Condensed.woff) format("woff"), url(../../assets/Acumin/Acumin-Pro-Extra-Condensed.otf) format("opentype"); } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/index.css ================================================ @import './tokens/colors.css'; @import './tokens/typography.css'; @import './tokens/spacing.css'; @import './tokens/size.css'; @import './tokens/breakpoints.css'; @import './tokens/container.css'; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/index.ts ================================================ import type { Breakpoints } from './tokens/breakpoints'; import breakpoints from './tokens/breakpoints'; import colors, { type Color } from './tokens/colors'; import type { Container } from './tokens/container'; import container from './tokens/container'; import size, { type Size } from './tokens/size'; import spacing, { type Spacing } from './tokens/spacing'; import typography, { type Typography } from './tokens/typography'; export type Theme = { colors: Color; typography: Typography; spacing: Spacing; size: Size; breakpoints: Breakpoints; container: Container; } export const theme: Theme = { colors, spacing, typography, size, breakpoints, container, }; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/breakpoints.css ================================================ :root { --ids-breakpoint-xs: 0rem; --ids-breakpoint-sm: 40rem; --ids-breakpoint-md: 48rem; --ids-breakpoint-lg: 64rem; --ids-breakpoint-xl: 80rem; --ids-breakpoint-2xl: 90rem; } @theme { --breakpoint-xs: 0rem; --breakpoint-sm: 40rem; --breakpoint-md: 48rem; --breakpoint-lg: 64rem; --breakpoint-xl: 80rem; --breakpoint-2xl: 90rem; } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/breakpoints.ts ================================================ export type Breakpoints = { 'xs': string; 'sm': string; 'md': string; 'lg': string; 'xl': string; '2xl': string; }; const breakpoints: Breakpoints = { xs: '0rem', sm: '40rem', md: '48rem', lg: '64rem', xl: '80rem', '2xl': '90rem', }; export default breakpoints; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/colors.css ================================================ :root { --black-0: rgba(0, 0, 0, 0); --black-50: rgba(0, 0, 0, 0.05); --black-100: rgba(0, 0, 0, 0.10); --black-200: rgba(0, 0, 0, 0.20); --black-300: rgba(0, 0, 0, 0.30); --black-400: rgba(0, 0, 0, 0.40); --black-500: rgba(0, 0, 0, 0.50); --black-600: rgba(0, 0, 0, 0.60); --black-700: rgba(0, 0, 0, 0.70); --black-800: rgba(0, 0, 0, 0.80); --black-900: rgba(0, 0, 0, 0.90); --black-950: rgba(0, 0, 0, 0.95); --black-1000: rgba(0, 0, 0, 1.00); --white-0: rgba(255, 255, 255, 0); --white-50: rgba(255, 255, 255, 0.05); --white-100: rgba(255, 255, 255, 0.10); --white-200: rgba(255, 255, 255, 0.20); --white-300: rgba(255, 255, 255, 0.30); --white-400: rgba(255, 255, 255, 0.40); --white-500: rgba(255, 255, 255, 0.50); --white-600: rgba(255, 255, 255, 0.60); --white-700: rgba(255, 255, 255, 0.70); --white-800: rgba(255, 255, 255, 0.80); --white-900: rgba(255, 255, 255, 0.90); --white-950: rgba(255, 255, 255, 0.95); --white-1000: rgba(255, 255, 255, 1.00); --gray-50: #FAFAFA; --gray-100: #F4F4F4; --gray-200: #E4E4E7; --gray-300: #D4D4D8; --gray-400: #9F9FA9; --gray-500: #71717B; --gray-600: #52525C; --gray-700: #3F3F46; --gray-800: #27272A; --gray-900: #18181B; --gray-950: #09090B; --red-50: #FEF2F2; --red-100: #FFE2E2; --red-200: #FFC9C9; --red-300: #FFA2A2; --red-400: #FF6467; --red-500: #FB2C36; --red-600: #E7000B; --red-700: #C10007; --red-800: #9F0712; --red-900: #82181A; --red-950: #460809; --orange-50: #FFF7ED; --orange-100: #FFEDD4; --orange-200: #FFD6A7; --orange-300: #FFB86A; --orange-400: #FF8904; --orange-500: #FF6900; --orange-600: #E54900; --orange-700: #CA3500; --orange-800: #9F2D00; --orange-900: #7E2A0C; --orange-950: #441306; --yellow-50: #FFFBEB; --yellow-100: #FEF3C6; --yellow-200: #FEE685; --yellow-300: #FFD230; --yellow-400: #FFB900; --yellow-500: #FE9A00; --yellow-600: #E17100; --yellow-700: #BB4D00; --yellow-800: #973C00; --yellow-900: #7B3306; --yellow-950: #461901; --lime-50: #F7FEE7; --lime-100: #ECFCCA; --lime-200: #D8F999; --lime-300: #BBF451; --lime-400: #9AE600; --lime-500: #7CCF00; --lime-600: #5EA500; --lime-700: #497D00; --lime-800: #3C6300; --lime-900: #35530E; --lime-950: #192E03; --green-50: #F0FDF4; --green-100: #DCFCE7; --green-200: #B9F8CF; --green-300: #7BF1A8; --green-400: #05DF72; --green-500: #00C950; --green-600: #00A63E; --green-700: #008236; --green-800: #016630; --green-900: #0D542B; --green-950: #032E15; --teal-50: #F0FDFA; --teal-100: #CBFBF1; --teal-200: #96F7E4; --teal-300: #46ECD5; --teal-400: #00D5BE; --teal-500: #00BBA7; --teal-600: #009689; --teal-700: #00786F; --teal-800: #005F5A; --teal-900: #0B4F4A; --teal-950: #022F2E; --cyan-50: #ECFEFF; --cyan-100: #CEFAFE; --cyan-200: #A2F4FD; --cyan-300: #53EAFD; --cyan-400: #00D3F2; --cyan-500: #00B8DB; --cyan-600: #0092B8; --cyan-700: #007595; --cyan-800: #005F78; --cyan-900: #104E64; --cyan-950: #053345; --purple-50: #FAF5FF; --purple-100: #F3E8FF; --purple-200: #E9D4FF; --purple-300: #DAB2FF; --purple-400: #C27AFF; --purple-500: #AD46FF; --purple-600: #9810FA; --purple-700: #8200DB; --purple-800: #6E11B0; --purple-900: #59168B; --purple-950: #3C0366; --pink-50: #FDF2F8; --pink-100: #FCE7F3; --pink-200: #FCCEE8; --pink-300: #FDA5D5; --pink-400: #FB64B6; --pink-500: #F6339A; --pink-600: #E60076; --pink-700: #C6005C; --pink-800: #A3004C; --pink-900: #861043; --pink-950: #510424; --rose-50: #FFF1F2; --rose-100: #FFE4E6; --rose-200: #FFCCD3; --rose-300: #FFA1AD; --rose-400: #FF637E; --rose-500: #FF2056; --rose-600: #EC003F; --rose-700: #C70036; --rose-800: #A50036; --rose-900: #8B0836; --rose-950: #4D0218; /* Tokens semânticos */ --bg-primary: var(--white-1000); --bg-secondary: var(--gray-100); --bg-tertiary: var(--gray-200); --bg-danger: var(--rose-100); --bg-positive: var(--green-100); --bg-warning: var(--yellow-100); --text-primary: var(--gray-900); --text-secondary: var(--black-700); --text-tertiary: var(--gray-400); --text-on-color: var(--white-1000); --text-inverse: var(--white-1000); --text-interactive: var(--purple-700); --text-danger: var(--rose-700); --text-positive: var(--green-700); --text-warning: var(--yellow-700); --text-info: var(--cyan-700); --icon-primary: var(--gray-900); --icon-secondary: var(--gray-600); --icon-tertiary: var(--gray-400); --icon-on-color: var(--white-1000); --icon-inverse: var(--white-1000); --icon-interactive: var(--purple-700); --icon-danger: var(--rose-700); --icon-positive: var(--green-700); --icon-warning: var(--yellow-700); --icon-info: var(--cyan-600); --border-primary: var(--gray-200); --border-secondary: var(--gray-400); --border-tertiary: var(--gray-600); } [data-theme="dark"] { --bg-primary: var(--gray-950); --bg-secondary: var(--gray-900); --bg-tertiary: var(--gray-800); --bg-danger: var(--gray-900); --bg-positive: var(--gray-900); --bg-warning: var(--gray-900); --text-primary: var(--white-950); --text-secondary: var(--white-600); --text-tertiary: var(--white-400); --text-on-color: var(--white-1000); --text-inverse: var(--gray-900); --text-interactive: var(--purple-400); --text-danger: var(--rose-400); --text-positive: var(--green-400); --text-warning: var(--yellow-400); --text-info: var(--cyan-400); --icon-primary: var(--white-950); --icon-secondary: var(--white-600); --icon-tertiary: var(--white-400); --icon-on-color: var(--white-1000); --icon-inverse: var(--gray-900); --icon-interactive: var(--purple-400); --icon-danger: var(--rose-400); --icon-positive: var(--green-400); --icon-warning: var(--yellow-400); --icon-info: var(--cyan-400); --border-primary: var(--gray-800); --border-secondary: var(--gray-600); --border-tertiary: var(--gray-400); } @theme inline { --color-white-0: var(--white-0); --color-white-50: var(--white-50); --color-white-100: var(--white-100); --color-white-200: var(--white-200); --color-white-300: var(--white-300); --color-white-400: var(--white-400); --color-white-500: var(--white-500); --color-white-600: var(--white-600); --color-white-700: var(--white-700); --color-white-800: var(--white-800); --color-white-900: var(--white-900); --color-white-950: var(--white-950); --color-white-1000: var(--white-1000); --color-black-0: var(--black-0); --color-black-50: var(--black-50); --color-black-100: var(--black-100); --color-black-200: var(--black-200); --color-black-300: var(--black-300); --color-black-400: var(--black-400); --color-black-500: var(--black-500); --color-black-600: var(--black-600); --color-black-700: var(--black-700); --color-black-800: var(--black-800); --color-black-900: var(--black-900); --color-black-950: var(--black-950); --color-black-1000: var(--black-1000); --color-gray-50: var(--gray-50); --color-gray-100: var(--gray-100); --color-gray-200: var(--gray-200); --color-gray-300: var(--gray-300); --color-gray-400: var(--gray-400); --color-gray-500: var(--gray-500); --color-gray-600: var(--gray-600); --color-gray-700: var(--gray-700); --color-gray-800: var(--gray-800); --color-gray-900: var(--gray-900); --color-gray-950: var(--gray-950); --color-red-50: var(--red-50); --color-red-100: var(--red-100); --color-red-200: var(--red-200); --color-red-300: var(--red-300); --color-red-400: var(--red-400); --color-red-500: var(--red-500); --color-red-600: var(--red-600); --color-red-700: var(--red-700); --color-red-800: var(--red-800); --color-red-900: var(--red-900); --color-red-950: var(--red-950); --color-orange-50: var(--orange-50); --color-orange-100: var(--orange-100); --color-orange-200: var(--orange-200); --color-orange-300: var(--orange-300); --color-orange-400: var(--orange-400); --color-orange-500: var(--orange-500); --color-orange-600: var(--orange-600); --color-orange-700: var(--orange-700); --color-orange-800: var(--orange-800); --color-orange-900: var(--orange-900); --color-orange-950: var(--orange-950); --color-yellow-50: var(--yellow-50); --color-yellow-100: var(--yellow-100); --color-yellow-200: var(--yellow-200); --color-yellow-300: var(--yellow-300); --color-yellow-400: var(--yellow-400); --color-yellow-500: var(--yellow-500); --color-yellow-600: var(--yellow-600); --color-yellow-700: var(--yellow-700); --color-yellow-800: var(--yellow-800); --color-yellow-900: var(--yellow-900); --color-yellow-950: var(--yellow-950); --color-lime-50: var(--lime-50); --color-lime-100: var(--lime-100); --color-lime-200: var(--lime-200); --color-lime-300: var(--lime-300); --color-lime-400: var(--lime-400); --color-lime-500: var(--lime-500); --color-lime-600: var(--lime-600); --color-lime-700: var(--lime-700); --color-lime-800: var(--lime-800); --color-lime-900: var(--lime-900); --color-lime-950: var(--lime-950); --color-green-50: var(--green-50); --color-green-100: var(--green-100); --color-green-200: var(--green-200); --color-green-300: var(--green-300); --color-green-400: var(--green-400); --color-green-500: var(--green-500); --color-green-600: var(--green-600); --color-green-700: var(--green-700); --color-green-800: var(--green-800); --color-green-900: var(--green-900); --color-green-950: var(--green-950); --color-teal-50: var(--teal-50); --color-teal-100: var(--teal-100); --color-teal-200: var(--teal-200); --color-teal-300: var(--teal-300); --color-teal-400: var(--teal-400); --color-teal-500: var(--teal-500); --color-teal-600: var(--teal-600); --color-teal-700: var(--teal-700); --color-teal-800: var(--teal-800); --color-teal-900: var(--teal-900); --color-teal-950: var(--teal-950); --color-cyan-50: var(--cyan-50); --color-cyan-100: var(--cyan-100); --color-cyan-200: var(--cyan-200); --color-cyan-300: var(--cyan-300); --color-cyan-400: var(--cyan-400); --color-cyan-500: var(--cyan-500); --color-cyan-600: var(--cyan-600); --color-cyan-700: var(--cyan-700); --color-cyan-800: var(--cyan-800); --color-cyan-900: var(--cyan-900); --color-cyan-950: var(--cyan-950); --color-purple-50: var(--purple-50); --color-purple-100: var(--purple-100); --color-purple-200: var(--purple-200); --color-purple-300: var(--purple-300); --color-purple-400: var(--purple-400); --color-purple-500: var(--purple-500); --color-purple-600: var(--purple-600); --color-purple-700: var(--purple-700); --color-purple-800: var(--purple-800); --color-purple-900: var(--purple-900); --color-purple-950: var(--purple-950); --color-pink-50: var(--pink-50); --color-pink-100: var(--pink-100); --color-pink-200: var(--pink-200); --color-pink-300: var(--pink-300); --color-pink-400: var(--pink-400); --color-pink-500: var(--pink-500); --color-pink-600: var(--pink-600); --color-pink-700: var(--pink-700); --color-pink-800: var(--pink-800); --color-pink-900: var(--pink-900); --color-pink-950: var(--pink-950); --color-bg-primary: var(--bg-primary); --color-bg-secondary: var(--bg-secondary); --color-bg-tertiary: var(--bg-tertiary); --color-bg-danger: var(--bg-danger); --color-bg-positive: var(--bg-positive); --color-bg-warning: var(--bg-warning); --color-text-primary: var(--text-primary); --color-text-secondary: var(--text-secondary); --color-text-tertiary: var(--text-tertiary); --color-text-on-color: var(--text-on-color); --color-text-inverse: var(--text-inverse); --color-text-interactive: var(--text-interactive); --color-text-danger: var(--text-danger); --color-text-positive: var(--text-positive); --color-text-warning: var(--text-warning); --color-text-info: var(--text-info); --color-icon-primary: var(--icon-primary); --color-icon-secondary: var(--icon-secondary); --color-icon-tertiary: var(--icon-tertiary); --color-icon-on-color: var(--icon-on-color); --color-icon-inverse: var(--icon-inverse); --color-icon-interactive: var(--icon-interactive); --color-icon-danger: var(--icon-danger); --color-icon-positive: var(--icon-positive); --color-icon-warning: var(--icon-warning); --color-icon-info: var(--icon-info); --color-border-primary: var(--border-primary); --color-border-secondary: var(--border-secondary); --color-border-tertiary: var(--border-tertiary); } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/colors.ts ================================================ const primitives = { black: { 0: 'rgba(0, 0, 0, 0)', 50: 'rgba(0, 0, 0, 0.05)', 100: 'rgba(0, 0, 0, 0.10)', 200: 'rgba(0, 0, 0, 0.20)', 300: 'rgba(0, 0, 0, 0.30)', 400: 'rgba(0, 0, 0, 0.40)', 500: 'rgba(0, 0, 0, 0.50)', 600: 'rgba(0, 0, 0, 0.60)', 700: 'rgba(0, 0, 0, 0.70)', 800: 'rgba(0, 0, 0, 0.80)', 900: 'rgba(0, 0, 0, 0.90)', 950: 'rgba(0, 0, 0, 0.95)', 1000: 'rgba(0, 0, 0, 1.00)', }, white: { 0: 'rgba(255, 255, 255, 0)', 50: 'rgba(255, 255, 255, 0.05)', 100: 'rgba(255, 255, 255, 0.10)', 200: 'rgba(255, 255, 255, 0.20)', 300: 'rgba(255, 255, 255, 0.30)', 400: 'rgba(255, 255, 255, 0.40)', 500: 'rgba(255, 255, 255, 0.50)', 600: 'rgba(255, 255, 255, 0.60)', 700: 'rgba(255, 255, 255, 0.70)', 800: 'rgba(255, 255, 255, 0.80)', 900: 'rgba(255, 255, 255, 0.90)', 950: 'rgba(255, 255, 255, 0.95)', 1000: 'rgba(255, 255, 255, 1.00)', }, gray: { 50: '#FAFAFA', 100: '#F4F4F4', 200: '#E4E4E7', 300: '#D4D4D8', 400: '#9F9FA9', 500: '#71717B', 600: '#52525C', 700: '#3F3F46', 800: '#27272A', 900: '#18181B', 950: '#09090B', }, red: { 50: '#FEF2F2', 100: '#FFE2E2', 200: '#FFC9C9', 300: '#FFA2A2', 400: '#FF6467', 500: '#FB2C36', 600: '#E7000B', 700: '#C10007', 800: '#9F0712', 900: '#82181A', 950: '#460809', }, orange: { 50: '#FFF7ED', 100: '#FFEDD4', 200: '#FFD6A7', 300: '#FFB86A', 400: '#FF8904', 500: '#FF6900', 600: '#E54900', 700: '#CA3500', 800: '#9F2D00', 900: '#7E2A0C', 950: '#441306', }, yellow: { 50: '#FFFBEB', 100: '#FEF3C6', 200: '#FEE685', 300: '#FFD230', 400: '#FFB900', 500: '#FE9A00', 600: '#E17100', 700: '#BB4D00', 800: '#973C00', 900: '#7B3306', 950: '#461901', }, lime: { 50: '#F7FEE7', 100: '#ECFCCA', 200: '#D8F999', 300: '#BBF451', 400: '#9AE600', 500: '#7CCF00', 600: '#5EA500', 700: '#497D00', 800: '#3C6300', 900: '#35530E', 950: '#192E03', }, green: { 50: '#F0FDF4', 100: '#DCFCE7', 200: '#B9F8CF', 300: '#7BF1A8', 400: '#05DF72', 500: '#00C950', 600: '#00A63E', 700: '#008236', 800: '#016630', 900: '#0D542B', 950: '#032E15', }, teal: { 50: '#F0FDFA', 100: '#CBFBF1', 200: '#96F7E4', 300: '#46ECD5', 400: '#00D5BE', 500: '#00BBA7', 600: '#009689', 700: '#00786F', 800: '#005F5A', 900: '#0B4F4A', 950: '#022F2E', }, cyan: { 50: '#ECFEFF', 100: '#CEFAFE', 200: '#A2F4FD', 300: '#53EAFD', 400: '#00D3F2', 500: '#00B8DB', 600: '#0092B8', 700: '#007595', 800: '#005F78', 900: '#104E64', 950: '#053345', }, purple: { 50: '#FAF5FF', 100: '#F3E8FF', 200: '#E9D4FF', 300: '#DAB2FF', 400: '#C27AFF', 500: '#AD46FF', 600: '#9810FA', 700: '#8200DB', 800: '#6E11B0', 900: '#59168B', 950: '#3C0366', }, pink: { 50: '#FDF2F8', 100: '#FCE7F3', 200: '#FCCEE8', 300: '#FDA5D5', 400: '#FB64B6', 500: '#F6339A', 600: '#E60076', 700: '#C6005C', 800: '#A3004C', 900: '#861043', 950: '#510424', }, rose: { 50: '#FFF1F2', 100: '#FFE4E6', 200: '#FFCCD3', 300: '#FFA1AD', 400: '#FF637E', 500: '#FF2056', 600: '#EC003F', 700: '#C70036', 800: '#A50036', 900: '#8B0836', 950: '#4D0218', }, }; const semantics = { light: { background: { primary: primitives.white[1000], secondary: primitives.gray[100], tertiary: primitives.gray[200], danger: primitives.rose[100], positive: primitives.green[100], warning: primitives.yellow[100], }, text: { primary: primitives.gray[900], secondary: primitives.black[700], tertiary: primitives.gray[400], onColor: primitives.white[1000], inverse: primitives.white[1000], interactive: primitives.purple[700], danger: primitives.rose[700], positive: primitives.green[700], warning: primitives.yellow[700], info: primitives.cyan[700], }, icon: { primary: primitives.gray[900], secondary: primitives.gray[600], tertiary: primitives.gray[400], onColor: primitives.white[1000], inverse: primitives.white[1000], interactive: primitives.purple[700], danger: primitives.rose[700], positive: primitives.green[700], warning: primitives.yellow[700], info: primitives.cyan[600], }, border: { primary: primitives.gray[200], secondary: primitives.gray[400], tertiary: primitives.gray[600], }, }, dark: { background: { primary: primitives.gray[950], secondary: primitives.gray[900], tertiary: primitives.gray[800], danger: primitives.gray[900], positive: primitives.gray[900], warning: primitives.gray[900], }, text: { primary: primitives.white[950], secondary: primitives.white[600], tertiary: primitives.white[400], onColor: primitives.white[1000], inverse: primitives.gray[900], interactive: primitives.purple[400], danger: primitives.rose[400], positive: primitives.green[400], warning: primitives.yellow[400], info: primitives.cyan[400], }, icon: { primary: primitives.white[950], secondary: primitives.white[600], tertiary: primitives.white[400], onColor: primitives.white[1000], inverse: primitives.gray[900], interactive: primitives.purple[400], danger: primitives.rose[400], positive: primitives.green[400], warning: primitives.yellow[400], info: primitives.cyan[400], }, border: { primary: primitives.gray[800], secondary: primitives.gray[600], tertiary: primitives.gray[400], }, }, }; const colors = { primitives, semantics, }; export type Color = typeof colors; export default colors; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/container.css ================================================ :root { --ids-container-sm: 40rem; --ids-container-md: 48rem; --ids-container-lg: 64rem; --ids-container-xl: 80rem; --ids-container-2xl: 90rem; } @theme { --container-sm: var(--ids-container-sm); --container-md: var(--ids-container-md); --container-lg: var(--ids-container-lg); --container-xl: var(--ids-container-xl); --container-2xl: var(--ids-container-2xl); } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/container.ts ================================================ export type Container = { 'xs': string; 'sm': string; 'md': string; 'lg': string; '2xl': string; } const container: Container = { 'xs':'40rem', 'sm':'48rem', 'md':'64rem', 'lg':'80rem', '2xl':'90rem', }; export default container; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/size.css ================================================ :root { --ids-size-scale-0: 0rem; --ids-size-scale-050: 0.125rem; --ids-size-scale-100: 0.25rem; --ids-size-scale-150: 0.375rem; --ids-size-scale-200: 0.5rem; --ids-size-scale-300: 0.75rem; --ids-size-scale-400: 1rem; --ids-size-scale-500: 1.25rem; --ids-size-scale-600: 1.5rem; --ids-size-scale-800: 2rem; --ids-size-scale-1000: 2.5rem; --ids-size-scale-1200: 3rem; --ids-size-scale-1400: 3.5rem; --ids-size-scale-1600: 4rem; --ids-size-scale-1800: 4.5rem; --ids-size-scale-2000: 5rem; --ids-size-scale-2400: 6rem; --ids-size-scale-3200: 8rem; --ids-size-scale-4000: 10rem; --ids-size-scale-6400: 16rem; --ids-size-scale-width-xs: 20rem; --ids-size-scale-width-sm: 24rem; --ids-size-scale-width-md: 28rem; --ids-size-scale-width-lg: 32rem; --ids-size-scale-width-xl: 36rem; --ids-size-scale-width-2xl: 42rem; --ids-size-scale-width-3xl: 48rem; --ids-size-scale-width-4xl: 56rem; --ids-size-scale-width-5xl: 64rem; --ids-size-scale-width-6xl: 72rem; --ids-size-scale-width-7xl: 80rem; --ids-size-scale-width-8xl: 90rem; --ids-size-radius-100: 0.25rem; --ids-size-radius-200: 0.5rem; --ids-size-radius-300: 0.75rem; --ids-size-radius-400: 1rem; --ids-size-radius-600: 1.5rem; --ids-size-radius-full: 9999rem; } @theme { --spacing-050: var(--ids-size-scale-050); --spacing-100: var(--ids-size-scale-100); --spacing-150: var(--ids-size-scale-150); --spacing-200: var(--ids-size-scale-200); --spacing-300: var(--ids-size-scale-300); --spacing-400: var(--ids-size-scale-400); --spacing-500: var(--ids-size-scale-500); --spacing-600: var(--ids-size-scale-600); --spacing-800: var(--ids-size-scale-800); --spacing-1000: var(--ids-size-scale-1000); --spacing-1200: var(--ids-size-scale-1200); --spacing-1400: var(--ids-size-scale-1400); --spacing-1600: var(--ids-size-scale-1600); --spacing-1800: var(--ids-size-scale-1800); --spacing-2000: var(--ids-size-scale-2000); --spacing-2400: var(--ids-size-scale-2400); --spacing-3200: var(--ids-size-scale-3200); --spacing-4000: var(--ids-size-scale-4000); --spacing-width-xs: var(--ids-size-scale-width-xs); --spacing-width-sm: var(--ids-size-scale-width-sm); --spacing-width-md: var(--ids-size-scale-width-md); --spacing-width-lg: var(--ids-size-scale-width-lg); --spacing-width-xl: var(--ids-size-scale-width-xl); --spacing-width-2xl: var(--ids-size-scale-width-2xl); --spacing-width-3xl: var(--ids-size-scale-width-3xl); --spacing-width-4xl: var(--ids-size-scale-width-4xl); --spacing-width-5xl: var(--ids-size-scale-width-5xl); --spacing-width-6xl: var(--ids-size-scale-width-6xl); --spacing-width-7xl: var(--ids-size-scale-width-7xl); --spacing-width-8xl: var(--ids-size-scale-width-8xl); --radius-100: var(--ids-size-radius-100); --radius-200: var(--ids-size-radius-200); --radius-300: var(--ids-size-radius-300); --radius-400: var(--ids-size-radius-400); --radius-600: var(--ids-size-radius-600); --radius-full: var(--ids-size-radius-full); } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/size.ts ================================================ const scale = { '0': '0rem', '025': '0.0625rem', '050': '0.125rem', '100': '0.25rem', '150': '0.375rem', '200': '0.5rem', '300': '0.75rem', '400': '1rem', '500': '1.25rem', '600': '1.5rem', '800': '2rem', '1000': '2.5rem', '1200': '3rem', '1400': '3.5rem', '1600': '4rem', '1800': '4.5rem', '2000': '5rem', '2400': '6rem', '3200': '8rem', '4000': '10rem', '6400': '16rem', 'width-xs': '20rem', 'width-sm': '24rem', 'width-md': '28rem', 'width-lg': '32rem', 'width-xl': '36rem', 'width-2xl': '42rem', 'width-3xl': '48rem', 'width-4xl': '56rem', 'width-5xl': '64rem', 'width-6xl': '72rem', 'width-7xl': '80rem', 'width-8xl': '90rem', }; const radius = { '100': '0.25rem', '200': '0.5rem', '300': '0.75rem', '400': '1rem', '600': '1.5rem', 'full': '9999rem', }; const size = { scale: scale, radius: radius, }; /** * The values are defined using the `rem` unit to ensure that they are relative to the root font size. */ export type Size = typeof size; export default size; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/spacing.css ================================================ :root { --spacing-1: 4px; --spacing-2: 8px; --spacing-3: 12px; --spacing-4: 16px; --spacing-5: 20px; --spacing-6: 24px; --spacing-7: 28px; --spacing-8: 32px; } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/spacing.ts ================================================ export type Spacing = { 1: number; 2: number; 3: number; 4: number; 5: number; 6: number; 7: number; 8: number; } const spacing: Spacing = { 1: 4, 2: 8, 3: 12, 4: 16, 5: 20, 6: 24, 7: 28, 8: 32, }; export default spacing; ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/typography.css ================================================ :root { /* Families */ --ids-typography-family-sans: Inter, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --ids-typography-family-sans-condensed: Acumin-Pro-Extra-Condensed, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --ids-typography-family-serif: GTUltra, ui-serif, Georgia, Cambria, "Times New Roman", serif; --ids-typography-family-mono: Roboto; /* Weights */ --ids-typography-weight-regular: 400; --ids-typography-weight-medium: 500; --ids-typography-weight-semibold: 600; --ids-typography-weight-bold: 700; /* Scales */ --ids-typography-scale-300: 0.75rem; --ids-typography-scale-350: 0.875rem; --ids-typography-scale-400: 1rem; --ids-typography-scale-450: 1.125rem; --ids-typography-scale-500: 1.25rem; --ids-typography-scale-600: 1.5rem; --ids-typography-scale-700: 1.75rem; --ids-typography-scale-800: 2rem; --ids-typography-scale-900: 2.25rem; --ids-typography-scale-1000: 2.5rem; --ids-typography-scale-1100: 2.75rem; --ids-typography-scale-1200: 3rem; --ids-typography-scale-1400: 3.5rem; --ids-typography-scale-1600: 4rem; --ids-typography-scale-1800: 4.5rem; --ids-typography-scale-2000: 5rem; --ids-typography-scale-2200: 5.5rem; --ids-typography-scale-2400: 6rem; --ids-typography-scale-2800: 6.5rem; } @theme { /* Font families */ --font-sans: var(--ids-typography-family-sans); --font-sans-condensed: var(--ids-typography-family-sans-condensed); --font-serif: var(--ids-typography-family-serif); --font-mono: var(--ids-typography-family-mono); /* Font Weight */ --font-weight-regular: var(--ids-typography-weight-regular); --font-weight-medium: var(--ids-typography-weight-medium); --font-weight-semibold: var(--ids-typography-weight-semibold); --font-weight-bold: var(--ids-typography-weight-bold); /* Ultra */ --text-ultra-2400: var(--ids-typography-scale-2400); --text-ultra-2400--line-height: var(--ids-typography-scale-2800); --text-ultra-2400--letter-spacing: -3.84px; --text-ultra-2000: var(--ids-typography-scale-2000); --text-ultra-2000--line-height: var(--ids-typography-scale-2200); --text-ultra-2000--letter-spacing: -3.2px; --text-ultra-1800: var(--ids-typography-scale-1800); --text-ultra-1800--line-height: var(--ids-typography-scale-2000); --text-ultra-1800--letter-spacing: -2.88px; /* Display */ --text-display-1600: var(--ids-typography-scale-1600); --text-display-1600--line-height: var(--ids-typography-scale-1800); --text-display-1600--letter-spacing: -1.92px; --text-display-1400: var(--ids-typography-scale-1400); --text-display-1400--line-height: var(--ids-typography-scale-1600); --text-display-1400--letter-spacing: -1.68px; --text-display-1200: var(--ids-typography-scale-1200); --text-display-1200--line-height: var(--ids-typography-scale-1400); --text-display-1200--letter-spacing: -0.96px; /* Headline */ --text-headline-1000: var(--ids-typography-scale-1000); --text-headline-1000--line-height: var(--ids-typography-scale-1200); --text-headline-1000--letter-spacing: -0.8px; --text-headline-900: var(--ids-typography-scale-900); --text-headline-900--line-height: var(--ids-typography-scale-1100); --text-headline-900--letter-spacing: -1.08px; --text-headline-800: var(--ids-typography-scale-800); --text-headline-800--line-height: var(--ids-typography-scale-1000); --text-headline-800--letter-spacing: -0.64px; /* Title */ --text-title-700: var(--ids-typography-scale-700); --text-title-700--line-height: var(--ids-typography-scale-900); --text-title-700--letter-spacing: -0.28px; --text-title-600: var(--ids-typography-scale-600); --text-title-600--line-height: var(--ids-typography-scale-800); --text-title-600--letter-spacing: -0.48px; --text-title-500: var(--ids-typography-scale-500); --text-title-500--line-height: var(--ids-typography-scale-700); --text-title-500--letter-spacing: -0.4px; /* Lead */ --text-lead-700: var(--ids-typography-scale-700); --text-lead-700--line-height: var(--ids-typography-scale-900); --text-lead-700--letter-spacing: -0.56px; --text-lead-600: var(--ids-typography-scale-600); --text-lead-600--line-height: var(--ids-typography-scale-800); --text-lead-600--letter-spacing: -0.48px; --text-lead-500: var(--ids-typography-scale-500); --text-lead-500--line-height: var(--ids-typography-scale-700); --text-lead-500--letter-spacing: -0.2px; /* Body */ --text-body-450: var(--ids-typography-scale-450); --text-body-450--line-height: var(--ids-typography-scale-700); --text-body-450--letter-spacing: -0.56px; --text-body-400: var(--ids-typography-scale-400); --text-body-400--line-height: var(--ids-typography-scale-600); --text-body-350: var(--ids-typography-scale-350); --text-body-350--line-height: var(--ids-typography-scale-500); /* Caption */ --text-caption-300: var(--ids-typography-scale-300); --text-caption-300--line-height: var(--ids-typography-scale-400); /* Support */ --text-support-1000: var(--ids-typography-scale-1000); --text-support-1000--line-height: var(--ids-typography-scale-1000); --text-support-800: var(--ids-typography-scale-800); --text-support-800--line-height: var(--ids-typography-scale-800); --text-support-600: var(--ids-typography-scale-600); --text-support-600--line-height: var(--ids-typography-scale-600); } ================================================ FILE: projects/insper/design-system/src/lib/styles/theme/tokens/typography.ts ================================================ const fontFamily = { sans: 'Inter, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', sansCondensed: 'Acumin-Pro-Extra-Condensed, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', serif: 'GTUltra, ui-serif, Georgia, Cambria, "Times New Roman", serif', mono: 'Roboto', }; const fontWeight = { regular: 400, medium: 500, semibold: 600, bold: 700, }; const fontScale = { '300': '0.75rem', '350': '0.875rem', '400': '1rem', '450': '1.125rem', '500': '1.25rem', '600': '1.5rem', '700': '1.75rem', '800': '2rem', '900': '2.25rem', '1000': '2.5rem', '1100': '2.75rem', '1200': '3rem', '1400': '3.5rem', '1600': '4rem', '1800': '4.5rem', '2000': '5rem', '2200': '5.5rem', '2400': '6rem', '2800': '6.5rem', }; const variants = { ultra: { '2400': { size: fontScale['2400'], lineHeight: fontScale['2800'], letterSpacing: '-3.84px', weight: fontWeight.medium }, '2400--str': { size: fontScale['2400'], lineHeight: fontScale['2800'], letterSpacing: '-3.84px', weight: fontWeight.bold }, '2400--exp': { size: fontScale['2400'], lineHeight: fontScale['2800'], letterSpacing: '-3.84px', weight: fontWeight.medium }, '2000': { size: fontScale['2000'], lineHeight: fontScale['2200'], letterSpacing: '-3.2px', weight: fontWeight.medium }, '2000--str': { size: fontScale['2000'], lineHeight: fontScale['2200'], letterSpacing: '-3.2px', weight: fontWeight.bold }, '2000--exp': { size: fontScale['2000'], lineHeight: fontScale['2200'], letterSpacing: '-3.2px', weight: fontWeight.medium }, '1800': { size: fontScale['1800'], lineHeight: fontScale['2000'], letterSpacing: '-2.88px', weight: fontWeight.medium }, '1800--str': { size: fontScale['1800'], lineHeight: fontScale['2000'], letterSpacing: '-2.88px', weight: fontWeight.bold }, '1800--exp': { size: fontScale['1800'], lineHeight: fontScale['2000'], letterSpacing: '-2.88px', weight: fontWeight.medium }, }, display: { '1600': { size: fontScale['1600'], lineHeight: fontScale['1800'], letterSpacing: '-1.92px', weight: fontWeight.bold }, '1600--str': { size: fontScale['1600'], lineHeight: fontScale['1800'], letterSpacing: '-1.92px', weight: fontWeight.bold }, '1600--exp': { size: fontScale['1600'], lineHeight: fontScale['1800'], letterSpacing: '-1.92px', weight: fontWeight.bold }, '1400': { size: fontScale['1400'], lineHeight: fontScale['1600'], letterSpacing: '-1.68px', weight: fontWeight.bold }, '1400--str': { size: fontScale['1400'], lineHeight: fontScale['1600'], letterSpacing: '-1.68px', weight: fontWeight.bold }, '1400--exp': { size: fontScale['1400'], lineHeight: fontScale['1600'], letterSpacing: '-1.68px', weight: fontWeight.bold }, '1200': { size: fontScale['1200'], lineHeight: fontScale['1400'], letterSpacing: '-0.96px', weight: fontWeight.bold }, '1200--str': { size: fontScale['1200'], lineHeight: fontScale['1400'], letterSpacing: '-0.96px', weight: fontWeight.bold }, '1200--exp': { size: fontScale['1200'], lineHeight: fontScale['1400'], letterSpacing: '-0.96px', weight: fontWeight.bold }, }, headline: { '1000': { size: fontScale['1000'], lineHeight: fontScale['1200'], letterSpacing: '-0.8px', weight: fontWeight.semibold }, '1000--str': { size: fontScale['1000'], lineHeight: fontScale['1200'], letterSpacing: '-0.8px', weight: fontWeight.bold }, '900': { size: fontScale['900'], lineHeight: fontScale['1100'], letterSpacing: '-1.08px', weight: fontWeight.semibold }, '900--str': { size: fontScale['900'], lineHeight: fontScale['1100'], letterSpacing: '-1.08px', weight: fontWeight.bold }, '800': { size: fontScale['800'], lineHeight: fontScale['1000'], letterSpacing: '-0.64px', weight: fontWeight.semibold }, '800--str': { size: fontScale['800'], lineHeight: fontScale['1000'], letterSpacing: '-0.64px', weight: fontWeight.bold }, }, title: { '700': { size: fontScale['700'], lineHeight: fontScale['900'], letterSpacing: '-0.28px', weight: fontWeight.semibold }, '700--str': { size: fontScale['700'], lineHeight: fontScale['900'], letterSpacing: '-0.28px', weight: fontWeight.bold }, '600': { size: fontScale['600'], lineHeight: fontScale['800'], letterSpacing: '-0.48px', weight: fontWeight.semibold }, '600--str': { size: fontScale['600'], lineHeight: fontScale['800'], letterSpacing: '-0.48px', weight: fontWeight.bold }, '500': { size: fontScale['500'], lineHeight: fontScale['700'], letterSpacing: '-0.4px', weight: fontWeight.semibold }, '500--str': { size: fontScale['500'], lineHeight: fontScale['700'], letterSpacing: '-0.4px', weight: fontWeight.bold }, }, lead: { '700': { size: fontScale['700'], lineHeight: fontScale['900'], letterSpacing: '-0.56px', weight: fontWeight.regular }, '700--str': { size: fontScale['700'], lineHeight: fontScale['900'], letterSpacing: '-0.56px', weight: fontWeight.medium }, '600': { size: fontScale['600'], lineHeight: fontScale['800'], letterSpacing: '-0.48px', weight: fontWeight.regular }, '600--str': { size: fontScale['600'], lineHeight: fontScale['800'], letterSpacing: '-0.48px', weight: fontWeight.medium }, '500': { size: fontScale['500'], lineHeight: fontScale['700'], letterSpacing: '-0.2px', weight: fontWeight.regular }, '500--str': { size: fontScale['500'], lineHeight: fontScale['700'], letterSpacing: '-0.2px', weight: fontWeight.medium }, }, body: { '450': { size: fontScale['450'], lineHeight: fontScale['600'], letterSpacing: '-0.56px', weight: fontWeight.regular }, '450--str': { size: fontScale['450'], lineHeight: fontScale['600'], letterSpacing: '-0.56px', weight: fontWeight.medium }, '400': { size: fontScale['400'], lineHeight: fontScale['600'], letterSpacing: 'normal', weight: fontWeight.regular }, '400--str': { size: fontScale['400'], lineHeight: fontScale['600'], letterSpacing: 'normal', weight: fontWeight.medium }, '350': { size: fontScale['350'], lineHeight: fontScale['500'], letterSpacing: 'normal', weight: fontWeight.regular }, '350--str': { size: fontScale['350'], lineHeight: fontScale['500'], letterSpacing: 'normal', weight: fontWeight.medium }, }, caption: { '300': { size: fontScale['300'], lineHeight: fontScale['400'], letterSpacing: 'normal', weight: fontWeight.regular }, '300--str': { size: fontScale['300'], lineHeight: fontScale['400'], letterSpacing: 'normal', weight: fontWeight.medium }, }, support: { '1000': { size: fontScale['1000'], lineHeight: fontScale['1000'], letterSpacing: 'normal', weight: fontWeight.semibold }, '1000--upper':{ size: fontScale['1000'], lineHeight: fontScale['1000'], letterSpacing: 'normal', weight: fontWeight.semibold }, '800': { size: fontScale['800'], lineHeight: fontScale['800'], letterSpacing: 'normal', weight: fontWeight.semibold }, '800--upper': { size: fontScale['800'], lineHeight: fontScale['800'], letterSpacing: 'normal', weight: fontWeight.semibold }, '600': { size: fontScale['600'], lineHeight: fontScale['600'], letterSpacing: 'normal', weight: fontWeight.semibold }, '600--upper': { size: fontScale['600'], lineHeight: fontScale['600'], letterSpacing: 'normal', weight: fontWeight.semibold }, }, } as const; const typography = { scale: fontScale, family: fontFamily, weight: fontWeight, variants, }; export type Typography = typeof typography; export default typography; ================================================ FILE: projects/preview/package.json ================================================ { "name": "preview", "version": "0.0.0", "private": true, "dependencies": { "@techinsper/insper-design-system": "workspace:^" } } ================================================ FILE: projects/preview/tsconfig.app.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", "types": [] }, "files": [ "src/main.ts" ], "include": [ "src/**/*.d.ts" ] } ================================================ FILE: projects/preview/tsconfig.spec.json ================================================ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", "types": [ "jasmine" ] }, "include": [ "src/**/*.spec.ts", "src/**/*.d.ts" ] } ================================================ FILE: projects/preview/src/index.html ================================================ Preview ================================================ FILE: projects/preview/src/main.ts ================================================ import { bootstrapApplication } from '@angular/platform-browser'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); ================================================ FILE: projects/preview/src/styles.css ================================================ [Empty file] ================================================ FILE: projects/preview/src/app/app.component.css ================================================ [Empty file] ================================================ FILE: projects/preview/src/app/app.component.html ================================================ ================================================ FILE: projects/preview/src/app/app.component.ts ================================================ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', standalone: true, imports: [RouterOutlet], templateUrl: './app.component.html', styleUrl: './app.component.css' }) export class AppComponent { } ================================================ FILE: projects/preview/src/app/app.config.ts ================================================ import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [provideRouter(routes)] }; ================================================ FILE: projects/preview/src/app/app.routes.ts ================================================ import { Routes } from '@angular/router'; import { ButtonPreviewComponent } from './button-preview/button-preview.component'; export const routes: Routes = [ { path: 'button', component: ButtonPreviewComponent } ]; ================================================ FILE: projects/preview/src/app/button-preview/button-preview.component.css ================================================ [Empty file] ================================================ FILE: projects/preview/src/app/button-preview/button-preview.component.html ================================================

================================================ FILE: projects/preview/src/app/button-preview/button-preview.component.spec.ts ================================================ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ButtonPreviewComponent } from './button-preview.component'; describe('ButtonPreviewComponent', () => { let component: ButtonPreviewComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ButtonPreviewComponent] }) .compileComponents(); fixture = TestBed.createComponent(ButtonPreviewComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); }); ================================================ FILE: projects/preview/src/app/button-preview/button-preview.component.ts ================================================ import { Component } from '@angular/core'; import { ToastComponent } from "../../../../insper/design-system/src/lib/components/toast/toast.component"; import { ButtonComponent, StackComponent } from '../../../../insper/design-system/src/public-api'; @Component({ selector: 'button-preview', standalone: true, imports: [ButtonComponent, StackComponent, ToastComponent], templateUrl: './button-preview.component.html', styleUrl: './button-preview.component.css' }) export class ButtonPreviewComponent { } ================================================ FILE: projects/preview/src/assets/.gitkeep ================================================ [Empty file] ================================================ FILE: projects/preview/src/stories/avatar.stories.ts ================================================ import type { Meta, StoryObj } from '@storybook/angular'; import { imageBase64 } from '../../../insper/design-system/src/lib/components/avatar/utils/testing'; import { AvatarComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Avatar', component: AvatarComponent, argTypes: { label: { control: { type: 'text' }, }, image: { control: { type: 'text' }, }, icon: { control: { type: 'text' }, }, size: { control: { type: 'select' }, options: ['sm', 'md', 'lg'], }, rounded: { control: { type: 'boolean' }, }, }, args: { label: '', image: '', icon: '', size: 'md', rounded: true, }, } export default meta; export const Label: StoryObj = { args: { ...meta.args, label: 'Avatar Label' }, }; export const Image: StoryObj = { args: { ...meta.args, image: imageBase64 }, }; export const Icon: StoryObj = { args: { ...meta.args, icon: 'sunglasses' }, }; ================================================ FILE: projects/preview/src/stories/container.stories.ts ================================================ import type { Meta, StoryObj } from '@storybook/angular'; import { ContainerComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Elements/Container', component: ContainerComponent, render: (args) => ({ props: args, template: `

Background color is for demonstration purposes

`, }), }; export default meta; export const Default: StoryObj = {}; ================================================ FILE: projects/preview/src/stories/form-field.stories.ts ================================================ import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { FormFieldComponent, IconComponent, InputTextComponent } from '../../../insper/design-system/src/public-api'; type StoryComponent = FormFieldComponent & { leadingIcon: string; trailingIcon: string; disabled: boolean; placeholder: string; required: boolean; }; const meta: Meta = { title: 'Insper Design System/Components/Form Item', component: FormFieldComponent, decorators: [ moduleMetadata({ imports: [ InputTextComponent, IconComponent ], }) ], argTypes: { disabled: { control: 'boolean', }, placeholder: { control: 'text' }, required: { control: 'boolean' }, leadingIcon: { control: 'text', }, trailingIcon: { control: 'text', }, }, args: { leadingIcon: 'pen', trailingIcon: 'plus', disabled: false, placeholder: 'Enter your name', required: false, }, render: (args) => ({ props: args, template: ` `, }), } export default meta; export const Default: StoryObj = {}; ================================================ FILE: projects/preview/src/stories/grid.stories.ts ================================================ import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { GridItemComponent } from '../../../insper/design-system/src/lib/components/grid-item/grid-item.component'; import { GridComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Elements/Grid', component: GridComponent, decorators: [ moduleMetadata({ imports: [GridItemComponent] }) ], argTypes: { alignItem: { control: { type: 'select', }, options: ['start', 'center', 'end', 'stretch'], }, justifyItem: { control: { type: 'select', }, options: ['start', 'center', 'end', 'stretch'], }, }, args: { alignItem: 'stretch', justifyItem: 'stretch', }, render: (args) => ({ props: args, template: `
Grid Column
Grid Column
Grid Column
Grid Column
Grid Column
Grid Column
`, }), } export default meta; export const Default: StoryObj = {}; ================================================ FILE: projects/preview/src/stories/input-checkbox.stories.ts ================================================ import { FormsModule } from '@angular/forms'; import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { InputCheckboxComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Input Checkbox', component: InputCheckboxComponent, decorators: [ moduleMetadata({ imports: [FormsModule], }) ], argTypes: { checked: { control: 'boolean' }, disabled: { control: 'boolean' }, }, args: { checked: false, disabled: false, }, render: (args) => ({ props: args, template: ` ` }) } export default meta; export const Default: StoryObj = {}; ================================================ FILE: projects/preview/src/stories/introduction.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; ## Iniciando com o Insper Design System O Insper Design System é um sistema de design desenvolvido com o objetivo de padronizar a interface e melhorar a experiência do usuário, oferecendo uma variedade de componentes reutilizáveis, estilos e utilitários que facilitam o desenvolvimento de aplicações consistentes e acessíveis. ### Tecnologias utilizadas - Angular - Tailwind CSS - Storybook - JSDoc - Jasmine/Karma ### Requisitos - Node.js >= 18 - Angular >= 17.3 ## Instalação Utilize a `@angular/cli` ou `npm` ### Angular CLI ```bash ng add @techinsper/insper-design-system ``` Pronto, já pode utilizar o IDS em seu projeto Angular! ### NPM ```bash npm install @techinsper/insper-design-system ``` Como não utilizamos a CLI, agora precisamos configurar o `angular.json` manualmente. Adicione o seguinte trecho ao `angular.json`: ```json "styles": [ "node_modules/@techinsper/insper-design-system/src/lib/styles/ids-theme.css" ], ``` ## Intelisense com Tailwind CSS Para ter suporte ao IntelliSense do Tailwind, adicione o seguinte trecho ao seu arquivo de styles global (geralmente `src/styles.css` ou `src/styles.scss`) ```css /* Corrija o path para a node_modules */ @import "node_modules/@techinsper/insper-design-system/src/lib/styles/theme/index.css"; @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); ``` ## Utilização Em seus componentes, importe os módulos do IDS que você deseja utilizar. O IDS é feito para ser utilizando tanto em componentes standalone quanto em módulos tradicionais do Angular. ### Standalone ```typescript import { ButtonComponent } from '@techinsper/insper-design-system'; @Component({ standalone: true, imports: [ButtonComponent], template: `Click me`, }) export class MyComponent {} ``` ### NgModule O módulo `InsperDesignSystemModule` importa todos os componentes do IDS: ```typescript import { InsperDesignSystemModule } from '@techinsper/insper-design-system'; @NgModule({ imports: [InsperDesignSystemModule], }) class AppModule {} ``` Caso queira importar apenas alguns componentes, importe os módulos específicos: ```typescript import { ButtonModule } from '@techinsper/insper-design-system'; @NgModule({ imports: [ButtonModule], }) class MyComponentModule {} ``` ## Varíaveis globais O IDS oferece algumas variáveis globais que podem ser utilizadas em seu projeto, principalmente varíveis de estilização. Todas as varíaveis CSS podem ser vistas em `node_modules/@techinsper/insper-design-system/src/lib/styles/tokens`. As varíaveis CSS possuem equivalentes em TS, que podem ser importadas de `@techinsper/insper-design-system`. ```typescript import { theme } from '@techinsper/insper-design-system'; const primaryColor = theme.colors.black[200]; ``` ## Developer Experience Buscando facilitar o desenvolvimento, o IDS oferece JSDoc e IntelliSense para todos os seus componentes. Então caso não saiba como utilizar um componente, provavelmente existe algum comentário JSDoc em suas propriedades ou até mesmo no componente. Recomendamos o uso do VSCode para melhor experiência, ou a instalação de plugins que ofereçam suporte a JSDoc. Caso não tenha familiriaridade com JSDoc, veja a [documentação oficial](https://jsdoc.app/), mas no geral, apenas passe o mouse sobre a propriedade ou componente que deseja saber mais. ### Types Sempre que necessário, o IDS exporta tipos para serem utilizados em seus componentes: ```typescript import { type BreadcrumbItem } from '@techinsper/insper-design-system'; ``` ================================================ FILE: projects/preview/src/stories/modal.stories.ts ================================================ import type { Meta, StoryObj } from '@storybook/angular'; import { ModalComponent } from '../../../insper/design-system/src/public-api'; import type { WithContent } from '../types/WithContent'; type StoryComponent = WithContent const meta: Meta = { title: 'Insper Design System/Components/Modal', component: ModalComponent, argTypes: { content: { control: 'text', }, title: { control: 'text', }, closeOnClickOutside: { control: 'boolean', }, visibility: { control: 'boolean', }, }, args: { content: 'This is a modal', title: 'Modal Title', closeOnClickOutside: false, visibility: true, }, render: (args) => ({ props: args, template: ` {{content}} `, }) }; export default meta; export const Default: StoryObj = {} ================================================ FILE: projects/preview/src/stories/stack.stories.ts ================================================ import { argsToTemplate, moduleMetadata, type Meta } from '@storybook/angular'; import { ButtonComponent, StackComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Elements/Stack', component: StackComponent, decorators: [ moduleMetadata({ imports: [ButtonComponent], }) ], args: { alignItems: 'start', direction: 'row', gap: 1, justifyContent: 'start', }, argTypes: { direction: { control: { type: 'select' }, options: ['row', 'column'], }, gap: { control: { type: 'select' }, options: [1, 2, 3, 4, 5, 6, 7, 8], }, justifyContent: { control: { type: 'select' }, options: ['start', 'center', 'end', 'between', 'around'], }, alignItems: { control: { type: 'select' }, options: ['start', 'center', 'end'], }, height: { control: { type: 'text' }, }, width: { control: { type: 'text' }, } }, render: (args) => ({ props: args, template: ` Button 1 Button 2 Button 3 `, }) } export default meta; export const Row = { args: { ...meta.args, } } export const Column = { args: { ...meta.args, direction: 'column', } } ================================================ FILE: projects/preview/src/stories/tab.stories.ts ================================================ import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { TabComponent, TabGroupComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Tabs', component: TabGroupComponent, decorators: [ moduleMetadata({ imports: [TabComponent] }) ], argTypes: { selectedTab: { control: { type: 'select' }, options: [0, 1, 2], }, }, render: (args) => ({ props: args, template: ` Content of Tab 1 Content of Tab 2 Content of Tab 3 `, }) } export default meta; export const Default: StoryObj = {} ================================================ FILE: projects/preview/src/stories/table.stories.ts ================================================ import { argsToTemplate, Meta, StoryObj } from "@storybook/angular"; import { IDSTableColumn, TableComponent } from "../../../insper/design-system/src/public-api"; const data = [ {name: 'John Doe', age: 30, id: 1}, {name: 'Jane Smith', age: 25, id: 2}, {name: 'Mike Johnson', age: 40, id: 3}, ] const columns: IDSTableColumn[] = [ { header: 'ID', accessorKey: 'id', enableSorting: true, }, { header: 'Name', accessorKey: 'name', }, { header: 'Age', accessorKey: 'age', }, ]; const meta: Meta> = { title: 'Insper Design System/Components/Table', component: TableComponent, argTypes: { enableSelection: { control: { type: 'boolean' }, }, enableSorting: { control: { type: 'boolean' }, }, }, args: { data, columns, enableSelection: false, enableSorting: false, }, render: (args) => ({ props: args, template: ``, }) } export default meta; export const Default: StoryObj> = {}; export const RowSelection: StoryObj> = { args: { ...meta.args, enableSelection: true, } }; export const Sorting: StoryObj> = { args: { ...meta.args, enableSorting: true, } } ================================================ FILE: projects/preview/src/stories/toast.stories.ts ================================================ import { Component, inject, input } from '@angular/core'; import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { ButtonComponent, ToastComponent, ToastOptions, ToastService } from '../../../insper/design-system/src/public-api'; @Component({ selector: 'ids-host', template: ` Show toast `, standalone: true, imports: [ToastComponent, ButtonComponent], }) class HostComponent { private toastService = inject(ToastService); variant = input('success'); duration = input(3000); showToast() { this.toastService.showToast({ title: 'Toast Title', message: 'This is a toast message.', variant: this.variant(), duration: this.duration(), }) } } const meta: Meta = { title: 'Insper Design System/Components/Toast', component: HostComponent, render: (args) => ({ props: args, template: ` `, }), argTypes: { variant: { control: { type: 'select' }, options: ['info', 'success', 'warning', 'error'], }, duration: { control: { type: 'number' }, description: 'Duração em milissegundos antes do toast desaparecer automaticamente.', }, } } export default meta; export const Default: StoryObj = {} ================================================ FILE: projects/preview/src/stories/toggle.stories.ts ================================================ import type { Meta, StoryObj } from '@storybook/angular'; import { ToggleComponent } from '../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Toggle', component: ToggleComponent, argTypes: { name: { control: 'text' }, size: { control: 'select', options: ['sm', 'md', 'lg'], }, toggled: { control: 'boolean', } }, args: { name: 'toggle-example', size: 'md', toggled: false, }, render: (args) => ({ props: args, template: ` ` }) } export default meta; export const Default: StoryObj = {} ================================================ FILE: projects/preview/src/stories/tooltip.stories.ts ================================================ import type { Meta, StoryObj } from '@storybook/angular'; import { TooltipComponent } from '../../../insper/design-system/src/public-api'; import type { WithContent } from '../types/WithContent'; type StoryComponent = WithContent; const meta: Meta = { title: 'Insper Design System/Components/Tooltip', component: TooltipComponent, argTypes: { tooltipText: { control: 'text' }, tooltipPosition: { control: {type: 'select'}, options: ['top', 'bottom', 'left', 'right'], }, }, args: { tooltipText: 'This is a tooltip', tooltipPosition: 'right', content: 'Hover over me', }, render: (args) => ({ props: args, template: `
{{ content }}
`, }) } export default meta; export const Default: StoryObj = {} ================================================ FILE: projects/preview/src/stories/accordion/accordion.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as AccordionStories from './accordion.stories'; # Accordion Deve ser utilizado como uma composição de ``, `` e `` ```html import { AccordionComponent, AccordionSummaryComponent, AccordionDetailsComponent } from '@techinsper/insper-design-system'; ``` ### Expanded By Default Utilize a propriedade `expanded` para definir se o accordion deve estar aberto por padrão. ### Custom Expand Icon Personalize o ícone de expansão utilizando a propriedade `expandIcon`. ### Icon Adicione um ícone ao resumo do accordion utilizando a propriedade `icon`. ### Playground Utilize a tabela abaixo para alterar as propriedades do accordion e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/accordion/accordion.stories.ts ================================================ import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { AccordionComponent, AccordionDetailsComponent, AccordionSummaryComponent } from '../../../../insper/design-system/src/public-api'; type StoryComponent = AccordionComponent & AccordionSummaryComponent & AccordionDetailsComponent; const meta: Meta = { title: 'Insper Design System/Components/Accordion', component: AccordionComponent, decorators: [ moduleMetadata({ imports: [ AccordionSummaryComponent, AccordionDetailsComponent, ], }), ], }; export default meta; export const Expanded_By_Default: StoryObj = { render: (args) => ({ props: args, template: ` Frameworks
  • React
  • Angular
  • Vue
`, }), }; export const Custom_Expand_Icon: StoryObj = { render: (args) => ({ props: args, template: `
Frameworks
  • React
  • Angular
  • Vue
Frameworks
  • React
  • Angular
  • Vue
`, }), }; export const Icon: StoryObj = { render: (args) => ({ props: args, template: `
Frameworks
  • React
  • Angular
  • Vue
Frameworks
  • React
  • Angular
  • Vue
` }), }; export const Playground: StoryObj = { argTypes: { expanded: { control: 'boolean', }, expandIcon: { control: 'text', }, icon: { control: 'text', }, }, args: { expanded: false, expandIcon: 'arrows-vertical', icon: 'house', }, render: (args) => ({ props: args, template: ` Frameworks
  • React
  • Angular
  • Vue
`, }), }; ================================================ FILE: projects/preview/src/stories/breadcrumb/breadcrumb.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as BreadcrumbStories from './breadcrumb.stories'; # Breadcrumb Deve ser utilizado com a tag `` e a propriedade `items`. Evite declarar o array de `items` diretamente no template, prefira declarar na classe do componente. Para isso, importe o type `BreadcrumbItem` do Design System. ```typescript import { BreadcrumbComponent, type BreadcrumbItem } from '@techinsper/insper-design-system'; @Component({ ... }) class MyComponent { items: BreadcrumbItem[] = [ { label: 'Home', path: '/' }, { label: 'Library', path: '/library' }, { label: 'Data', path: '/data' } ]; } // No arquivo HTML ``` ### Label with icon ### Application Path Utilize a propriedade `path` para navegar dentro da aplicação, no caso, no contexto do Angular Router. Caso seja necessário passar parâmetros, utilize a propriedade `queryParams`. ### External Path Utilize a propriedade `externalUrl` caso queira navegar para fora da aplicação. ### Playground Utilize a tabela abaixo para alterar as propriedades do breadcrumb e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/breadcrumb/breadcrumb.stories.ts ================================================ import { Component } from '@angular/core'; import { provideRouter, type Routes } from '@angular/router'; import { applicationConfig, type Meta, type StoryObj } from '@storybook/angular'; import { BreadcrumbComponent } from '../../../../insper/design-system/src/public-api'; @Component({ standalone: true, }) class DummyComponent {} const routes: Routes = [ { path: '', component: DummyComponent, }, { path: 'library', component: DummyComponent, }, { path: 'data', component: DummyComponent, }, ]; const meta: Meta = { title: 'Insper Design System/Components/Breadcrumb', component: BreadcrumbComponent, decorators: [ applicationConfig({ providers: [provideRouter(routes)], }) ], }; export default meta; export const Label_With_Icon: StoryObj = { args: { items: [ { iconName: 'house', label: 'Home', path: '/' }, { iconName: 'book', label: 'Library', path: '/library' }, { iconName: 'database', label: 'Data', path: '/data' } ] }, } export const Application_Path: StoryObj = { args: { items: [ { label: 'Home', path: '/' }, { label: 'Library', path: '/library' }, { label: 'Data', path: '/data', queryParams: { id: 123 } } ] }, }; export const External_Path: StoryObj = { args: { items: [ { label: 'Institucional', externalUrl: 'https://www.insper.edu.br/' }, { label: 'Google', externalUrl: 'https://www.google.com/' }, ] }, }; export const Playground: StoryObj = { args: { items: [ { label: 'Home', path: '/' }, { label: 'Library', path: '/library' }, { label: 'Data', path: '/data' } ] }, argTypes: { items: { control: 'object' } }, }; ================================================ FILE: projects/preview/src/stories/button/button.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as ButtonStories from './button.stories'; {/* */} # Button Deve ser utilizado com a diretiva `idsButton` para aplicar os estilos do componente. ```html import { ButtonComponent } from '@techinsper/insper-design-system'; ``` ### Variants ### Colors ### Shapes ### Inverted ### Sizes ### Leading e Trailing Icons Os ícones podem ser utilizados como leading (início) ou trailing (fim). Para utilizar um botão com apenas ícones, ver a seção: [Icon Buttons](#icon-buttons) ### Icon Buttons ### Fullwidth O botão pode ser utilizado com a propriedade `fullwidth` para ocupar toda a largura do elemento pai. ### Playground Utilize a tabela abaixo para alterar as propriedades do botão e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/button/button.stories.ts ================================================ import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { ButtonComponent, IconComponent, StackComponent } from '../../../../insper/design-system/src/public-api'; import type { WithContent } from '../../types/WithContent'; type StoryComponent = ButtonComponent & { disabled: boolean }; const meta: Meta = { title: 'Insper Design System/Components/Button', component: ButtonComponent, decorators: [ moduleMetadata({ imports: [IconComponent, StackComponent] }) ], // argTypes: { // disabled: { // control: { type: 'boolean' }, // }, // loading: { // control: { type: 'boolean' }, // }, // variant: { // control: { type: 'select' }, // options: ['fill', 'tonal', 'outline', 'text'], // }, // color: { // control: { type: 'select' }, // options: ['neutral', 'danger'], // }, // fullwidth: { // control: { type: 'boolean' }, // }, // leadingIcon: { // control: { type: 'text' }, // }, // icon: { // control: { type: 'text' }, // }, // trailingIcon: { // control: { type: 'text' }, // }, // inverted: { // control: { type: 'boolean' }, // }, // shape: { // control: { type: 'select' }, // options: ['rectangle', 'circle', 'square'], // }, // size: { // control: { type: 'select' }, // options: ['sm', 'md', 'lg', 'xl'], // }, // }, // args: { // disabled: false, // loading: false, // }, } export default meta; export const Variants: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Colors: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Shapes: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Inverted: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Sizes: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Leading_And_Trailing_Icons: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Icon_Buttons: StoryObj = { render: (args) => ({ props: args, template: `
` }) } export const Fullwidth: StoryObj = { render: (args) => ({ props: args, template: ` ` }) } export const Playground: StoryObj> = { argTypes: { content: { control: { type: 'text' }, description: 'O texto a ser renderizado', }, loading: { control: { type: 'boolean' }, description: 'Altera o estado de carregamento do botão', }, variant: { control: { type: 'select' }, options: ['fill', 'tonal', 'outline', 'ghost'], description: 'Variante visual do botão', }, color: { control: { type: 'select' }, options: ['neutral', 'danger'], description: 'Cor do botão', }, fullwidth: { control: { type: 'boolean' }, description: 'Define se o botão ocupará toda a largura disponível', }, leadingIcon: { control: { type: 'text' }, description: 'Ícone exibido antes do texto', }, icon: { control: { type: 'text' }, description: 'Ícone para botões que só contêm ícone', }, trailingIcon: { control: { type: 'text' }, description: 'Ícone exibido após o texto', }, inverted: { control: { type: 'boolean' }, description: 'Inverte as cores para uso em fundos escuros', }, shape: { control: { type: 'select' }, options: ['rectangle', 'circle', 'square'], description: 'Formato do botão', }, size: { control: { type: 'select' }, options: ['sm', 'md', 'lg', 'xl'], description: 'Tamanho do botão', }, }, render: (args) => ({ props: args, template: ` ` }) } ================================================ FILE: projects/preview/src/stories/chip/chip.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as ChipStories from './chip.stories'; # Chip Deve ser utilizado com a tag `` ```html import { ChipComponent } from '@techinsper/insper-design-system'; ``` ### Label ### Deletable ### Icon ### Selectable ### Disabled ### Playground Utilize a tabela abaixo para alterar as propriedades do chip e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/chip/chip.stories.ts ================================================ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { ChipComponent } from '../../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Chip', component: ChipComponent, } export default meta; export const Label: StoryObj = { render: () => ({ template: ``, }), } export const Deletable: StoryObj = { render: () => ({ template: `
`, }), } export const Icon: StoryObj = { render: () => ({ template: `
`, }), } export const Selectable: StoryObj = { render: () => ({ props: { selected: true, onSelect() { this['selected'] = !this['selected']; } }, template: ` `, }), } export const Disabled: StoryObj = { render: () => ({ template: `
`, }), } export const Playground: StoryObj = { argTypes: { label: { control: { type: 'text' }, }, deletable: { control: { type: 'boolean' }, }, deleteIcon: { control: { type: 'text' }, description: 'Icon to be used when the chip is deletable', }, icon: { control: { type: 'text' }, }, selected: { control: { type: 'boolean' }, }, disabled: { control: { type: 'boolean' }, }, }, args: { label: 'Chip', deletable: false, deleteIcon: 'x-circle', selected: false, icon: 'check', disabled: false, }, render: (args) => ({ props: args, template: ` `, }), } ================================================ FILE: projects/preview/src/stories/divider/divider.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as DividerStories from './divider.stories'; # Divider Deve ser utilizado com a tag `` ```html import { DividerComponent } from '@techinsper/insper-design-system'; ``` ### Horizontal ### Vertical ### Playground Utilize a tabela abaixo para alterar as propriedades do chip e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/divider/divider.stories.ts ================================================ import { NgIf } from '@angular/common'; import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { DividerComponent } from '../../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Divider', component: DividerComponent, decorators: [ moduleMetadata({ imports: [NgIf] }) ], }; export default meta; export const Horizontal: StoryObj = { render: () => ({ template: `
Content 1
Content 2
`, }), }; export const Vertical: StoryObj = { render: () => ({ template: `
Content 1
Content 2
`, }), }; export const Playground: StoryObj = { argTypes: { vertical: { control: 'boolean', defaultValue: false }, }, render: (args) => { const containerStyle = args['vertical'] ? `display: flex; flex-direction: row;` : ''; return { props: args, template: `
Content 1
Content 2
`, }; }, }; ================================================ FILE: projects/preview/src/stories/icon/icon.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as IconStories from './icon.stories'; # Icon Deve ser utilizado com a diretiva `idsIcon` sobre uma tag `` ou ``. ```html import { IconComponent } from '@techinsper/insper-design-system'; ``` ### Icon Name ### Weight ### Sizes ### Playground Utilize a tabela abaixo para alterar as propriedades do icon e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/icon/icon.stories.ts ================================================ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { IconComponent } from '../../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Elements/Icon', component: IconComponent, } export default meta; export const Icon_Name: StoryObj = { render: () => ({ template: `
`, }), } export const Weight: StoryObj = { render: () => ({ template: `
`, }), } export const Sizes: StoryObj = { render: () => ({ template: `
`, }), } export const Playground: StoryObj = { argTypes: { iconName: { control: 'text', }, size: { control: { type: 'select', }, options: ['16', '20', '24', '32'], }, weight: { control: { type: 'select', }, options: ['regular', 'fill'], } }, args: { iconName: 'house', size: '20', weight: 'regular', }, render: (args) => ({ props: args, template: ``, }), }; ================================================ FILE: projects/preview/src/stories/search/search.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as SearchStories from './search.stories'; # Search Deve ser utilizado com a tag `` ```html import { SearchComponent } from '@techinsper/insper-design-system'; ``` ### NgModel Caso utilize o campo de busca com `ngModel`, lembre-se de importar o `FormsModule` no módulo onde o componente é utilizado. ### Two Way Binding Caso queira utilizar o campo de busca com Two Way Binding, utilize com uma varíavel qualquer ou um `signal`. ### Disabled ### Playground Utilize a tabela abaixo para alterar as propriedades do componente e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/search/search.stories.ts ================================================ import { FormsModule } from '@angular/forms'; import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { SearchComponent } from '../../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Search', component: SearchComponent, decorators: [ moduleMetadata({ imports: [FormsModule], }) ], } export default meta; export const NgModel: StoryObj = { args: { value: '', }, render: (args) => ({ props: args, template: `

NgModel: {{ value }}

`, }) }; export const Two_Way_Binding: StoryObj = { args: { value: '', }, render: (args) => ({ props: args, template: `

Model: {{ value }}

`, }) }; export const Disabled: StoryObj = { render: () => ({ template: `
`, }) }; export const Playground: StoryObj = { argTypes: { placeholder: { control: 'text', }, disabled: { control: 'boolean', }, value: { control: 'text', }, required: { control: 'boolean', }, }, args: { placeholder: 'Search...', value: '', disabled: false, required: false, }, }; ================================================ FILE: projects/preview/src/stories/typography/typography.mdx ================================================ import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks'; import * as TypographyStories from './typography.stories'; # Typography Deve ser utilizado com a diretiva `idsTypography` para aplicar os estilos do componente. Qualquer elemento HTML pode ser utilizado para aplicar a tipografia, como `

`, ``, `

`, `

`, etc. ```html import { TypographyComponent } from '@techinsper/insper-design-system';

Typography

``` ### Variants ### Ultra ### Display ### Headline ### Title ### Lead ### Body ### Caption ### Support ### Modifiers O IDS utiliza três modificadores de tipografia: `--str` (strong), `--exp` (expressive) e `--upper` (uppercase). Nem todas as variantes possuem suporte para esses modificadores. ### Playground Utilize a tabela abaixo para alterar as propriedades e ver o resultado em tempo real. ================================================ FILE: projects/preview/src/stories/typography/typography.stories.ts ================================================ import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; import { IDS_TYPOGRAPHY_VARIANTS, TypographyComponent } from '../../../../insper/design-system/src/public-api'; const meta: Meta = { title: 'Insper Design System/Components/Typography', component: TypographyComponent, } export default meta; export const Variants: StoryObj = { render: () => ({ template: `

Ultra

Display

Headline

Title

Lead

Body

Caption

Support

`, }) }; export const Sizes: StoryObj = { render: () => ({ template: `
xs sm md lg xl 2xl
xs sm md lg xl 2xl
xs sm md lg xl 2xl
xs sm md lg xl 2xl
`, }), } export const Underline: StoryObj = { render: () => ({ template: `

Underline

Underline

Underline

Underline

Underline

Underline

Underline

Underline

`, }) }; export const Ultra: StoryObj = { render: () => ({ template: `

Ultra

Ultra Str

Ultra Exp

`, }) }; export const Display: StoryObj = { render: () => ({ template: `

Display

Display Str

Display Exp

`, }) }; export const Headline: StoryObj = { render: () => ({ template: `

Headline

Headline Str

`, }) }; export const Title: StoryObj = { render: () => ({ template: `

Title

Title Str

`, }) }; export const Lead: StoryObj = { render: () => ({ template: `

Lead

Lead Str

`, }) }; export const Body: StoryObj = { render: () => ({ template: `

Body

Body Str

`, }) }; export const Caption: StoryObj = { render: () => ({ template: `

Caption

Caption Str

`, }) }; export const Support: StoryObj = { render: () => ({ template: `

Support

Support Upper

`, }) }; export const Modifiers: StoryObj = { render: () => ({ template: `
Ultra Expressive Display Str Support Upper
`, }), } export const Playground: StoryObj = { argTypes: { idsTypography: { control: { type: 'select' }, options: IDS_TYPOGRAPHY_VARIANTS }, underline: { control: { type: 'boolean' }, } }, render: (args) => ({ props: args, template: `

Typography

`, }), }; ================================================ FILE: projects/preview/src/types/WithContent.ts ================================================ export type WithContent = T & { content?: string } ================================================ FILE: projects/preview/.storybook/main.ts ================================================ import type { StorybookConfig } from '@storybook/angular'; import { dirname, join } from "path"; /** * This function is used to resolve the absolute path of a package. * It is needed in projects that use Yarn PnP or are set up within a monorepo. */ function getAbsolutePath(value: string): any { return dirname(require.resolve(join(value, 'package.json'))) } const config: StorybookConfig = { "stories": [ "../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" ], "addons": [ getAbsolutePath('@storybook/addon-essentials'), getAbsolutePath('@storybook/addon-onboarding'), getAbsolutePath('@storybook/addon-interactions') ], "framework": { "name": getAbsolutePath('@storybook/angular'), "options": {} }, webpackFinal: (config) => { config.module?.rules?.push({ test: /\.css$/, exclude: /node_modules/, use: [ { loader: 'postcss-loader', options: { postcssOptions: { plugins: ['@tailwindcss/postcss'], }, }, }, ], }); return config; }, previewHead: (head) => { return ` ${head} `; } }; export default config; ================================================ FILE: projects/preview/.storybook/preview.ts ================================================ import { setCompodocJson } from "@storybook/addon-docs/angular"; import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; import type { Preview } from '@storybook/angular'; import docJson from "../documentation.json"; setCompodocJson(docJson); const preview: Preview = { globalTypes: { theme: { name: 'Theme', description: 'Toggle theme', defaultValue: 'dark', toolbar: { icon: 'moon', items: [ { value: 'light', title: 'Light Theme', icon: 'sun' }, { value: 'dark', title: 'Dark Theme', icon: 'moon' }, ], showName: true, dynamicTitle: true, }, }, }, parameters: { viewport: { viewports: { ...MINIMAL_VIEWPORTS, 'HD': { name: 'HD', styles: { width: '1280px', height: '720px', }, }, 'HDReady': { name: 'HD Ready', styles: { width: '1366px', height: '768px', }, }, 'FullHD': { name: 'Full HD', styles: { width: '1920px', height: '1080px', }, }, '4K': { name: '4K', styles: { width: '3840px', height: '2160px', }, }, } }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, }, decorators: [ (storyFn, context) => { const story = storyFn(); return { ...story, moduleMetadata: { ...story.moduleMetadata, providers: [ ...(story.moduleMetadata?.providers || []), { provide: '_tmp_', useValue: story.props } ] } }; }, (storyFn, context) => { const theme = context.globals['theme']; document.body.setAttribute('data-theme', theme); document.body.style.setProperty('background-color', theme === 'dark' ? 'var(--gray-950)' : 'var(--gray-200)'); return storyFn(); } ], tags: ['autodocs'], }; export default preview; ================================================ FILE: projects/preview/.storybook/tsconfig.doc.json ================================================ // This tsconfig is used by Compodoc to generate the documentation for the project. // If Compodoc is not used, this file can be deleted. { "extends": "./tsconfig.json", // Please make sure to include all files from which Compodoc should generate documentation. "include": ["../../insper/design-system/src/**/*.ts"], // Exclude all files that are not needed for documentation generation. "exclude": [ "../src/app/**/*" ], "files": ["./typings.d.ts"] } ================================================ FILE: projects/preview/.storybook/tsconfig.json ================================================ { "extends": "../tsconfig.app.json", "compilerOptions": { "types": ["node"], "allowSyntheticDefaultImports": true, "resolveJsonModule": true }, "exclude": ["../src/test.ts", "../src/**/*.spec.ts"], "include": ["../src/**/*.stories.*", "./preview.ts"], "files": ["./typings.d.ts"] } ================================================ FILE: projects/preview/.storybook/typings.d.ts ================================================ declare module '*.md' { const content: string; export default content; } ================================================ FILE: .github/workflows/publisher.yml ================================================ name: Publish to GitHub Packages on: release: types: [created] jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout código uses: actions/checkout@v4 - name: Configurar Node.js uses: actions/setup-node@v4 with: node-version: 20 registry-url: https://npm.pkg.github.com/ scope: "@${{ github.repository_owner }}" - name: Habilitar Corepack (Yarn 4+) run: corepack enable - name: Instalar dependências run: yarn install --immutable - name: Build do projeto run: yarn build - name: Publicar no GitHub Packages run: | cd dist/insper/design-system npm publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .husky/pre-commit ================================================ npm run test:ci npm run lint --fix ================================================ FILE: .husky/pre-push ================================================ npm run build npm run build:storybook