diff --git a/README.md b/README.md index 8fea2d6..fcb56be 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Open in your browser: `http://localhost:4210/` | icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:plugin:icons | http://localhost:4303/assets/manifest.json | | lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | npm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json | | create-palette-plugin | Creates a board with all the palette colors | 4305 | npm run start:plugin:palette | http://localhost:4305/assets/manifest.json | -| import-table | -- | 4306 | -- | -- | +| table-plugin | Create or import table | 4306 | npm run start:table-plugin | http://localhost:4306/assets/manifest.json | | -- | ## Web Apps diff --git a/apps/table-plugin/eslint.config.js b/apps/table-plugin/eslint.config.js new file mode 100644 index 0000000..29c2176 --- /dev/null +++ b/apps/table-plugin/eslint.config.js @@ -0,0 +1,43 @@ +import baseConfig from '../../eslint.config.js'; +import { compat } from '../../eslint.base.config.js'; + +export default [ + ...baseConfig, + ...compat + .config({ + extends: [ + 'plugin:@nx/angular', + 'plugin:@angular-eslint/template/process-inline-templates', + ], + }) + .map((config) => ({ + ...config, + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'app', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'app', + style: 'kebab-case', + }, + ], + }, + })), + ...compat + .config({ extends: ['plugin:@nx/angular-template'] }) + .map((config) => ({ + ...config, + files: ['**/*.html'], + rules: {}, + })), + { ignores: ['**/assets/*.js'] }, +]; diff --git a/apps/table-plugin/project.json b/apps/table-plugin/project.json new file mode 100644 index 0000000..0b44595 --- /dev/null +++ b/apps/table-plugin/project.json @@ -0,0 +1,89 @@ +{ + "name": "table-plugin", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "prefix": "app", + "sourceRoot": "apps/table-plugin/src", + "tags": [], + "targets": { + "build": { + "executor": "@angular-devkit/build-angular:application", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/apps/table-plugin", + "index": "apps/table-plugin/src/index.html", + "browser": "apps/table-plugin/src/main.ts", + "polyfills": ["zone.js"], + "tsConfig": "apps/table-plugin/tsconfig.app.json", + "assets": [ + "apps/table-plugin/src/favicon.ico", + "apps/table-plugin/src/assets" + ], + "styles": [ + "libs/plugins-styles/src/lib/styles.css", + "apps/table-plugin/src/styles.css" + ], + "optimization": { + "scripts": true, + "styles": true, + "fonts": false + }, + "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": { + "executor": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "table-plugin:build:production" + }, + "development": { + "buildTarget": "table-plugin:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "executor": "@angular-devkit/build-angular:extract-i18n", + "options": { + "buildTarget": "table-plugin:build" + } + }, + "buildPlugin": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "minify": true, + "outputPath": "apps/table-plugin/src/assets/", + "main": "apps/table-plugin/src/plugin.ts", + "tsConfig": "apps/table-plugin/tsconfig.plugin.json", + "generatePackageJson": false, + "format": ["esm"], + "deleteOutputPath": false + } + } + } +} diff --git a/apps/table-plugin/src/app/app.component.css b/apps/table-plugin/src/app/app.component.css new file mode 100644 index 0000000..cd44abd --- /dev/null +++ b/apps/table-plugin/src/app/app.component.css @@ -0,0 +1,123 @@ +.text { + margin-block-start: var(--spacing-24); +} + +.input-container { + background-color: var(--db-tertiary); + border-radius: var(--spacing-8); + color: #8a9ca2; + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-s); + margin-block-start: var(--spacing-8); + padding: var(--spacing-8) var(--spacing-24) var(--spacing-8) var(--spacing-24); + text-transform: uppercase; + &:hover { + cursor: pointer; + } +} +.inputfile { + block-size: 1px; + border: 0; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + inline-size: 1px; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + white-space: nowrap; +} + +.inputfile + label { + display: block; + text-align: center; + + &:hover { + cursor: pointer; + } +} + +hr { + border-block-end: 2px solid var(--db-quaternary); + margin-block: var(--spacing-24); +} + +.table-grid { + background-color: var(--db-tertiary); + display: grid; + grid-template-columns: repeat(8, auto); + grid-template-rows: repeat(6, auto); + border-radius: var(--spacing-8); + padding: 10px; + margin-block: var(--spacing-16) var(--spacing-20); +} + +.cell { + align-items: center; + block-size: 26px; + display: flex; + justify-content: center; +} + +.square { + block-size: 22px; + border: 1px solid var(--df-secondary); + border-radius: var(--spacing-4); + display: block; + inline-size: 22px; + + &.active { + background-color: var(--da-primary); + } + + &:hover { + background-color: var(--da-tertiary); + cursor: pointer; + } +} + +.checkbox-container { + margin-block-end: var(--spacing-12); +} + +.new-table { + align-items: center; + block-size: 22px; + display: flex; + justify-content: space-between; + + & .text { + margin-block-start: 0; + } +} + +.tag { + border: 1px solid var(--da-primary); + border-radius: var(--spacing-4); + color: var(--da-primary); + font-size: var(--font-size-s); + font-weight: var(--font-weight-bold); + line-height: var(--font-line-height-s); + padding: var(--spacing-4); +} + +.error { + align-items: center; + background-color: var(--error-950); + border: 1px solid var(--error-700); + border-radius: var(--spacing-8); + display: flex; + margin-block-start: var(--spacing-8); + padding: var(--spacing-8); + + & .close-icon:hover { + cursor: pointer; + } + + & .message { + color: var(--lb-primary); + margin-inline-start: var(--spacing-8); + } +} diff --git a/apps/table-plugin/src/app/app.component.html b/apps/table-plugin/src/app/app.component.html new file mode 100644 index 0000000..55950be --- /dev/null +++ b/apps/table-plugin/src/app/app.component.html @@ -0,0 +1,91 @@ +
+

Import a data file (CSV)

+
+ close error + Something was wrong.
+ Make sure the formst is .csv
+
+
+ + +
+
+
+

Or create a new table

+ {{ selectedRow }} rows x {{ selectedColumn }} cols +
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/apps/table-plugin/src/app/app.component.ts b/apps/table-plugin/src/app/app.component.ts new file mode 100644 index 0000000..a42dfce --- /dev/null +++ b/apps/table-plugin/src/app/app.component.ts @@ -0,0 +1,123 @@ +import { Component, inject } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { CommonModule } from '@angular/common'; +import type { Cell, PluginMessageEvent, TableOptions } from '../app/model'; +import { filter, fromEvent, map, merge, take } from 'rxjs'; +import { FormsModule } from '@angular/forms'; + +@Component({ + standalone: true, + imports: [RouterModule, CommonModule, FormsModule], + selector: 'app-root', + templateUrl: './app.component.html', + styleUrl: './app.component.css', + host: { + '[attr.data-theme]': 'theme()', + }, +}) +export class AppComponent { + public table: string[][] = []; + public cells = [...Array(48).keys()]; + public selectedRow = 0; + public selectedColumn = 0; + public selectedCell: Cell | undefined; + public tableOptions: TableOptions = { + filledHeaderRow: true, + filledHeaderColumn: false, + borders: true, + alternateRows: true, + }; + public fileError = false; + + route = inject(ActivatedRoute); + messages$ = fromEvent>(window, 'message'); + + initialTheme$ = this.route.queryParamMap.pipe( + map((params) => params.get('theme')), + filter((theme) => !!theme), + take(1) + ); + + theme = toSignal( + merge( + this.initialTheme$, + this.messages$.pipe( + filter((event) => event.data.type === 'theme'), + map((event) => { + return event.data.content; + }) + ) + ) + ); + + onSelectFile(event: Event) { + const target = event.target as HTMLInputElement; + if ( + target.files && + target.files[0] && + target.files[0].type === 'text/csv' + ) { + var reader = new FileReader(); + reader.readAsText(target.files[0]); + reader.onload = (e) => { + this.table = (e?.target?.result as string) + ?.split(/\r?\n|\r|\n/g) + .map((it) => it.trim()) + .filter((it) => it !== '') + .map((it) => it.split(',')); + + this.sendMessage({ + content: { + import: this.table, + type: 'import', + options: this.tableOptions, + }, + type: 'table', + }); + }; + } else { + this.fileError = true; + } + } + + createTable(cell: number) { + const data = this.getCellColRow(cell); + this.sendMessage({ + content: { + new: { column: data.column, row: data.row }, + type: 'new', + options: this.tableOptions, + }, + type: 'table', + }); + } + + setColRow(cell: number) { + this.clearError(); + this.selectedCell = this.getCellColRow(cell); + this.selectedColumn = this.selectedCell.column; + this.selectedRow = this.selectedCell.row; + } + + clearColRow() { + this.selectedCell = undefined; + this.selectedColumn = 0; + this.selectedRow = 0; + } + + clearError() { + this.fileError = false; + } + + getCellColRow(cell: number) { + return { + column: (cell % 8) + 1, + row: Math.floor(cell / 8) + 1, + }; + } + + private sendMessage(message: PluginMessageEvent): void { + parent.postMessage(message, '*'); + } +} diff --git a/apps/table-plugin/src/app/app.config.ts b/apps/table-plugin/src/app/app.config.ts new file mode 100644 index 0000000..ed40494 --- /dev/null +++ b/apps/table-plugin/src/app/app.config.ts @@ -0,0 +1,7 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { appRoutes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(appRoutes)], +}; diff --git a/apps/table-plugin/src/app/app.routes.ts b/apps/table-plugin/src/app/app.routes.ts new file mode 100644 index 0000000..8762dfe --- /dev/null +++ b/apps/table-plugin/src/app/app.routes.ts @@ -0,0 +1,3 @@ +import { Route } from '@angular/router'; + +export const appRoutes: Route[] = []; diff --git a/apps/table-plugin/src/app/model.ts b/apps/table-plugin/src/app/model.ts new file mode 100644 index 0000000..70efa6b --- /dev/null +++ b/apps/table-plugin/src/app/model.ts @@ -0,0 +1,35 @@ +export interface InitPluginEvent { + type: 'init'; + content: { + theme: string; + }; +} + +export interface TablePluginEvent { + type: 'table'; + content: { + import?: string[][]; + new?: Cell; + type: 'new' | 'import'; + options: TableOptions; + }; +} + +export interface ThemePluginEvent { + type: 'theme'; + content: string; +} + +export type PluginMessageEvent = + | InitPluginEvent + | TablePluginEvent + | ThemePluginEvent; + +export type Cell = { column: number; row: number }; + +export type TableOptions = { + filledHeaderRow: boolean; + filledHeaderColumn: boolean; + borders: boolean; + alternateRows: boolean; +}; diff --git a/apps/table-plugin/src/app/nx-welcome.component.ts b/apps/table-plugin/src/app/nx-welcome.component.ts new file mode 100644 index 0000000..d473ea1 --- /dev/null +++ b/apps/table-plugin/src/app/nx-welcome.component.ts @@ -0,0 +1,907 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-nx-welcome', + standalone: true, + imports: [CommonModule], + template: ` + + +
+
+ +
+

+ Hello there, + Welcome table-plugin 👋 +

+
+ +
+
+

+ + + + You're up and running +

+ What's next? +
+
+ + + +
+
+ + + +
+

Next steps

+

Here are some things you can do with Nx:

+
+ + + + + Add UI library + +
# Generate UI lib
+nx g @nx/angular:lib ui
+# Add a component
+nx g @nx/angular:component ui/src/lib/button
+
+
+ + + + + View project details + +
nx show project table-plugin --web
+
+
+ + + + + View interactive project graph + +
nx graph
+
+
+ + + + + Run affected commands + +
# see what's been affected by changes
+nx affected:graph
+# run tests for current changes
+nx affected:test
+# run e2e tests for current changes
+nx affected:e2e
+
+
+

+ Carefully crafted with + + + +

+
+
+ `, + styles: [], + encapsulation: ViewEncapsulation.None, +}) +export class NxWelcomeComponent {} diff --git a/apps/table-plugin/src/assets/.gitkeep b/apps/table-plugin/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/table-plugin/src/assets/close.svg b/apps/table-plugin/src/assets/close.svg new file mode 100644 index 0000000..b9f8c89 --- /dev/null +++ b/apps/table-plugin/src/assets/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/avatar_plugin_table.png b/apps/table-plugin/src/assets/icon.png similarity index 100% rename from apps/avatar_plugin_table.png rename to apps/table-plugin/src/assets/icon.png diff --git a/apps/table-plugin/src/assets/manifest.json b/apps/table-plugin/src/assets/manifest.json new file mode 100644 index 0000000..c603dac --- /dev/null +++ b/apps/table-plugin/src/assets/manifest.json @@ -0,0 +1,8 @@ +{ + "name": "Table plugin", + "host": "http://localhost:4306", + "description": "Table plugin to import or create tables", + "code": "/assets/plugin.js", + "icon": "/assets/icon.png", + "permissions": ["page:read", "file:read", "selection:read"] +} diff --git a/apps/table-plugin/src/favicon.ico b/apps/table-plugin/src/favicon.ico new file mode 100644 index 0000000..317ebcb Binary files /dev/null and b/apps/table-plugin/src/favicon.ico differ diff --git a/apps/table-plugin/src/index.html b/apps/table-plugin/src/index.html new file mode 100644 index 0000000..49bb534 --- /dev/null +++ b/apps/table-plugin/src/index.html @@ -0,0 +1,13 @@ + + + + + table-plugin + + + + + + + + diff --git a/apps/table-plugin/src/main.ts b/apps/table-plugin/src/main.ts new file mode 100644 index 0000000..514c89a --- /dev/null +++ b/apps/table-plugin/src/main.ts @@ -0,0 +1,7 @@ +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) +); diff --git a/apps/table-plugin/src/plugin.ts b/apps/table-plugin/src/plugin.ts new file mode 100644 index 0000000..a27b397 --- /dev/null +++ b/apps/table-plugin/src/plugin.ts @@ -0,0 +1,120 @@ +import { PluginMessageEvent } from './app/model'; + +penpot.ui.open('Plugin table', `?theme=${penpot.getTheme()}`, { + width: 235, + height: 510, +}); + +penpot.on('themechange', (theme) => { + penpot.ui.sendMessage({ type: 'theme', content: theme }); +}); + +penpot.ui.onMessage((message) => { + if (message.type === 'table') { + let numRows = 0; + let numCols = 0; + if (message.content.type === 'import' && message.content.import) { + numRows = message.content.import.length; + numCols = message.content.import[0].length; + } else if (message.content.new) { + numRows = message.content.new.row; + numCols = message.content.new.column; + } + + const frame = penpot.createFrame(); + frame.name = 'Table'; + + const viewport = penpot.viewport; + frame.x = viewport.center.x - 150; + frame.y = viewport.center.y - 200; + frame.resize(numCols * 160, numRows * 50); + frame.borderRadius = 8; + + // create grid + const grid = frame.addGridLayout(); + + for (let i = 0; i < numRows; i++) { + grid.addRow('auto'); + } + + for (let i = 0; i < numCols; i++) { + grid.addColumn('auto'); + } + + grid.alignItems = 'center'; + grid.justifyItems = 'start'; + grid.justifyContent = 'stretch'; + grid.alignContent = 'stretch'; + + // create text + for (let row = 0; row < numRows; row++) { + for (let col = 0; col < numCols; col++) { + const board = penpot.createFrame(); + + if (col === 0 && row === 0) { + board.borderRadiusTopLeft = 8; + } else if (col === 0 && row === numRows - 1) { + board.borderRadiusBottomRight = 8; + } else if (col === numCols - 1 && row === 0) { + board.borderRadiusTopRight = 8; + } else if (col === numCols - 1 && row === numRows - 1) { + board.borderRadiusBottomRight = 8; + } + + grid.appendChild(board, row + 1, col + 1); + + if (board.layoutChild) { + board.layoutChild.horizontalSizing = 'fill'; + board.layoutChild.verticalSizing = 'fill'; + } + + if (message.content.options.alternateRows && !(row % 2)) { + board.fills = [{ fillColor: '#f8f9fc' }]; + } + + if ( + (message.content.options.filledHeaderRow && row === 0) || + (message.content.options.filledHeaderColumn && col === 0) + ) { + board.fills = [{ fillColor: '#d9dfea' }]; + } + + if (message.content.options.borders) { + board.strokes = [ + { + strokeColor: '#d4dadc', + strokeStyle: 'solid', + strokeWidth: 0.5, + strokeAlignment: 'center', + }, + ]; + } + + const flex = board.addFlexLayout(); + flex.alignItems = 'center'; + flex.justifyContent = 'start'; + flex.verticalPadding = 10; + flex.horizontalPadding = 20; + + let text; + if (message.content.type === 'import' && message.content.import) { + text = penpot.createText(message.content.import[row][col]); + } else if (message.content.new) { + text = + row === 0 ? penpot.createText('Header') : penpot.createText('Cell'); + } + + if (text) { + text.growType = 'auto-width'; + text.fontFamily = 'Work Sans'; + text.fontId = 'gfont-work-sans'; + text.fontVariantId = row === 0 ? '500' : 'regular'; + text.fontSize = '12'; + text.fontWeight = row === 0 ? '500' : '400'; + board.appendChild(text); + } + } + } + penpot.closePlugin(); + } +}); diff --git a/apps/table-plugin/src/styles.css b/apps/table-plugin/src/styles.css new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/apps/table-plugin/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/table-plugin/tsconfig.app.json b/apps/table-plugin/tsconfig.app.json new file mode 100644 index 0000000..fff4a41 --- /dev/null +++ b/apps/table-plugin/tsconfig.app.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [] + }, + "files": ["src/main.ts"], + "include": ["src/**/*.d.ts"], + "exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"] +} diff --git a/apps/table-plugin/tsconfig.editor.json b/apps/table-plugin/tsconfig.editor.json new file mode 100644 index 0000000..4ee6393 --- /dev/null +++ b/apps/table-plugin/tsconfig.editor.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "compilerOptions": { + "types": [] + } +} diff --git a/apps/table-plugin/tsconfig.json b/apps/table-plugin/tsconfig.json new file mode 100644 index 0000000..4c48587 --- /dev/null +++ b/apps/table-plugin/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es2022", + "useDefineForClassFields": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.editor.json" + }, + { + "path": "./tsconfig.plugin.json" + } + ], + "extends": "../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/apps/table-plugin/tsconfig.plugin.json b/apps/table-plugin/tsconfig.plugin.json new file mode 100644 index 0000000..961987f --- /dev/null +++ b/apps/table-plugin/tsconfig.plugin.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "files": ["src/plugin.ts"], + "include": ["../../libs/plugin-types/index.d.ts"] +} diff --git a/package.json b/package.json index bf72043..4abe7ca 100644 --- a/package.json +++ b/package.json @@ -5,17 +5,15 @@ "license": "MIT", "scripts": { "start": "npm run start:app:runtime", - "start:app:runtime": "concurrently --kill-others --names build,server \"npx nx run plugins-runtime:build --watch --mode development\" \"npx nx run plugins-runtime:preview\"", "start:app:styles-example": "npx nx run example-styles:serve --host 0.0.0.0 --port 4201", - "start:plugin:all": "concurrently --kill-others \"npm:start:plugin:*(!all)\"", "start:plugin:poc-state": "npx nx run-many --targets=buildPlugin,serve --projects=poc-state-plugin --watch --host 0.0.0.0 --port 4301", "start:contrast-plugin": "npx nx run-many --targets=buildPlugin,serve --projects=contrast-plugin --watch --host 0.0.0.0 --port 4302", "start:plugin:icons": "npx nx run-many --targets=buildPlugin,serve --projects=icons-plugin --watch --host 0.0.0.0 --port 4303", "start:plugin:loremipsum": "npx nx run-many --targets=buildPlugin,serve --projects=lorem-ipsum-plugin --watch --port 4304", "start:plugin:palette": "npx nx run create-palette-plugin:build --watch & npx nx run create-palette-plugin:preview", - + "start:plugin:table": "npx nx run-many --targets=buildPlugin,serve --projects=table-plugin --watch --port 4306", "build": "npx nx build plugins-runtime --emptyOutDir=true", "lint": "nx run-many --all --target=lint --parallel", "lint:affected": "npx nx affected --target=lint",