diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 5fec67e..b09f96f 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -45,8 +45,13 @@ jobs: key: ${{ runner.os }}-node-v${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} restore-keys: | ${{ runner.os }}-node-v${{ matrix.node-version }}- + - name: Update yarn + run: | + corepack enable + yarn config set -H enableImmutableInstalls false + yarn install - name: Install dependencies - run: yarn install --dev + run: yarn install - name: Run syntax linter run: yarn cs @@ -72,8 +77,13 @@ jobs: key: ${{ runner.os }}-node-v${{ matrix.node-version }}-${{ hashFiles('**/package.json') }} restore-keys: | ${{ runner.os }}-node-v${{ matrix.node-version }}- + - name: Update yarn + run: | + corepack enable + yarn config set -H enableImmutableInstalls false + yarn install - name: Install dependencies - run: yarn install --dev + run: yarn install - name: Run unit tests run: yarn test:unit @@ -144,10 +154,15 @@ jobs: with: node-version: '23' - uses: actions/checkout@v4 + - name: Update yarn + run: | + corepack enable + yarn config set -H enableImmutableInstalls false + yarn install - name: Install dependencies - run: yarn install --dev + run: yarn install - name: Build app - run: yarn build && yarn build-storybook + run: yarn build && yarn storybook:build - uses: actions/upload-artifact@v4 name: storybook-static with: diff --git a/.gitignore b/.gitignore index 8307e0e..2a0b8b7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ dist/ # Misc .DS_Store +.yarn *.tgz diff --git a/.storybook/main.ts b/.storybook/main.ts index c63ee40..87b8050 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,53 +1,16 @@ -import type { StorybookConfig } from '@storybook/web-components-webpack5'; - -import projectWebpackConfig from '../webpack.config.cjs'; +import type { StorybookConfig } from '@storybook/web-components-vite'; const config: StorybookConfig = { stories: ['../storybook/**/*.mdx', '../storybook/**/*.stories.ts'], + staticDirs: [{ from: '../dist/', to: '/dist' }], addons: [ - '@storybook/addon-webpack5-compiler-swc', - '@storybook/addon-essentials', '@storybook/addon-themes', '@storybook/addon-a11y', + '@storybook/addon-docs' ], framework: { - name: '@storybook/web-components-webpack5', + name: '@storybook/web-components-vite', options: {}, - }, - webpackFinal: async (config) => - { - let rules = [...config.module.rules, ...projectWebpackConfig.module.rules]; - let filteredRules = []; - for (let i = 0; i < rules.length; i++) { - let rule = rules[i]; - if (!('test' in rule)){ - continue; - } - if (rule.test.toString().includes('css')) { - continue; - } - filteredRules.push(rule); - } - - filteredRules.push({ - test: /\.css$/, - oneOf: [ - { - resourceQuery: /style/, - use: [ 'style-loader', 'css-loader' ], - }, - { - type: 'asset/source', - }, - ] - }); - - - - return { - ...config, - module: { ...config.module, rules: filteredRules } - } - }, + } }; export default config; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 6fae04a..e4b8f27 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -1,5 +1,4 @@ - - + - + diff --git a/.storybook/preview.ts b/.storybook/preview.ts index a313fd9..fb8463c 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,25 +1,70 @@ -import type { Preview, WebComponentsRenderer } from '@storybook/web-components'; -import { withThemeByClassName } from '@storybook/addon-themes'; +import type {Preview} from '@storybook/web-components-vite'; -import * as tmp from '../dist/browser/index'; -console.log(tmp); - - -import * as EmberNexus from '@ember-nexus/web-sdk'; import './style.css?style'; import './preview.css?style'; -import {init} from '@ember-nexus/app-core'; -const WebSdkConfiguration = EmberNexus.Service.WebSdkConfiguration; -const container = EmberNexus.Container; -container.get(WebSdkConfiguration).setApiHost('https://reference-dataset.ember-nexus.dev'); -container.get(WebSdkConfiguration).setToken('secret-token:PIPeJGUt7c00ENn8a5uDlc' as EmberNexus.Type.Definition.Token); -container.get(EmberNexus.BrowserEvent.BrowserEventHandler).addBrowserEventListeners(document.body as HTMLElement); -console.log(document.body); +import {init as appPluginExperimentalInit} from '../dist/browser/index.js'; +// import * as appPluginExperimental from '../dist/browser/index.js'; +// console.log(appPluginExperimental); + +// import appPluginExperimental = require('../dist/browser/index.js'); +// console.log(appPluginExperimental); +// +// const appPluginExperimentalInit = appPluginExperimental.init; + -const serviceResolver = init(document.body); +// import {init as appPluginExperimentalInit} from '../dist/browser/index'; + + +import {init as appCoreInit} from '@ember-nexus/app-core'; +import {ApiConfiguration} from "@ember-nexus/app-core/Service"; +import {withTheme} from "./withTheme.decorator"; + +const serviceResolver = appCoreInit(document.body); (window as any).serviceResolver = serviceResolver; +const apiConfiguration = serviceResolver.getServiceOrFail(ApiConfiguration.identifier); +apiConfiguration.setApiHost('https://reference-dataset.ember-nexus.dev'); +apiConfiguration.setToken('secret-token:PIPeJGUt7c00ENn8a5uDlc' as any); + +appPluginExperimentalInit(serviceResolver); + +const themeService = serviceResolver.getServiceOrFail('ember-nexus.app-plugin-experimental.service.theme-service'); +const languageService = serviceResolver.getServiceOrFail('ember-nexus.app-plugin-experimental.service.language-service'); + + +const globalTypes = { + locale: { + name: "Language", + defaultValue: "en", + toolbar: { + icon: "globe", + items: [ + // default language + { value: "en", right: "EN", title: "English" }, + + // core languages + { value: "de", right: "DE", title: "German" }, + { value: "no", right: "NO", title: "Norwegian" }, + { value: "ko", right: "KO", title: "Korean" }, + + // additional languages + { value: "ar", right: "AR", title: "Arabic" }, + { value: "zh-CN", right: "ZH-CN", title: "Chinese" }, + { value: "fr", right: "FR", title: "French" }, + { value: "hi", right: "HI", title: "Hindi" }, + { value: "it", right: "IT", title: "Italian" }, + { value: "ja", right: "JA", title: "Japanese" }, + { value: "ru", right: "RU", title: "Russian" }, + { value: "es", right: "ES", title: "Spanish" }, + { value: "sw", right: "SW", title: "Swahili" }, + ], + title: "Language", + dynamicTitle: true + } + } +}; + const preview: Preview = { parameters: { controls: { @@ -30,21 +75,29 @@ const preview: Preview = { }, options: { storySort: (a, b) => { - return a.id === b.id ? 0 : a.id.localeCompare(b.id, undefined, { numeric: true }); + return a.id === b.id ? 0 : a.id.localeCompare(b.id, undefined, {numeric: true}); } }, layout: 'centered', }, decorators: [ - withThemeByClassName({ + withTheme({ themes: { - light: '', + light: 'light', dark: 'dark', - 'high contrast': 'high-contrast' + emerald: 'emerald', + dim: 'dim', }, defaultTheme: 'light', + themeService: themeService }), + (story, context) => { + const { locale } = context.globals; + // @ts-ignore + languageService.applyLanguage(locale); + return story(); + } ], }; - +export {globalTypes}; export default preview; diff --git a/.storybook/style.css b/.storybook/style.css index 92b708d..9798fcc 100644 --- a/.storybook/style.css +++ b/.storybook/style.css @@ -1,74 +1,4 @@ -/*@import url('@ember-nexus/uix/Style/index.css');*/ - -html { - font-size: 20px; - font-family: "Roboto"; - font-weight: 300; -} - body { margin: 0; padding: 0; - - --border-radius-none: 0; - --border-radius-thin: 3px; - --border-radius: 10px; - --border-radius-thick: 20px; - - --border-width-none: 0; - --border-width: 1px; - --color-content-background: #fff; - --color-border: #000; - --color-shadow: 220 3% 15%; - --shadow-strength: 1%; - --shadow-1: 0 1px 2px -1px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 9%)); - --shadow-2: 0 3px 5px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 3%)), - 0 7px 14px -5px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 5%)); - --shadow-3: 0 -1px 3px 0 hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 2%)), - 0 1px 2px -5px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 2%)), - 0 2px 5px -5px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 4%)), - 0 4px 12px -5px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 5%)), - 0 12px 15px -5px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 7%)); - --shadow-4: 0 -2px 5px 0 hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 2%)), - 0 1px 1px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 2px 2px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 5px 5px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 4%)), - 0 9px 9px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 5%)), - 0 16px 16px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 6%)); - --shadow-5: 0 -1px 2px 0 hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 2%)), - 0 2px 1px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 5px 5px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 10px 10px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 4%)), - 0 20px 20px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 5%)), - 0 40px 40px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 7%)); - --shadow-6: 0 -1px 2px 0 hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 2%)), - 0 3px 2px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 7px 5px -2px hsl(var(--color-shadow) / calc(var(--shadow-strength) + 3%)), - 0 12px 10px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 4%)), - 0 22px 18px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 5%)), - 0 41px 33px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 6%)), - 0 100px 80px -2px hsl(var(--color-shadow) / - calc(var(--shadow-strength) + 7%)); - --card-background: var(--color-content-background); - --card-border-radius: var(--border-radius); - --card-border-width: var(--border-width-none); - --card-border-color: var(--color-border); - --card-border-style: solid; - --card-shadow: var(--shadow-2); } diff --git a/.storybook/withTheme.decorator.ts b/.storybook/withTheme.decorator.ts new file mode 100644 index 0000000..80eb6b2 --- /dev/null +++ b/.storybook/withTheme.decorator.ts @@ -0,0 +1,22 @@ +import { DecoratorHelpers } from '@storybook/addon-themes'; +import type { DecoratorFunction, Renderer } from 'storybook/internal/types'; +import {useEffect} from 'storybook/preview-api'; + +const { initializeThemeState, pluckThemeFromContext } = DecoratorHelpers; + +export const withTheme = ({ themes, defaultTheme, themeService }): DecoratorFunction => { + initializeThemeState(Object.keys(themes), defaultTheme); + + return (story, context) => { + const selected = pluckThemeFromContext(context); + const { themeOverride } = context.parameters.themes ?? {}; + + useEffect(() => { + const themeKey = themeOverride || selected || defaultTheme; + + themeService.applyTheme(themeKey); + }, [themeOverride, selected]); + + return story(); + }; +}; diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..3186f3f --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/CHANGELOG.md b/CHANGELOG.md index a981946..a6431dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed +- Refactor app plugin experimental to use app-core instead of web-sdk. ## 0.0.12 - 2024-12-16 ### Added diff --git a/sync-translation.sh b/bin/sync-translation.sh old mode 100644 new mode 100755 similarity index 94% rename from sync-translation.sh rename to bin/sync-translation.sh index bb6df3e..51e86dc --- a/sync-translation.sh +++ b/bin/sync-translation.sh @@ -9,13 +9,15 @@ updateTermRequest=$(curl -s -X POST https://api.poeditor.com/v2/projects/upload -F updating="terms" \ -F language="en" \ -F sync_terms="1" \ - -F file=@"./src/Asset/Translation/en.json") + -F file=@"../src/Asset/Translation/en.json") echo "Downloading list of available languages." languagesRequest=$(curl -s -X POST https://api.poeditor.com/v2/languages/list \ -d api_token="${POEDITOR_API_KEY}" \ -d id="${POEDITOR_PROJECT_ID}") +echo $languagesRequest + echo "Downloading language files for available languages" for language in $(echo $languagesRequest | jq -r '.result.languages[].code'); do echo "Downloading file for language '$language'." diff --git a/eslint.config.mjs b/eslint.config.mjs index 49df53a..86c5e9e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,93 +1,147 @@ -import typescriptEslint from "@typescript-eslint/eslint-plugin"; -import prettier from "eslint-plugin-prettier"; -import _import from "eslint-plugin-import"; -import { fixupPluginRules } from "@eslint/compat"; -import globals from "globals"; -import tsParser from "@typescript-eslint/parser"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { fixupPluginRules } from '@eslint/compat'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import tseslint from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import compat from 'eslint-plugin-compat'; +import _import from 'eslint-plugin-import'; +import perfectionist from 'eslint-plugin-perfectionist'; +import prettier from 'eslint-plugin-prettier'; +import pluginPromise from 'eslint-plugin-promise'; +import globals from 'globals'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all +const flatCompat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, }); -export default [{ - ignores: ["dist/*", "**/*.js"], -}, ...compat.extends( - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:prettier/recommended", -).map(config => ({ - ...config, - files: ["**/*.ts"], -})), { - files: ["**/*.ts"], +const files = ['**/*.ts']; +export default [ + pluginPromise.configs['flat/recommended'], + compat.configs['flat/recommended'], + ...flatCompat.plugins('require-extensions'), + ...flatCompat + .extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ) + .map((config) => ({ + ...config, + files: files, + })), + { + files: files, plugins: { - "@typescript-eslint": typescriptEslint, - prettier, - import: fixupPluginRules(_import), + '@typescript-eslint': typescriptEslint, + prettier, + import: fixupPluginRules(_import), + perfectionist, }, - languageOptions: { - globals: { - ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])), - ...globals.node, - }, - - parser: tsParser, - ecmaVersion: 2020, - sourceType: "module", - - parserOptions: { - project: "tsconfig.json", - }, + globals: { + ...Object.fromEntries( + Object.entries(globals.browser).map(([key]) => [key, 'off']), + ), + ...globals.node, + ...globals.browser, + }, + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + parserOptions: { + project: 'tsconfig.test.json', + }, }, - settings: { - "import/resolver": { - typescript: { - project: "./tsconfig.json", - }, + 'import/resolver': { + typescript: { + project: './tsconfig.test.json', }, + }, }, - rules: { - "@typescript-eslint/explicit-function-return-type": "warn", - - "sort-imports": ["error", { - ignoreCase: false, - ignoreDeclarationSort: true, - ignoreMemberSort: false, - memberSyntaxSortOrder: ["none", "all", "multiple", "single"], - allowSeparatedGroups: true, - }], - - "import/no-unresolved": "error", - - "import/order": ["error", { - groups: [ - "builtin", - "external", - "internal", - ["sibling", "parent"], - "index", - "unknown", - ], - - "newlines-between": "always", - - alphabetize: { - order: "asc", - caseInsensitive: true, - }, - }], - "no-duplicate-imports": "error", + ...js.configs.recommended.rules, + ...tseslint.configs.strict, + '@typescript-eslint/explicit-function-return-type': 'warn', + '@typescript-eslint/no-unused-vars': 'error', + 'accessor-pairs': 'error', + 'block-scoped-var': 'error', + 'camelcase': 'error', + 'dot-notation': 'warn', + 'eqeqeq': ['error', 'always'], + 'import/no-unresolved': 'error', + 'import/order': [ + 'error', + { + groups: [ + 'builtin', + 'external', + 'internal', + ['sibling', 'parent'], + 'index', + 'unknown', + ], + 'newlines-between': 'always', + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + }, + ], + 'no-case-declarations': 'off', + 'no-console': 'error', + 'no-eq-null': 'error', + 'no-extra-bind': 'error', + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'error', + 'no-invalid-this': 'error', + 'no-return-assign': 'error', + 'no-sequences': 'error', + 'no-template-curly-in-string': 'error', + 'no-throw-literal': 'error', + 'no-unused-vars': 'off', + 'no-use-before-define': 'error', + 'no-var': 'error', + 'perfectionist/sort-exports': 'error', + 'perfectionist/sort-named-exports': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-const': 'error', + 'promise/always-return': 'off', + 'promise/avoid-new': 'off', + 'promise/catch-or-return': 'off', + 'promise/no-callback-in-promise': 'warn', + 'promise/no-multiple-resolved': 'error', + 'promise/no-native': 'off', + 'promise/no-nesting': 'warn', + 'promise/no-new-statics': 'error', + 'promise/no-promise-in-callback': 'warn', + 'promise/no-return-in-finally': 'warn', + 'promise/no-return-wrap': 'error', + 'promise/param-names': 'error', + 'promise/valid-params': 'warn', + 'require-atomic-updates': 'warn', + 'require-await': 'error', + 'require-extensions/require-extensions': 'error', + 'require-extensions/require-index': 'error', + 'sort-imports': [ + 'error', + { + ignoreCase: false, + ignoreDeclarationSort: true, + ignoreMemberSort: false, + memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], + allowSeparatedGroups: true, + }, + ] }, -}]; + }, +]; diff --git a/globals.d.ts b/globals.d.ts index b6f1ff0..afa48aa 100644 --- a/globals.d.ts +++ b/globals.d.ts @@ -12,3 +12,8 @@ declare module '*.css?style' { const content: string; export default content; } + +declare module '*.css?inline' { + const content: string; + export default content; +} diff --git a/package.json b/package.json index e4c1296..1d37872 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,16 @@ "engines": { "node": ">= 20.0" }, + "browserslist": [ + "last 2 years" + ], "main": "dist/esm/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts", "scripts": { "build": "rm -rf ./dist && yarn build:esm && yarn build:browser", - "build:browser": "webpack --config webpack.config.cjs", - "build:esm": "yarn tsc -b tsconfig.esm.json", + "build:browser": "vite build", + "build:esm": "yarn tsc -b tsconfig.esm.json && yarn cpy 'src/**/*' '!src/**/*.ts' dist/esm", "cs": "eslint ./src", "cs:fix": "eslint ./src --fix", "test:unit": "vitest run ./test/Unit", @@ -19,52 +22,65 @@ "prepare": "ts-patch install", "typedoc": "typedoc --tsconfig tsconfig.release.json", "storybook": "storybook dev -p 6006 -h 0.0.0.0", - "build-storybook": "storybook build" + "storybook:build": "storybook build" }, "author": "Syndesi ", "license": "MIT", "dependencies": { - "@ember-nexus/app-core": "^0.0.1", - "@ember-nexus/uix": "^0.0.4", - "@ember-nexus/web-sdk": "^0.0.59", + "@antv/g6": "^5.0.49", + "@ember-nexus/app-core": "^0.0.6", "apca-w3": "^0.1.9", "color-hash": "^2.0.2", "colorparsley": "^0.1.8", - "date-fns": "^4.1.0", + "culori": "^4.0.2", + "i18next": "^25.3.2", "lit": "^3.1.3", + "lucide-static": "^0.525.0", + "luxon": "^3.6.1", + "shiki": "^3.7.0", "tslog": "^4.9.2", "xstate": "^5.13.1" }, "devDependencies": { - "@chromatic-com/storybook": "^3.2.2", + "@ameinhardt/unocss-preset-daisy": "^1.1.8", + "@chromatic-com/storybook": "^4.0.1", "@eslint/compat": "^1.1.0", "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.6.0", - "@storybook/addon-a11y": "^8.4.7", - "@storybook/addon-essentials": "^8.4.7", - "@storybook/addon-themes": "^8.4.7", - "@storybook/addon-webpack5-compiler-swc": "^1.0.5", - "@storybook/blocks": "^8.4.7", - "@storybook/test": "^8.4.7", - "@storybook/web-components": "^8.4.7", - "@storybook/web-components-webpack5": "^8.4.7", + "@storybook/addon-a11y": "^9.0.18", + "@storybook/addon-docs": "^9.0.18", + "@storybook/addon-themes": "^9.0.18", + "@storybook/web-components-vite": "^9.0.18", + "@tailwindcss/vite": "^4.1.11", "@types/color-hash": "^2.0.0", + "@types/culori": "^4.0.0", + "@types/luxon": "^3.6.2", "@types/node": "^20.8.10", "@types/sinon": "^17.0.0", "@typescript-eslint/eslint-plugin": "^7.14", "@typescript-eslint/parser": "^7.14", + "@unocss/core": "^66.3.2", + "@unocss/preset-attributify": "^66.3.2", + "@unocss/preset-typography": "^66.3.2", + "@unocss/preset-wind4": "^66.3.2", "@vitest/coverage-v8": "^3.1.4", "@vitest/eslint-plugin": "^1.2.1", "@vitest/ui": "^3.1.4", "copyfiles": "^2.4.1", + "cpy-cli": "^5.0.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", + "daisyui": "^5.0.43", "eslint": "^9.6", "eslint-config-prettier": "^9.0.0", "eslint-import-resolver-typescript": "^3.5.3", + "eslint-plugin-compat": "^6.0.2", "eslint-plugin-import": "^2.27.5", + "eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-prettier": "^5.0.1", - "eslint-plugin-storybook": "^0.11.1", + "eslint-plugin-promise": "^7.2.1", + "eslint-plugin-require-extensions": "^0.1.3", + "eslint-plugin-storybook": "^9.0.18", "globals": "^15.6.0", "http-server": "^14.1.1", "ignore-loader": "^0.1.2", @@ -74,19 +90,24 @@ "prettier": "^3.0.3", "rimraf": "~5.0.5", "sinon": "^18.0", - "storybook": "^8.4.7", + "storybook": "^9.0.18", + "strong-mock": "^9.0.1", "style-loader": "^4.0.0", + "tailwindcss": "^4.1.11", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", "ts-mockito": "^2.6.1", "ts-node": "^10.9.2", "ts-patch": "^3.0.2", + "tslib": "^2.8.1", "tsutils": "~3.21", "typedoc": "^0.26", "typedoc-plugin-markdown": "^4.1", "typedoc-plugin-mdn-links": "^3.1.9", "typedoc-theme-hierarchy": "^5.0", "typescript": "^5.5", + "unocss": "^66.3.2", + "vite": "^7.0.0", "vitest": "^3.1.4", "webpack": "^5.89.0", "webpack-cli": "^5.1.4" @@ -95,5 +116,6 @@ "extends": [ "plugin:storybook/recommended" ] - } + }, + "packageManager": "yarn@4.9.2" } diff --git a/src/Asset/Translation/ar.json b/src/Asset/Translation/ar.json index c6ee767..71882af 100644 --- a/src/Asset/Translation/ar.json +++ b/src/Asset/Translation/ar.json @@ -1,4 +1,11 @@ { "description": "مجموعة من المكونات التجريبية.", - "name": "تجريبي" + "name": "تجريبي", + "page.login.title": "تسجيل الدخول", + "page.login.usernameLabel": "البريد الإلكتروني أو اسم المستخدم:", + "page.login.passwordLabel": "كلمة المرور:", + "page.login.usernamePlaceholder": "اكتب هنا", + "page.login.passwordPlaceholder": "اكتب هنا", + "page.login.actionClear": "واضح", + "page.login.actionLogin": "تسجيل الدخول" } \ No newline at end of file diff --git a/src/Asset/Translation/de.json b/src/Asset/Translation/de.json index 81fbdf9..30e97fd 100644 --- a/src/Asset/Translation/de.json +++ b/src/Asset/Translation/de.json @@ -1,4 +1,11 @@ { "description": "Sammlung experimenteller Komponenten.", - "name": "Experimental" + "name": "Experimental", + "page.login.title": "Login", + "page.login.usernameLabel": "E-Mail oder Benutzername:", + "page.login.passwordLabel": "Passwort:", + "page.login.usernamePlaceholder": "Hier eingeben", + "page.login.passwordPlaceholder": "Hier eingeben", + "page.login.actionClear": "Formular leeren", + "page.login.actionLogin": "Login" } \ No newline at end of file diff --git a/src/Asset/Translation/en.json b/src/Asset/Translation/en.json index 653e7e1..6400dce 100644 --- a/src/Asset/Translation/en.json +++ b/src/Asset/Translation/en.json @@ -1,4 +1,11 @@ { "description": "Collection of experimental components.", - "name": "Experimental" + "name": "Experimental", + "page.login.title": "Login", + "page.login.usernameLabel": "Email or username:", + "page.login.passwordLabel": "Password:", + "page.login.usernamePlaceholder": "Type here", + "page.login.passwordPlaceholder": "Type here", + "page.login.actionClear": "Clear", + "page.login.actionLogin": "Login" } \ No newline at end of file diff --git a/src/Asset/Translation/es.json b/src/Asset/Translation/es.json index 33f6810..4001bdf 100644 --- a/src/Asset/Translation/es.json +++ b/src/Asset/Translation/es.json @@ -1,4 +1,11 @@ { "description": "Colección de componentes experimentales.", - "name": "Experimental" + "name": "Experimental", + "page.login.title": "Acceso", + "page.login.usernameLabel": "Correo electrónico o nombre de usuario:", + "page.login.passwordLabel": "Contraseña:", + "page.login.usernamePlaceholder": "Escribe aquí", + "page.login.passwordPlaceholder": "Escribe aquí", + "page.login.actionClear": "Clara", + "page.login.actionLogin": "Acceso" } \ No newline at end of file diff --git a/src/Asset/Translation/fr.json b/src/Asset/Translation/fr.json index 6c53baa..cdca0e6 100644 --- a/src/Asset/Translation/fr.json +++ b/src/Asset/Translation/fr.json @@ -1,4 +1,11 @@ { "description": "Collection de composants expérimentaux.", - "name": "Expérimental" + "name": "Expérimental", + "page.login.title": "Se connecter", + "page.login.usernameLabel": "E-mail ou nom d'utilisateur :", + "page.login.passwordLabel": "Mot de passe:", + "page.login.usernamePlaceholder": "Tapez ici", + "page.login.passwordPlaceholder": "Tapez ici", + "page.login.actionClear": "Claire", + "page.login.actionLogin": "Se connecter" } \ No newline at end of file diff --git a/src/Asset/Translation/hi.json b/src/Asset/Translation/hi.json index 8bdcd32..55eca95 100644 --- a/src/Asset/Translation/hi.json +++ b/src/Asset/Translation/hi.json @@ -1,4 +1,11 @@ { "description": "प्रयोगात्मक घटकों का संग्रह.", - "name": "प्रयोगात्मक" + "name": "प्रयोगात्मक", + "page.login.title": "लॉग इन करें", + "page.login.usernameLabel": "ईमेल या उपयोगकर्ता का नाम:", + "page.login.passwordLabel": "पासवर्ड:", + "page.login.usernamePlaceholder": "यहाँ टाइप करें", + "page.login.passwordPlaceholder": "यहाँ टाइप करें", + "page.login.actionClear": "स्पष्ट", + "page.login.actionLogin": "लॉग इन करें" } \ No newline at end of file diff --git a/src/Asset/Translation/it.json b/src/Asset/Translation/it.json index 9e171f8..2447207 100644 --- a/src/Asset/Translation/it.json +++ b/src/Asset/Translation/it.json @@ -1,4 +1,11 @@ { "description": "Raccolta di componenti sperimentali.", - "name": "Sperimentale" + "name": "Sperimentale", + "page.login.title": "Login", + "page.login.usernameLabel": "Email o nome utente:", + "page.login.passwordLabel": "Password:", + "page.login.usernamePlaceholder": "Scrivi qui", + "page.login.passwordPlaceholder": "Scrivi qui", + "page.login.actionClear": "Chiara", + "page.login.actionLogin": "Login" } \ No newline at end of file diff --git a/src/Asset/Translation/ja.json b/src/Asset/Translation/ja.json index 88b70b2..d999c77 100644 --- a/src/Asset/Translation/ja.json +++ b/src/Asset/Translation/ja.json @@ -1,4 +1,11 @@ { "description": "実験コンポーネントのコレクション。", - "name": "実験的" + "name": "実験的", + "page.login.title": "ログイン", + "page.login.usernameLabel": "メールアドレスまたはユーザー名:", + "page.login.passwordLabel": "パスワード:", + "page.login.usernamePlaceholder": "ここに入力してください", + "page.login.passwordPlaceholder": "ここに入力してください", + "page.login.actionClear": "クリア", + "page.login.actionLogin": "ログイン" } \ No newline at end of file diff --git a/src/Asset/Translation/ko.json b/src/Asset/Translation/ko.json index 37f3c60..7d11d07 100644 --- a/src/Asset/Translation/ko.json +++ b/src/Asset/Translation/ko.json @@ -1,4 +1,11 @@ { "description": "실험적 구성요소의 수집.", - "name": "실험적" + "name": "실험적", + "page.login.title": "로그인", + "page.login.usernameLabel": "이메일 또는 사용자 이름:", + "page.login.passwordLabel": "비밀번호:", + "page.login.usernamePlaceholder": "여기에 입력하세요", + "page.login.passwordPlaceholder": "여기에 입력하세요", + "page.login.actionClear": "분명한", + "page.login.actionLogin": "로그인" } \ No newline at end of file diff --git a/src/Asset/Translation/no.json b/src/Asset/Translation/no.json index 6e3d2b2..d71198a 100644 --- a/src/Asset/Translation/no.json +++ b/src/Asset/Translation/no.json @@ -1,4 +1,11 @@ { "description": "Samling av eksperimentelle komponenter.", - "name": "Eksperimentell" + "name": "Eksperimentell", + "page.login.title": "Logg inn", + "page.login.usernameLabel": "E-post eller brukernavn:", + "page.login.passwordLabel": "Passord:", + "page.login.usernamePlaceholder": "Skriv her", + "page.login.passwordPlaceholder": "Skriv her", + "page.login.actionClear": "Klar", + "page.login.actionLogin": "Logg inn" } \ No newline at end of file diff --git a/src/Asset/Translation/ru.json b/src/Asset/Translation/ru.json index 9173a8d..5e57daf 100644 --- a/src/Asset/Translation/ru.json +++ b/src/Asset/Translation/ru.json @@ -1,4 +1,11 @@ { "description": "Коллекция экспериментальных компонентов.", - "name": "Экспериментальный" + "name": "Экспериментальный", + "page.login.title": "Авторизоваться", + "page.login.usernameLabel": "Электронная почта или имя пользователя:", + "page.login.passwordLabel": "Пароль:", + "page.login.usernamePlaceholder": "Введите здесь", + "page.login.passwordPlaceholder": "Введите здесь", + "page.login.actionClear": "Прозрачный", + "page.login.actionLogin": "Авторизоваться" } \ No newline at end of file diff --git a/src/Asset/Translation/sw.json b/src/Asset/Translation/sw.json index fced8dd..c13efdb 100644 --- a/src/Asset/Translation/sw.json +++ b/src/Asset/Translation/sw.json @@ -1,4 +1,11 @@ { "description": "Mkusanyiko wa vipengele vya majaribio.", - "name": "Majaribio" + "name": "Majaribio", + "page.login.title": "Ingia", + "page.login.usernameLabel": "Barua pepe au jina la mtumiaji:", + "page.login.passwordLabel": "Nenosiri:", + "page.login.usernamePlaceholder": "Andika hapa", + "page.login.passwordPlaceholder": "Andika hapa", + "page.login.actionClear": "Wazi", + "page.login.actionLogin": "Ingia" } \ No newline at end of file diff --git a/src/Asset/Translation/zh-CN.json b/src/Asset/Translation/zh-CN.json index 386c2ff..787581f 100644 --- a/src/Asset/Translation/zh-CN.json +++ b/src/Asset/Translation/zh-CN.json @@ -1,4 +1,11 @@ { "description": "实验组件的收集。", - "name": "实验" + "name": "实验", + "page.login.title": "登录", + "page.login.usernameLabel": "电子邮件或用户名:", + "page.login.passwordLabel": "密码:", + "page.login.usernamePlaceholder": "在此处输入", + "page.login.passwordPlaceholder": "在此处输入", + "page.login.actionClear": "清除", + "page.login.actionLogin": "登录" } \ No newline at end of file diff --git a/src/Component/Debug/EmberNexusDebugCard.ts b/src/Component/Debug/EmberNexusDebugCard.ts new file mode 100644 index 0000000..b181459 --- /dev/null +++ b/src/Component/Debug/EmberNexusDebugCard.ts @@ -0,0 +1,115 @@ +import { ServiceResolver } from '@ember-nexus/app-core/Service'; +import { Color, formatHex8, rgb } from 'culori'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { DirectiveResult } from 'lit/directive.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { UnsafeHTMLDirective } from 'lit-html/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { HighlighterCore } from 'shiki'; +import { SnapshotFrom } from 'xstate'; + +import { withUpdateOnThemeChange } from '../../Decorator/index.js'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { ShikiJsonHighlighterService, ThemeService } from '../../Service/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-debug-card') +@withUpdateOnThemeChange() +@withGetElementMachine() +class EmberNexusDebugCard extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + highlighter: HighlighterCore | undefined; + + calculateColorStyles(): object { + const styles = {}; + const color = this.state.context?.element?.data?.color; + if (color) { + styles['--color-base-100'] = color; + styles['--tw-shadow-color'] = formatHex8({ ...rgb(String(color)), alpha: 0.2 } as Color); + } + return styles; + } + + onServiceResolverLoaded(serviceResolver: ServiceResolver): void { + const highlighterService = serviceResolver.getServiceOrFail( + ShikiJsonHighlighterService.identifier, + ); + highlighterService.getShikiHighlighter().then((highlighter: HighlighterCore) => { + this.highlighter = highlighter; + this.requestUpdate(); + }); + } + + render(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + const errorName = this.state.context?.error?.constructor?.name ?? 'Error'; + const errorDescription = (this.state.context?.error as Error)?.message ?? String(this.state.context?.error); + return html` +
+
+ ${unsafeHTML(TriangleAlert)} +
+
+

${errorName}

+

${this.state.context?.element?.id ?? this.elementId}

+

${errorDescription}

+
+
+ `; + case getElementMachineTags.Loading: + return html` +
+
+

Loading

+

${this.state.context?.element?.id ?? this.elementId}

+
+
+ `; + case getElementMachineTags.Loaded: + let renderedCode: string | TemplateResult | DirectiveResult = + 'waiting to be rendered'; + if (this.highlighter) { + const code = this.highlighter.codeToHtml(JSON.stringify(this.state.context?.element, null, 2), { + lang: 'json', + theme: + this.state.context.serviceResolver + ?.getServiceOrFail(ThemeService.identifier) + ?.getActiveTheme()?.shikiTheme ?? ShikiJsonHighlighterService.defaultTheme, + }); + renderedCode = unsafeHTML( + `
${code}
`, + ); + } + return html` +
+
+

+ ${this.state.context?.element?.data?.name ?? 'unknown name'} +
+ ${this.state.context?.element?.type ?? 'unknown type'} +
+

+

${this.state.context?.element?.id ?? this.elementId}

+ ${renderedCode} +
+
+ `; + } + } +} + +export { EmberNexusDebugCard }; diff --git a/src/Component/Debug/index.ts b/src/Component/Debug/index.ts new file mode 100644 index 0000000..b42b43e --- /dev/null +++ b/src/Component/Debug/index.ts @@ -0,0 +1 @@ +export * from './EmberNexusDebugCard.js'; diff --git a/src/Component/Default/EmberNexusDefaultCard.ts b/src/Component/Default/EmberNexusDefaultCard.ts index bdc6dad..185156a 100644 --- a/src/Component/Default/EmberNexusDefaultCard.ts +++ b/src/Component/Default/EmberNexusDefaultCard.ts @@ -1,150 +1,83 @@ -import CardStyle from '@ember-nexus/uix/Style/Component/CardStyle.css'; -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { format } from 'date-fns'; +import { Color, formatHex8, rgb } from 'culori'; import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; +import { customElement, property } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; -import { Actor, createActor } from 'xstate'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; -import { - findBestFontWeightColor, - getColorFromElementOrId, - getIconForElement, - getNameFromElementOrId, - getTitleFromElementOrId, -} from '../../Helper'; -import { singleElementMachine } from '../../Machine'; -import { tmpStyle } from '../../Style'; -import { colorWarning } from '../../Type'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; @customElement('ember-nexus-default-card') +@withGetElementMachine() class EmberNexusDefaultCard extends LitElement { - static styles = [unsafeCSS(CardStyle), tmpStyle]; + static styles = [unsafeCSS(indexStyles)]; - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; - - @state() - protected _color: string = '#000'; - - protected actor: Actor; + state: SnapshotFrom; - constructor() { - super(); + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; } - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - switch (snapshot.value) { - case 'Loaded': - this._color = getColorFromElementOrId(this.elementId, this._element); - break; - case 'Error': - this._color = colorWarning; - break; - default: - this._color = '#000'; - } - this.requestUpdate(); - }); - } - - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } - - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); - } + @property({ type: String, attribute: 'element-id' }) + elementId: string; - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); + calculateColorStyles(): object { + const styles = {}; + const color = this.state.context?.element?.data?.color; + if (color) { + styles['--color-base-100'] = color; + styles['--tw-shadow-color'] = formatHex8({ ...rgb(String(color)), alpha: 0.2 } as Color); } + return styles; } render(): TemplateResult { - const textStyles = findBestFontWeightColor(this._color, ['#000', '#fff'], [400, 500, 600, 700]); - - const backgroundStyle = { - backgroundColor: this._color, - }; - let title: string; - if (this._error == null) { - title = getTitleFromElementOrId(this.elementId, this._element); - } else { - title = this._error; - } - - let icon: TemplateResult | null = null; - const iconStyle = { - fill: textStyles.color, - }; - if (this._element) { - icon = html`${getIconForElement(this._element)}`; - } else { - icon = html` - - - - `; - } - - let description = ''; - if (this._element) { - if ('description' in this._element.data) { - description = this._element.data.description as string; - } + switch (this.stateTag) { + case getElementMachineTags.Error: + const errorName = this.state.context?.error?.constructor?.name ?? 'Error'; + const errorDescription = (this.state.context?.error as Error)?.message ?? String(this.state.context?.error); + return html` +
+
+ ${unsafeHTML(TriangleAlert)} +
+
+

${errorName}

+

${this.state.context?.element?.id ?? this.elementId}

+

${errorDescription}

+
+
+ `; + case getElementMachineTags.Loading: + return html` +
+
+

Loading

+

${this.state.context?.element?.id ?? this.elementId}

+
+
+ `; + case getElementMachineTags.Loaded: + let description = this.state.context?.element?.data?.description; + if (description) description = html`

${description}

`; + return html` +
+
+

+ ${this.state.context?.element?.data?.name ?? 'unknown name'} +
+ ${this.state.context?.element?.type ?? 'unknown type'} +
+

+

${this.state.context?.element?.id ?? this.elementId}

+ ${description} +
+
+ `; } - - let createdString: null | string = null; - if (this._element?.data.created) { - if (this._element?.data.created instanceof Date) { - createdString = `created on ${format(this._element?.data.created as Date, 'yyyy-MM-dd')}`; - } - } - - let updatedString: null | string = null; - if (this._element?.data.updated) { - if (this._element?.data.updated instanceof Date) { - updatedString = `updated on ${format(this._element?.data.updated as Date, 'yyyy-MM-dd')}`; - } - } - - return html`
-
- ${icon} -

${getNameFromElementOrId(this.elementId, this._element)}

-
-
-

${this.elementId} ${this._element?.type}

-

${createdString} ${updatedString}

-
-
-

${description}

-

blub blab :D

-
-
`; } } diff --git a/src/Component/Default/EmberNexusDefaultFrameless.ts b/src/Component/Default/EmberNexusDefaultFrameless.ts deleted file mode 100644 index 2297d66..0000000 --- a/src/Component/Default/EmberNexusDefaultFrameless.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import { Actor, createActor } from 'xstate'; - -import { findBestFontWeightColor, getInitialsFromElementOrId, getTitleFromElementOrId } from '../../Helper'; -import { getColorFromElementOrId } from '../../Helper/ColorHelper'; -import { singleElementMachine } from '../../Machine'; -import { fontStyle, framelessComponentStyle } from '../../Style'; -import { colorWarning } from '../../Type'; - -@customElement('ember-nexus-default-frameless') -class EmberNexusDefaultFrameless extends LitElement { - static styles = [framelessComponentStyle, fontStyle]; - - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; - - @state() - protected _color: string = '#000'; - - protected actor: Actor; - - constructor() { - super(); - } - - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - switch (snapshot.value) { - case 'Loaded': - this._color = getColorFromElementOrId(this.elementId, this._element); - break; - case 'Error': - this._color = colorWarning; - break; - default: - this._color = '#000'; - } - this.requestUpdate(); - }); - } - - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } - - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); - } - - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); - } - } - - render(): TemplateResult { - const textStyles = findBestFontWeightColor(this._color, ['#000', '#fff'], [400, 500, 600, 700]); - - const backgroundStyle = { - backgroundColor: this._color, - }; - let content: TemplateResult; - let title: string; - if (this._error == null) { - content = html` - ${getInitialsFromElementOrId(this.elementId, this._element)} - `; - title = getTitleFromElementOrId(this.elementId, this._element); - } else { - content = html`
- - - -
`; - title = this._error; - } - return html`
- ${content} -
`; - } -} - -export { EmberNexusDefaultFrameless }; diff --git a/src/Component/Default/EmberNexusDefaultIcon.ts b/src/Component/Default/EmberNexusDefaultIcon.ts index 153b010..6f0f11e 100644 --- a/src/Component/Default/EmberNexusDefaultIcon.ts +++ b/src/Component/Default/EmberNexusDefaultIcon.ts @@ -1,109 +1,63 @@ -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import { Actor, createActor } from 'xstate'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; -import { - findBestFontWeightColor, - getColorFromElementOrId, - getInitialsFromElementOrId, - getTitleFromElementOrId, -} from '../../Helper'; -import { singleElementMachine } from '../../Machine'; -import { fontStyle, iconComponentStyle, shadowStyle } from '../../Style'; -import { colorWarning } from '../../Type'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; @customElement('ember-nexus-default-icon') +@withGetElementMachine() class EmberNexusDefaultIcon extends LitElement { - static styles = [iconComponentStyle, shadowStyle, fontStyle]; + static styles = [unsafeCSS(indexStyles)]; - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; - - @state() - protected _color: string = '#000'; - - protected actor: Actor; - - constructor() { - super(); - } + state: SnapshotFrom; - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - switch (snapshot.value) { - case 'Loaded': - this._color = getColorFromElementOrId(this.elementId, this._element); - break; - case 'Error': - this._color = colorWarning; - break; - default: - this._color = '#000'; - } - this.requestUpdate(); - }); + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; } - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } - - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); - } + @property({ type: String, attribute: 'element-id' }) + elementId: string; - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); + renderIconContent(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` +
+ ${unsafeHTML(TriangleAlert)} +
+ `; + case getElementMachineTags.Loading: + return html` +
+ +
+ `; + case getElementMachineTags.Loaded: + let identifier = this.state.context?.element?.data?.name; + if (identifier) { + identifier = String(identifier).toUpperCase().slice(0, 2); + } else { + identifier = this.state.context?.element?.id ?? this.elementId; + identifier = String(identifier).toUpperCase().slice(0, 2); + identifier = html`${identifier}`; + } + return html` +
+ ${identifier} +
+ `; } } render(): TemplateResult { - const textStyles = findBestFontWeightColor(this._color, ['#000', '#fff'], [400, 500, 600, 700]); - - const backgroundStyle = { - backgroundColor: this._color, - }; - let content: TemplateResult; - let title: string; - if (this._error == null) { - content = html` - ${getInitialsFromElementOrId(this.elementId, this._element)} - `; - title = getTitleFromElementOrId(this.elementId, this._element); - } else { - content = html`
- - - -
`; - title = this._error; - } - return html`
- ${content} -
`; + return html`
${this.renderIconContent()}
`; } } diff --git a/src/Component/Default/EmberNexusDefaultInlineText.ts b/src/Component/Default/EmberNexusDefaultInlineText.ts index 02f85aa..6d2a13f 100644 --- a/src/Component/Default/EmberNexusDefaultInlineText.ts +++ b/src/Component/Default/EmberNexusDefaultInlineText.ts @@ -1,82 +1,39 @@ -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { Actor, createActor } from 'xstate'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { SnapshotFrom } from 'xstate'; -import { getNameOrFirstLettersFromIdFromElementOrId, getTitleFromElementOrId } from '../../Helper'; -import { singleElementMachine } from '../../Machine'; -import { inlineTextComponentStyle } from '../../Style'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; @customElement('ember-nexus-default-inline-text') +@withGetElementMachine() class EmberNexusDefaultInlineText extends LitElement { - static styles = [inlineTextComponentStyle]; + static styles = [unsafeCSS(indexStyles)]; - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; + state: SnapshotFrom; - protected actor: Actor; - - constructor() { - super(); - } - - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - this.requestUpdate(); - }); + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; } - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } - - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); - } - - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); - } - } + @property({ type: String, attribute: 'element-id' }) + elementId: string; render(): TemplateResult { - let content: string; - switch (this.actor.getSnapshot().value) { - case 'Loaded': - content = getTitleFromElementOrId(this.elementId, this._element); - break; - case 'Error': - content = '[' + this._error + ']'; - break; - default: - content = getNameOrFirstLettersFromIdFromElementOrId(this.elementId, this._element); + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` Error `; + case getElementMachineTags.Loading: + return html` Loading `; + case getElementMachineTags.Loaded: + let name = this.state.context?.element?.data?.name; + if (!name) { + const namePart = (this.state.context?.element?.id ?? this.elementId).slice(0, 8); + name = html`${namePart} (${this.state.context?.element?.type})`; + } + return html` ${name} `; } - return html` - - ${content} - - `; } } diff --git a/src/Component/Default/EmberNexusDefaultPill.ts b/src/Component/Default/EmberNexusDefaultPill.ts index dc226e0..3c17e1d 100644 --- a/src/Component/Default/EmberNexusDefaultPill.ts +++ b/src/Component/Default/EmberNexusDefaultPill.ts @@ -1,119 +1,46 @@ -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import { Actor, createActor } from 'xstate'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { SnapshotFrom } from 'xstate'; -import { - findBestFontWeightColor, - getColorFromElement, - getColorFromElementOrId, - getNameFromElementOrId, - getNameOrFirstLettersFromIdFromElementOrId, -} from '../../Helper'; -import { singleElementMachine } from '../../Machine'; -import { fontStyle, pillComponentStyle, shadowStyle } from '../../Style'; -import { colorWarning } from '../../Type'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; @customElement('ember-nexus-default-pill') +@withGetElementMachine() class EmberNexusDefaultPill extends LitElement { - static styles = [pillComponentStyle, shadowStyle, fontStyle]; + static styles = [unsafeCSS(indexStyles)]; - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; - - @state() - protected _borderColor: string = '#000'; - - @state() - protected _backgroundColor: string = '#fff'; - - protected actor: Actor; - - constructor() { - super(); - } + state: SnapshotFrom; - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - switch (snapshot.value) { - case 'Loaded': - this._borderColor = getColorFromElementOrId(this.elementId, this._element); - this._backgroundColor = getColorFromElement(this._element) ?? '#fff'; - break; - case 'Error': - this._borderColor = colorWarning; - this._backgroundColor = colorWarning; - break; - default: - this._borderColor = '#000'; - this._backgroundColor = '#fff'; - } - this.requestUpdate(); - }); + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; } - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } - - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); - } - - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); - } - } + @property({ type: String, attribute: 'element-id' }) + elementId: string; render(): TemplateResult { - let content: string; - let icon: TemplateResult | null = null; - if (this._error == null) { - if (this.actor.getSnapshot().value !== 'Loaded') { - content = getNameOrFirstLettersFromIdFromElementOrId(this.elementId, this._element); - } else { - content = getNameFromElementOrId(this.elementId, this._element); - } - } else { - content = 'Error'; - icon = html`
- - - -
`; + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` +
+ Error +
+ `; + case getElementMachineTags.Loading: + return html`
Loading
`; + case getElementMachineTags.Loaded: + let name = this.state.context?.element?.data?.name; + if (!name) { + const namePart = (this.state.context?.element?.id ?? this.elementId).slice(0, 8); + name = html`${namePart} (${this.state.context?.element?.type})`; + } + return html`
${name}
`; } - const colorStyle = { - backgroundColor: this._backgroundColor, - borderColor: this._borderColor, - }; - const textStyles = findBestFontWeightColor(this._backgroundColor, ['#000', '#fff'], [400, 500, 600, 700]); - - return html`
- ${icon} - ${content} -
`; } } diff --git a/src/Component/Default/EmberNexusDefaultThumbnail.ts b/src/Component/Default/EmberNexusDefaultThumbnail.ts index 4af7545..54cd5f1 100644 --- a/src/Component/Default/EmberNexusDefaultThumbnail.ts +++ b/src/Component/Default/EmberNexusDefaultThumbnail.ts @@ -1,113 +1,65 @@ -import { Node, Relation, Uuid } from '@ember-nexus/web-sdk/Type/Definition'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; -import { Actor, createActor } from 'xstate'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; -import { - findBestFontWeightColor, - getColorFromElementOrId, - getNameFromElementOrId, - getNameOrFirstLettersFromIdFromElementOrId, -} from '../../Helper'; -import { singleElementMachine } from '../../Machine'; -import { fontStyle, shadowStyle, thumbnailComponentStyle } from '../../Style'; -import { colorWarning } from '../../Type'; +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; @customElement('ember-nexus-default-thumbnail') +@withGetElementMachine() class EmberNexusDefaultThumbnail extends LitElement { - static styles = [thumbnailComponentStyle, shadowStyle, fontStyle]; + static styles = [unsafeCSS(indexStyles)]; - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @state() - protected _element: null | Node | Relation = null; - - @state() - protected _error: null | string = null; - - @state() - protected _color: string = '#000'; - - protected actor: Actor; - - constructor() { - super(); - } - - setupActorSubscription(): void { - this.actor.subscribe((snapshot) => { - this._element = snapshot.context.element; - this._error = snapshot.context.error; - switch (snapshot.value) { - case 'Loaded': - this._color = getColorFromElementOrId(this.elementId, this._element); - break; - case 'Error': - this._color = colorWarning; - break; - default: - this._color = '#000'; - } - this.requestUpdate(); - }); - } - - connectedCallback(): void { - super.connectedCallback(); - this.actor = createActor(singleElementMachine, { - input: { - elementId: this.elementId as Uuid, - htmlElement: this.renderRoot, - }, - }); - this.setupActorSubscription(); - this.actor.start(); - } + state: SnapshotFrom; - disconnectedCallback(): void { - this.actor.stop(); - super.disconnectedCallback(); + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; } - updated(changedProperties): void { - if (changedProperties.has('elementId')) { - this.actor.send({ - type: 'reset', - elementId: this.elementId as Uuid, - }); - } - } + @property({ type: String, attribute: 'element-id' }) + elementId: string; render(): TemplateResult { - const textStyles = findBestFontWeightColor(this._color, ['#000', '#fff'], [400, 500, 600, 700]); - - const backgroundStyle = { - backgroundColor: this._color, - }; - - const primaryText = getNameOrFirstLettersFromIdFromElementOrId(this.elementId, this._element); - const primaryTextTitle = getNameFromElementOrId(this.elementId, this._element); - let secondaryText = this.actor.getSnapshot().value as string; - let secondaryTextTitle = this.actor.getSnapshot().value as string; - - if ((this.actor.getSnapshot().value as string) == 'Loaded') { - secondaryText = this._element?.type ?? ''; - secondaryTextTitle = this._element?.type ?? ''; + switch (this.stateTag) { + case getElementMachineTags.Error: + const errorDescription = (this.state.context?.error as Error)?.message ?? String(this.state.context?.error); + return html` +
+
+ ${unsafeHTML(TriangleAlert)} +
+
+

Error

+

${errorDescription}

+
+
+ `; + case getElementMachineTags.Loading: + return html` +
+
+

Loading

+
+
+ `; + case getElementMachineTags.Loaded: + return html` +
+
+

+ ${this.state.context?.element?.data?.name ?? 'unknown name'} +

+

+ ${(this.state.context?.element?.id ?? this.elementId).slice(0, 8)} +

+
+
+ `; } - - if ((this.actor.getSnapshot().value as string) == 'Error') { - secondaryText = 'Error'; - secondaryTextTitle = this._error ?? ''; - } - - return html`
- ${primaryText} - - ${secondaryText} - -
`; + return html``; } } diff --git a/src/Component/Default/index.ts b/src/Component/Default/index.ts index 7e276b4..ca74176 100644 --- a/src/Component/Default/index.ts +++ b/src/Component/Default/index.ts @@ -1,6 +1,5 @@ -export * from './EmberNexusDefaultIcon'; -export * from './EmberNexusDefaultPill'; -export * from './EmberNexusDefaultCard'; -export * from './EmberNexusDefaultThumbnail'; -export * from './EmberNexusDefaultFrameless'; -export * from './EmberNexusDefaultInlineText'; +export * from './EmberNexusDefaultCard.js'; +export * from './EmberNexusDefaultIcon.js'; +export * from './EmberNexusDefaultInlineText.js'; +export * from './EmberNexusDefaultPill.js'; +export * from './EmberNexusDefaultThumbnail.js'; diff --git a/src/Component/Graph/GraphCard.ts b/src/Component/Graph/GraphCard.ts new file mode 100644 index 0000000..8501da6 --- /dev/null +++ b/src/Component/Graph/GraphCard.ts @@ -0,0 +1,115 @@ +import { Graph } from '@antv/g6'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { query } from 'lit/decorators/query.js'; +import { customElement } from 'lit/decorators.js'; +import { SnapshotFrom } from 'xstate'; + +import { + ThemeVariables, + withGetServiceResolverMachine, + withThemeVariables, + withUpdateOnThemeChange, +} from '../../Decorator/index.js'; +import { getServiceResolverMachine } from '../../Machine/index.js'; +import { ThemeService } from '../../Service/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-graph-card') +@withUpdateOnThemeChange() +@withGetServiceResolverMachine() +@withThemeVariables() +class GraphCard extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + themeVariables: ThemeVariables | undefined; + + state: SnapshotFrom; + + @query('#g6Root') + g6Root; + + graph: Graph; + + delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + firstUpdated(): void { + const { width, height } = this.g6Root.getBoundingClientRect(); + this.delay(500).then(() => { + this.graph = new Graph({ + container: this.g6Root, + width: width, + height: height, + data: { + nodes: new Array(10).fill(0).map((_, i) => ({ id: `${i}`, label: `${i}` })), + edges: [ + { source: '0', target: '1' }, + { source: '0', target: '2' }, + { source: '0', target: '3' }, + { source: '0', target: '4' }, + { source: '0', target: '5' }, + { source: '0', target: '7' }, + { source: '0', target: '8' }, + { source: '0', target: '9' }, + { source: '2', target: '3' }, + { source: '4', target: '5' }, + { source: '4', target: '6' }, + { source: '5', target: '6' }, + ], + }, + node: { + style: { + labelText: (d): string => String(d.label ?? '-'), + labelPlacement: 'center', + labelFill: '#fff', + }, + }, + layout: { + type: 'd3-force', + link: { + distance: 100, + strength: 2, + }, + collide: { + radius: 40, + }, + }, + behaviors: [ + 'drag-canvas', + 'zoom-canvas', + { + type: 'drag-element-force', + fixed: true, + }, + ], + plugins: [ + { + type: 'background', + backgroundColor: this.themeVariables?.['--color-base-200'] ?? '#eff1f5', + }, + ], + }); + + this.graph.render(); + }); + } + + render(): TemplateResult { + this.graph?.updatePlugin({ + key: 'background', + backgroundColor: + this.state.context?.serviceResolver?.getServiceOrFail(ThemeService.identifier)?.getActiveTheme() + ?.cssVariables['color-base-200'] ?? '#eff1f5', + }); + return html` +
+
+

G6.js

+
+
+
+ `; + } +} + +export { GraphCard }; diff --git a/src/Component/Graph/index.ts b/src/Component/Graph/index.ts new file mode 100644 index 0000000..cda407a --- /dev/null +++ b/src/Component/Graph/index.ts @@ -0,0 +1 @@ +export * from './GraphCard.js'; diff --git a/src/Component/MockElementDataProvider.ts b/src/Component/MockElementDataProvider.ts deleted file mode 100644 index 1eaeca0..0000000 --- a/src/Component/MockElementDataProvider.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { GetElementEvent } from '@ember-nexus/web-sdk/BrowserEvent/Element'; -import { LitElement, TemplateResult, html } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('ember-nexus-mock-element-data-provider') -class EmberNexusMockElementDataProvider extends LitElement { - @property({ type: String, attribute: 'element-id' }) - elementId: string; - - @property({ type: String, attribute: 'element-data' }) - elementData: string; - - constructor() { - super(); - } - - handleGetElementEvent(event: GetElementEvent): void { - if (event.getElementId() != this.elementId) { - return; - } - const elementRawData = JSON.parse(this.elementData); - elementRawData['id'] = this.elementId; - event.setElement(elementRawData); - event.stopPropagation(); - } - - connectedCallback(): void { - super.connectedCallback(); - this.addEventListener('ember-nexus-sdk-get-element', this.handleGetElementEvent); - } - - disconnectedCallback(): void { - this.removeEventListener('ember-nexus-sdk-get-element', this.handleGetElementEvent); - super.disconnectedCallback(); - } - - render(): TemplateResult { - return html``; - } -} - -export { EmberNexusMockElementDataProvider }; diff --git a/src/Component/Task/EmberNexusTaskCard.ts b/src/Component/Task/EmberNexusTaskCard.ts new file mode 100644 index 0000000..19e723f --- /dev/null +++ b/src/Component/Task/EmberNexusTaskCard.ts @@ -0,0 +1,80 @@ +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { Circle, CircleCheckBig, CircleQuestionMark, CircleSlash, TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; + +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-task-card') +@withGetElementMachine() +class EmberNexusTaskCard extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + render(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + const errorName = this.state.context?.error?.constructor?.name ?? 'Error'; + const errorDescription = (this.state.context?.error as Error)?.message ?? String(this.state.context?.error); + return html` +
+
+ ${unsafeHTML(TriangleAlert)} +
+
+

${errorName}

+

${this.state.context?.element?.id ?? this.elementId}

+

${errorDescription}

+
+
+ `; + case getElementMachineTags.Loading: + return html` +
+
+

Loading

+

${this.state.context?.element?.id ?? this.elementId}

+
+
+ `; + case getElementMachineTags.Loaded: + let icon: string; + switch (this.state.context?.element?.data?.status) { + case 'todo': + icon = Circle; + break; + case 'blocked': + icon = CircleSlash; + break; + case 'done': + icon = CircleCheckBig; + break; + default: + icon = CircleQuestionMark; + } + return html` +
+
+

+ ${unsafeHTML(icon)} + ${this.state.context?.element?.data?.title ?? 'unknown title'} +

+
+
+ `; + } + } +} + +export { EmberNexusTaskCard }; diff --git a/src/Component/Task/EmberNexusTaskIcon.ts b/src/Component/Task/EmberNexusTaskIcon.ts new file mode 100644 index 0000000..ed42485 --- /dev/null +++ b/src/Component/Task/EmberNexusTaskIcon.ts @@ -0,0 +1,64 @@ +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; + +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-task-icon') +@withGetElementMachine() +class EmberNexusTaskIcon extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + renderIconContent(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` +
+ ${unsafeHTML(TriangleAlert)} +
+ `; + case getElementMachineTags.Loading: + return html` +
+ +
+ `; + case getElementMachineTags.Loaded: + let identifier = this.state.context?.element?.data?.name; + if (identifier) { + identifier = String(identifier).toUpperCase().slice(0, 2); + } else { + identifier = this.state.context?.element?.id ?? this.elementId; + identifier = String(identifier).toUpperCase().slice(0, 2); + identifier = html`${identifier}`; + } + return html` +
+ ${identifier} +
+ `; + } + } + + render(): TemplateResult { + return html`
${this.renderIconContent()}
`; + } +} + +export { EmberNexusTaskIcon }; diff --git a/src/Component/Task/EmberNexusTaskInlineText.ts b/src/Component/Task/EmberNexusTaskInlineText.ts new file mode 100644 index 0000000..7af3321 --- /dev/null +++ b/src/Component/Task/EmberNexusTaskInlineText.ts @@ -0,0 +1,40 @@ +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { SnapshotFrom } from 'xstate'; + +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-task-inline-text') +@withGetElementMachine() +class EmberNexusTaskInlineText extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + render(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` Error `; + case getElementMachineTags.Loading: + return html` Loading `; + case getElementMachineTags.Loaded: + let name = this.state.context?.element?.data?.name; + if (!name) { + const namePart = (this.state.context?.element?.id ?? this.elementId).slice(0, 8); + name = html`${namePart} (${this.state.context?.element?.type})`; + } + return html` Task ${name} `; + } + } +} + +export { EmberNexusTaskInlineText }; diff --git a/src/Component/Task/EmberNexusTaskPill.ts b/src/Component/Task/EmberNexusTaskPill.ts new file mode 100644 index 0000000..2b85a5f --- /dev/null +++ b/src/Component/Task/EmberNexusTaskPill.ts @@ -0,0 +1,49 @@ +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { SnapshotFrom } from 'xstate'; + +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-task-pill') +@withGetElementMachine() +class EmberNexusTaskPill extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + render(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + return html` +
+ Error +
+ `; + case getElementMachineTags.Loading: + return html`
Loading
`; + case getElementMachineTags.Loaded: + let name = this.state.context?.element?.data?.name; + if (!name) { + const namePart = (this.state.context?.element?.id ?? this.elementId).slice(0, 8); + name = html`${namePart} (${this.state.context?.element?.type})`; + } + return html` +
Task ${name}
+ `; + } + } +} + +export { EmberNexusTaskPill }; diff --git a/src/Component/Task/EmberNexusTaskThumbnail.ts b/src/Component/Task/EmberNexusTaskThumbnail.ts new file mode 100644 index 0000000..a3a6870 --- /dev/null +++ b/src/Component/Task/EmberNexusTaskThumbnail.ts @@ -0,0 +1,66 @@ +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { TriangleAlert } from 'lucide-static'; +import { SnapshotFrom } from 'xstate'; + +import { withGetElementMachine } from '../../Decorator/withGetElementMachine.js'; +import { getElementMachine, getElementMachineTags } from '../../Machine/index.js'; +import { indexStyles } from '../../Style/index.js'; + +@customElement('ember-nexus-task-thumbnail') +@withGetElementMachine() +class EmberNexusTaskThumbnail extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + state: SnapshotFrom; + + get stateTag(): getElementMachineTags { + return [...this.state.tags][0] as getElementMachineTags; + } + + @property({ type: String, attribute: 'element-id' }) + elementId: string; + + render(): TemplateResult { + switch (this.stateTag) { + case getElementMachineTags.Error: + const errorDescription = (this.state.context?.error as Error)?.message ?? String(this.state.context?.error); + return html` +
+
+ ${unsafeHTML(TriangleAlert)} +
+
+

Error

+

${errorDescription}

+
+
+ `; + case getElementMachineTags.Loading: + return html` +
+
+

Loading

+
+
+ `; + case getElementMachineTags.Loaded: + return html` +
+
+

+ ${this.state.context?.element?.data?.name ?? 'unknown name'} +

+

+ ${(this.state.context?.element?.id ?? this.elementId).slice(0, 8)}, Task +

+
+
+ `; + } + return html``; + } +} + +export { EmberNexusTaskThumbnail }; diff --git a/src/Component/Task/index.ts b/src/Component/Task/index.ts new file mode 100644 index 0000000..cdb70a0 --- /dev/null +++ b/src/Component/Task/index.ts @@ -0,0 +1,5 @@ +export * from './EmberNexusTaskCard.js'; +export * from './EmberNexusTaskIcon.js'; +export * from './EmberNexusTaskInlineText.js'; +export * from './EmberNexusTaskPill.js'; +export * from './EmberNexusTaskThumbnail.js'; diff --git a/src/Component/index.ts b/src/Component/index.ts index 42d2e0c..098d651 100644 --- a/src/Component/index.ts +++ b/src/Component/index.ts @@ -1,2 +1,4 @@ -export * as Default from './Default'; -export * from './MockElementDataProvider'; +export * as Debug from './Debug/index.js'; +export * as Default from './Default/index.js'; +export * as Graph from './Graph/index.js'; +export * as Task from './Task/index.js'; diff --git a/src/Decorator/index.ts b/src/Decorator/index.ts new file mode 100644 index 0000000..edc6319 --- /dev/null +++ b/src/Decorator/index.ts @@ -0,0 +1,8 @@ +export * from './withDescription.js'; +export * from './withGetElementMachine.js'; +export * from './withGetServiceResolverMachine.js'; +export * from './withServiceResolver.js'; +export * from './withStateMachine.js'; +export * from './withThemeVariables.js'; +export * from './withTranslation.js'; +export * from './withUpdateOnThemeChange.js'; diff --git a/src/Decorator/withDescription.ts b/src/Decorator/withDescription.ts new file mode 100644 index 0000000..ed382a3 --- /dev/null +++ b/src/Decorator/withDescription.ts @@ -0,0 +1,12 @@ +import { Constructor } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withDescription(description: string): (Base: TBase) => any { + return function (Base: TBase): any { + return class extends Base { + description = description; + }; + }; +} + +export { withDescription }; diff --git a/src/Decorator/withGetElementMachine.ts b/src/Decorator/withGetElementMachine.ts new file mode 100644 index 0000000..75e3928 --- /dev/null +++ b/src/Decorator/withGetElementMachine.ts @@ -0,0 +1,55 @@ +import { Uuid } from '@ember-nexus/app-core/Type/Definition'; +import { PropertyValues } from '@lit/reactive-element'; +import { ActorRefFrom, SnapshotFrom, createActor } from 'xstate'; + +import { getElementMachine } from '../Machine/index.js'; +import { Constructor, ElementCapableWebComponent } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withGetElementMachine(): >(Base: TBase) => any { + return function >(Base: TBase): any { + return class extends Base { + actor: ActorRefFrom; + state: SnapshotFrom; + send: ActorRefFrom['send']; + + connectedCallback(): void { + super.connectedCallback?.(); + + this.actor = createActor(getElementMachine, { + input: { + elementId: this.elementId, + htmlElement: this, + }, + }); + this.state = this.actor.getSnapshot(); + this.send = this.actor.send; + + this.actor.subscribe((snapshot) => { + this.state = snapshot; + this.requestUpdate?.(); + }); + + this.actor.start(); + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + this.actor.stop(); + } + + updated(changedProperties: PropertyValues): void { + // @ts-expect-error TS2339 + super.updated?.(); + if (changedProperties.has('elementId')) { + this.actor.send({ + type: 'reset', + elementId: this.elementId as Uuid, + }); + } + } + }; + }; +} + +export { withGetElementMachine }; diff --git a/src/Decorator/withGetServiceResolverMachine.ts b/src/Decorator/withGetServiceResolverMachine.ts new file mode 100644 index 0000000..2ea5d4d --- /dev/null +++ b/src/Decorator/withGetServiceResolverMachine.ts @@ -0,0 +1,41 @@ +import { ActorRefFrom, SnapshotFrom, createActor } from 'xstate'; + +import { getServiceResolverMachine } from '../Machine/index.js'; +import { Constructor, LifecycleCapableWebComponent } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withGetServiceResolverMachine(): >( + Base: TBase, +) => any { + return function >(Base: TBase): any { + return class extends Base { + actor: ActorRefFrom; + state: SnapshotFrom; + + connectedCallback(): void { + super.connectedCallback?.(); + + this.actor = createActor(getServiceResolverMachine, { + input: { + htmlElement: this, + }, + }); + this.state = this.actor.getSnapshot(); + + this.actor.subscribe((snapshot) => { + this.state = snapshot; + this.requestUpdate?.(); + }); + + this.actor.start(); + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + this.actor.stop(); + } + }; + }; +} + +export { withGetServiceResolverMachine }; diff --git a/src/Decorator/withServiceResolver.ts b/src/Decorator/withServiceResolver.ts new file mode 100644 index 0000000..dc43865 --- /dev/null +++ b/src/Decorator/withServiceResolver.ts @@ -0,0 +1,55 @@ +import { GetServiceResolverEvent } from '@ember-nexus/app-core/BrowserEvent'; +import { ServiceResolver } from '@ember-nexus/app-core/Service'; + +import { delay } from '../Helper/index.js'; +import { Constructor, LifecycleCapableWebComponent } from '../Type/Definition/index.js'; +import { maxRetryAttempts, retryTimeoutMinMilliseconds, retryTimeoutVariance } from '../Type/index.js'; + +async function resolveService(element: HTMLElement): Promise { + let attempt = 0; + while (attempt < maxRetryAttempts) { + const getServiceResolverEvent = new GetServiceResolverEvent(); + element.dispatchEvent(getServiceResolverEvent); + const serviceResolver = getServiceResolverEvent.getServiceResolver(); + if (serviceResolver !== null) { + return serviceResolver; + } + + const timeToWaitInMilliseconds = Math.round( + (1 << attempt) * + retryTimeoutMinMilliseconds * + (Math.random() * 2 * retryTimeoutVariance + 1 - retryTimeoutVariance), + ); + await delay(timeToWaitInMilliseconds); + + attempt++; + } + + // logger service can not be used here, as service resolver is not available -> we can not resolve the logger + throw new Error('Service resolution failed after max retries.'); +} + +function hasRefreshData(obj: unknown): obj is { refreshData: () => void } { + return typeof (obj as any).refreshData === 'function'; +} + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withServiceResolver(): >(Base: TBase) => any { + return function >(Base: TBase): any { + return class extends Base { + serviceResolver: ServiceResolver; + + connectedCallback(): void { + super.connectedCallback?.(); + resolveService(this).then((serviceResolver: ServiceResolver) => { + this.serviceResolver = serviceResolver; + if (hasRefreshData(this)) { + this.refreshData(); + } + }); + } + }; + }; +} + +export { resolveService, withServiceResolver }; diff --git a/src/Decorator/withStateMachine.ts b/src/Decorator/withStateMachine.ts new file mode 100644 index 0000000..ebc8d9a --- /dev/null +++ b/src/Decorator/withStateMachine.ts @@ -0,0 +1,38 @@ +import { AnyStateMachine, createActor } from 'xstate'; + +import { Constructor, LifecycleCapableWebComponent } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withStateMachine( + machine: AnyStateMachine, +): >(Base: TBase) => any { + return function >(Base: TBase): any { + return class extends Base { + actor = createActor(machine, { + input: { + htmlElement: this, + }, + }); + state = this.actor.getSnapshot(); + send = this.actor.send; + + connectedCallback(): void { + super.connectedCallback?.(); + + this.actor.subscribe((snapshot) => { + this.state = snapshot; + this.requestUpdate?.(); + }); + + this.actor.start(); + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + this.actor.stop(); + } + }; + }; +} + +export { withStateMachine }; diff --git a/src/Decorator/withThemeVariables.ts b/src/Decorator/withThemeVariables.ts new file mode 100644 index 0000000..c55228c --- /dev/null +++ b/src/Decorator/withThemeVariables.ts @@ -0,0 +1,65 @@ +import { Constructor, LifecycleCapableWebComponent } from '../Type/Definition/index.js'; + +const THEME_VARIABLE_KEYS = [ + '--color-base-100', + '--color-base-200', + '--color-base-300', + + '--color-base-content', + '--color-primary', + '--color-primary-content', + '--color-secondary', + '--color-secondary-content', + '--color-accent', + '--color-accent-content', + '--color-neutral', + '--color-neutral-content', + + '--color-info', + '--color-info-content', + '--color-success', + '--color-success-content', + '--color-warning', + '--color-warning-content', + '--color-error', + '--color-error-content', + + '--radius-selector', + '--radius-field', + '--radius-box', + '--size-selector', + '--size-field', + '--border', + '--depth', + '--noise', + '--shiki-theme', +] as const; + +type ThemeVariables = Record<(typeof THEME_VARIABLE_KEYS)[number], string>; + +function readThemeVariables(element: Element): ThemeVariables { + const computed = getComputedStyle(element); + const result = {} as ThemeVariables; + + for (const key of THEME_VARIABLE_KEYS) { + result[key] = computed.getPropertyValue(key).trim(); + } + + return result; +} + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withThemeVariables(): >(Base: TBase) => any { + return function >(Base: TBase): any { + return class extends Base { + themeVariables: ThemeVariables | undefined = undefined; + + connectedCallback(): void { + this.themeVariables = readThemeVariables(this); + super.connectedCallback?.(); + } + }; + }; +} + +export { THEME_VARIABLE_KEYS, ThemeVariables, withThemeVariables }; diff --git a/src/Decorator/withTranslation.ts b/src/Decorator/withTranslation.ts new file mode 100644 index 0000000..4113347 --- /dev/null +++ b/src/Decorator/withTranslation.ts @@ -0,0 +1,40 @@ +import { EventDispatcher, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { EventInterface } from '@ember-nexus/app-core/Type/Definition'; + +import { LanguageChangeEvent } from '../Event/index.js'; +import { TranslationService } from '../Service/index.js'; +import { Constructor, TranslationCapableWebComponent } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withTranslation(): >(Base: TBase) => any { + return function >(Base: TBase): any { + return class extends Base { + private eventDispatcher: EventDispatcher; + + onServiceResolverLoaded(serviceResolver: ServiceResolver): void { + this.eventDispatcher = serviceResolver.getServiceOrFail(EventDispatcher.identifier); + this.eventDispatcher.addListener(LanguageChangeEvent.identifier, this); + this.i18n = serviceResolver + .getServiceOrFail(TranslationService.identifier) + .getI18nInstance(); + super.onServiceResolverLoaded?.(serviceResolver); + } + + onEvent(event: EventInterface): void { + if (event.getIdentifier() === LanguageChangeEvent.identifier) { + this.requestUpdate?.(); + return; + } + // @ts-expect-error TS2339 + super.onEvent?.(); + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + this.eventDispatcher?.removeListener(LanguageChangeEvent.identifier, this); + } + }; + }; +} + +export { withTranslation }; diff --git a/src/Decorator/withUpdateOnThemeChange.ts b/src/Decorator/withUpdateOnThemeChange.ts new file mode 100644 index 0000000..34f75eb --- /dev/null +++ b/src/Decorator/withUpdateOnThemeChange.ts @@ -0,0 +1,38 @@ +import { EventDispatcher, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { EventInterface } from '@ember-nexus/app-core/Type/Definition'; + +import { ThemeChangeEvent } from '../Event/index.js'; +import { Constructor, ServiceResolverCapableWebComponent } from '../Type/Definition/index.js'; + +/* eslint @typescript-eslint/no-explicit-any: "off" */ +function withUpdateOnThemeChange(): >( + Base: TBase, +) => any { + return function >(Base: TBase): any { + return class extends Base { + private eventDispatcher: EventDispatcher; + + onServiceResolverLoaded(serviceResolver: ServiceResolver): void { + this.eventDispatcher = serviceResolver.getServiceOrFail(EventDispatcher.identifier); + this.eventDispatcher.addListener(ThemeChangeEvent.identifier, this); + super.onServiceResolverLoaded?.(serviceResolver); + } + + onEvent(event: EventInterface): void { + if (event.getIdentifier() === ThemeChangeEvent.identifier) { + this.requestUpdate?.(); + return; + } + // @ts-expect-error TS2339 + super.onEvent?.(); + } + + disconnectedCallback(): void { + super.disconnectedCallback?.(); + this.eventDispatcher?.removeListener(ThemeChangeEvent.identifier, this); + } + }; + }; +} + +export { withUpdateOnThemeChange }; diff --git a/src/Event/LanguageChangeEvent.ts b/src/Event/LanguageChangeEvent.ts new file mode 100644 index 0000000..3bf2d14 --- /dev/null +++ b/src/Event/LanguageChangeEvent.ts @@ -0,0 +1,24 @@ +import { Event } from '@ember-nexus/app-core/Type/Definition'; + +import { EventIdentifier } from '../Type/Enum/index.js'; + +class LanguageChangeEvent extends Event { + static identifier: EventIdentifier = EventIdentifier.languageChangeEvent; + + constructor(private language: string) { + super(LanguageChangeEvent.identifier); + } + + getLanguage(): string { + return this.language; + } + + /** + * this event can not be stopped + */ + stopPropagation(): this { + return this; + } +} + +export { LanguageChangeEvent }; diff --git a/src/Event/ThemeChangeEvent.ts b/src/Event/ThemeChangeEvent.ts new file mode 100644 index 0000000..8f586aa --- /dev/null +++ b/src/Event/ThemeChangeEvent.ts @@ -0,0 +1,24 @@ +import { Event } from '@ember-nexus/app-core/Type/Definition'; + +import { EventIdentifier } from '../Type/Enum/index.js'; + +class ThemeChangeEvent extends Event { + static identifier: EventIdentifier = EventIdentifier.themeChangeEvent; + + constructor(private theme: string) { + super(ThemeChangeEvent.identifier); + } + + getTheme(): string { + return this.theme; + } + + /** + * this event can not be stopped + */ + stopPropagation(): this { + return this; + } +} + +export { ThemeChangeEvent }; diff --git a/src/Event/index.ts b/src/Event/index.ts new file mode 100644 index 0000000..35a9c2e --- /dev/null +++ b/src/Event/index.ts @@ -0,0 +1,2 @@ +export * from './LanguageChangeEvent.js'; +export * from './ThemeChangeEvent.js'; diff --git a/src/Helper/ColorHelper.ts b/src/Helper/ColorHelper.ts index 060dcc5..278d450 100644 --- a/src/Helper/ColorHelper.ts +++ b/src/Helper/ColorHelper.ts @@ -1,10 +1,6 @@ -import { Node, Relation } from '@ember-nexus/web-sdk/Type/Definition'; +import { Node, Relation } from '@ember-nexus/app-core/Type/Definition'; import ColorHash from 'color-hash'; -function getColorFromElementOrId(id: string, element: null | Node | Relation): string { - return getColorFromElement(element) ?? getColorFromType(element?.type ?? null) ?? getColorFromId(id); -} - function getColorFromElement(element: null | Node | Relation): string | null { const color = element?.data?.color; if (color) { @@ -14,7 +10,7 @@ function getColorFromElement(element: null | Node | Relation): string | null { } function getColorFromType(type: null | string): string | null { - if (type == null) { + if (type === null) { return null; } return new ColorHash().hex(type); @@ -24,4 +20,8 @@ function getColorFromId(id: string): string { return new ColorHash().hex(id); } -export { getColorFromElementOrId, getColorFromElement, getColorFromType, getColorFromId }; +function getColorFromElementOrId(id: string, element: null | Node | Relation): string { + return getColorFromElement(element) ?? getColorFromType(element?.type ?? null) ?? getColorFromId(id); +} + +export { getColorFromElement, getColorFromElementOrId, getColorFromId, getColorFromType }; diff --git a/src/Helper/IconHelper.ts b/src/Helper/IconHelper.ts index ab3c4cc..56c6ba4 100644 --- a/src/Helper/IconHelper.ts +++ b/src/Helper/IconHelper.ts @@ -1,4 +1,4 @@ -import { Node, Relation } from '@ember-nexus/web-sdk/Type/Definition'; +import { Node, Relation } from '@ember-nexus/app-core/Type/Definition'; import { TemplateResult, html } from 'lit'; function getIconForElement(element: Node | Relation): TemplateResult { diff --git a/src/Helper/NameHelper.ts b/src/Helper/NameHelper.ts index 3976ede..1b16d31 100644 --- a/src/Helper/NameHelper.ts +++ b/src/Helper/NameHelper.ts @@ -1,6 +1,6 @@ -import { Node, Relation } from '@ember-nexus/web-sdk/Type/Definition'; +import { Node, Relation } from '@ember-nexus/app-core/Type/Definition'; -import { initialsLength, nameMaxLength } from '../Type'; +import { initialsLength, nameMaxLength } from '../Type/index.js'; function getNameFromElementOrId(id: string, element: null | Node | Relation): string { const name = element?.data?.name; @@ -34,4 +34,4 @@ function getNameOrFirstLettersFromIdFromElementOrId(id: string, element: null | return String(name); } -export { getNameFromElementOrId, getInitialsFromElementOrId, getNameOrFirstLettersFromIdFromElementOrId }; +export { getInitialsFromElementOrId, getNameFromElementOrId, getNameOrFirstLettersFromIdFromElementOrId }; diff --git a/src/Helper/TitleHelper.ts b/src/Helper/TitleHelper.ts index 5bf5cda..3915cfa 100644 --- a/src/Helper/TitleHelper.ts +++ b/src/Helper/TitleHelper.ts @@ -1,7 +1,7 @@ -import { Node, Relation } from '@ember-nexus/web-sdk/Type/Definition'; +import { Node, Relation } from '@ember-nexus/app-core/Type/Definition'; -import { getNameFromElementOrId } from './NameHelper'; -import { titleTypeMaxLength } from '../Type'; +import { getNameFromElementOrId } from './NameHelper.js'; +import { titleTypeMaxLength } from '../Type/index.js'; function getTitleFromElementOrId(id: string, element: null | Node | Relation): string { const name = getNameFromElementOrId(id, element); diff --git a/src/Helper/delay.ts b/src/Helper/delay.ts new file mode 100644 index 0000000..e452089 --- /dev/null +++ b/src/Helper/delay.ts @@ -0,0 +1,4 @@ +const delay = (delayInMilliseconds: number): Promise => + new Promise((resolve) => setTimeout(resolve, delayInMilliseconds)); + +export { delay }; diff --git a/src/Helper/index.ts b/src/Helper/index.ts index bbb19b3..9e4e1e8 100644 --- a/src/Helper/index.ts +++ b/src/Helper/index.ts @@ -1,5 +1,6 @@ -export * from './ColorHelper'; -export * from './IconHelper'; -export * from './NameHelper'; -export * from './TextContrastOptimizer'; -export * from './TitleHelper'; +export * from './ColorHelper.js'; +export * from './delay.js'; +export * from './IconHelper.js'; +export * from './NameHelper.js'; +export * from './TextContrastOptimizer.js'; +export * from './TitleHelper.js'; diff --git a/src/Machine/Page/index.ts b/src/Machine/Page/index.ts new file mode 100644 index 0000000..b3a6287 --- /dev/null +++ b/src/Machine/Page/index.ts @@ -0,0 +1 @@ +export * from './loginPageMachine.js'; diff --git a/src/Machine/Page/loginPageMachine.ts b/src/Machine/Page/loginPageMachine.ts new file mode 100644 index 0000000..8cd759e --- /dev/null +++ b/src/Machine/Page/loginPageMachine.ts @@ -0,0 +1,170 @@ +import { ApiConfiguration, ApiWrapper, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { Token, UniqueUserIdentifier } from '@ember-nexus/app-core/Type/Definition'; +import { assign, fromPromise, setup } from 'xstate'; + +import { resolveService } from '../../Decorator/index.js'; +import { LifecycleCapableWebComponent } from '../../Type/Definition/index.js'; + +type HTMLElementWithOptionalOnServiceResolverLoaded = LifecycleCapableWebComponent & { + onServiceResolverLoaded?(serviceResolver: ServiceResolver): void; +}; + +const enum loginPageMachineTags { + Loading = 'LOADING', + WaitingForFormEdit = 'WAITING_FOR_FORM_EDIT', + SubmittingForm = 'SUBMITTING_FORM', + Error = 'ERROR', + LoginSuccessful = 'LOGIN_SUCCESSFUL', +} + +const loginPageMachine = setup({ + actors: { + getServiceResolver: fromPromise< + ServiceResolver, + { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + } + >(({ input }) => { + return resolveService(input.htmlElement); + }), + postToken: fromPromise< + Token, + { + uniqueUserIdentifier: UniqueUserIdentifier; + password: string; + serviceResolver: ServiceResolver; + } + >(({ input }) => { + const apiWrapper = input.serviceResolver.getServiceOrFail(ApiWrapper.identifier); + const promise = apiWrapper.postToken(input.uniqueUserIdentifier, input.password); + return promise; + }), + }, + types: { + context: {} as { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + uniqueUserIdentifier: UniqueUserIdentifier | string; + password: string; + error: null | unknown; + serviceResolver: null | ServiceResolver; + }, + input: {} as { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + }, + events: {} as + | { type: 'formClear' } + | { type: 'formUpdate'; uniqueUserIdentifier: UniqueUserIdentifier | string; password: string } + | { type: 'formSubmit' }, + }, +}).createMachine({ + id: 'login-page-machine', + context: ({ input }) => ({ + htmlElement: input.htmlElement, + uniqueUserIdentifier: '', + password: '', + element: null, + error: null, + serviceResolver: null, + }), + initial: 'Initial', + states: { + Initial: { + tags: [loginPageMachineTags.Loading], + always: [ + { + target: 'GetServiceResolver', + }, + ], + }, + GetServiceResolver: { + tags: [loginPageMachineTags.Loading], + invoke: { + src: 'getServiceResolver', + input: ({ context }) => ({ + htmlElement: context.htmlElement, + }), + onDone: { + target: 'WaitingForFormUpdate', + actions: [ + assign({ + serviceResolver: ({ event }) => event.output, + }), + ({ context }): void => { + context.htmlElement.onServiceResolverLoaded?.(context.serviceResolver!); + }, + ], + }, + onError: { + target: 'UnrecoverableError', + actions: assign({ + error: ({ event }) => event.error, + }), + }, + }, + }, + WaitingForFormUpdate: { + tags: [loginPageMachineTags.WaitingForFormEdit], + on: { + formClear: { + actions: [ + assign({ + uniqueUserIdentifier: () => '', + password: () => '', + }), + ({ context }): void => { + context.htmlElement.requestUpdate?.(); + }, + ], + target: 'WaitingForFormUpdate', + }, + formUpdate: { + actions: assign({ + uniqueUserIdentifier: ({ event }) => event.uniqueUserIdentifier, + password: ({ event }) => event.password, + }), + target: 'WaitingForFormUpdate', + }, + formSubmit: { + target: 'SubmittingForm', + }, + }, + }, + SubmittingForm: { + tags: [loginPageMachineTags.SubmittingForm], + entry: assign({ + error: null, + }), + invoke: { + src: 'postToken', + // @ts-expect-error error description + input: ({ context }) => ({ + serviceResolver: context.serviceResolver, + uniqueUserIdentifier: context.uniqueUserIdentifier, + password: context.password, + }), + onDone: { + target: 'LoginSuccessful', + actions: ({ context, event }) => { + const serviceResolver = context.serviceResolver; + const apiConfiguration = serviceResolver?.getServiceOrFail(ApiConfiguration.identifier); + apiConfiguration?.setToken(event.output); + }, + }, + onError: { + target: 'WaitingForFormUpdate', + actions: assign({ + error: ({ event }) => event.error, + }), + }, + }, + }, + LoginSuccessful: { + tags: [loginPageMachineTags.LoginSuccessful], + }, + UnrecoverableError: { + tags: [loginPageMachineTags.Error], + }, + }, +}); + +export { loginPageMachine, loginPageMachineTags }; diff --git a/src/Machine/SingleElementMachine.ts b/src/Machine/SingleElementMachine.ts deleted file mode 100644 index 973958a..0000000 --- a/src/Machine/SingleElementMachine.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { GetElementEvent } from '@ember-nexus/web-sdk/BrowserEvent/Element'; -import { Element, Node, Relation, Uuid, uuidv4Regex } from '@ember-nexus/web-sdk/Type/Definition'; -import { assign, fromPromise, setup } from 'xstate'; - -import { maxRetryAttempts, retryTimeoutMinMilliseconds, retryTimeoutVariance } from '../Type'; - -export const singleElementMachine = setup({ - actors: { - loadElement: fromPromise( - async ({ input }) => { - const event = new GetElementEvent(input.elementId!); - input.htmlElement.dispatchEvent(event); - const getElementResult = event.getElement(); - if (getElementResult == null) { - return Promise.reject('Unable to get Ember Nexus Web SDK events handled.'); - } - return getElementResult; - }, - ), - }, - delays: { - retryTimeout: ({ context }) => { - // exponential backoff with ± 10% random variance - return Math.round( - (1 << context.retryAttempts) * - retryTimeoutMinMilliseconds * - (Math.random() * 2 * retryTimeoutVariance + 1 - retryTimeoutVariance), - ); - }, - }, - guards: { - isValidElementId: ({ context }) => { - if (context.elementId == null) { - return false; - } - return context.elementId.match(uuidv4Regex) !== null; - }, - isElementIdEmpty: ({ context }) => { - if (context.elementId == null) { - return true; - } - return context.elementId == ''; - }, - shouldAttemptRetry: ({ context }) => { - if (context.retryAttempts > maxRetryAttempts) { - return false; - } - return context.error == 'Unable to get Ember Nexus Web SDK events handled.'; - }, - }, - types: { - context: {} as { - elementId: null | Uuid; - element: null | Node | Relation; - error: null | string; - htmlElement: HTMLElement | DocumentFragment; - retryAttempts: number; - }, - input: {} as { - elementId: Uuid; - htmlElement: HTMLElement | DocumentFragment; - }, - events: {} as { type: 'reset'; elementId: Uuid }, - }, -}).createMachine({ - id: 'single-element-machine', - context: ({ input }) => ({ - elementId: input.elementId, - element: null, - error: null, - htmlElement: input.htmlElement, - retryAttempts: 0, - }), - initial: 'Initial', - states: { - Initial: { - entry: assign({ - elementId: ({ context }) => { - return context.elementId; - }, - element: null, - error: null, - retryAttempts: 0, - }), - always: [ - { - guard: 'isValidElementId', - target: 'Loading', - }, - { - guard: 'isElementIdEmpty', - target: 'Error', - actions: assign({ - error: 'Element id can not be empty.', - }), - }, - { - target: 'Error', - actions: assign({ - error: 'Unable to parse element id as uuid.', - }), - }, - ], - }, - Loading: { - invoke: { - src: 'loadElement', - // @ts-expect-error error description - input: ({ context }) => ({ - elementId: context.elementId, - htmlElement: context.htmlElement, - }), - onDone: { - target: 'Loaded', - actions: assign({ - element: ({ event }) => event.output, - }), - }, - onError: { - target: 'Error', - actions: assign({ - error: ({ event }) => String(event.error), - }), - }, - }, - }, - WaitingForRetry: { - after: { - retryTimeout: { - target: 'Loading', - }, - }, - on: { - reset: { - actions: assign({ - elementId: ({ event }) => event.elementId, - }), - target: 'Initial', - }, - }, - }, - Loaded: { - on: { - reset: { - actions: assign({ - elementId: ({ event }) => event.elementId, - }), - target: 'Initial', - }, - }, - }, - Error: { - on: { - reset: { - actions: assign({ - elementId: ({ event }) => event.elementId, - }), - target: 'Initial', - }, - }, - always: { - target: 'WaitingForRetry', - actions: assign({ - retryAttempts: ({ context }) => context.retryAttempts + 1, - error: null, - }), - guard: 'shouldAttemptRetry', - }, - }, - }, -}); - -export { Element }; diff --git a/src/Machine/getElementMachine.ts b/src/Machine/getElementMachine.ts new file mode 100644 index 0000000..6865b63 --- /dev/null +++ b/src/Machine/getElementMachine.ts @@ -0,0 +1,174 @@ +import { ApiWrapper, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { Node, Relation, Uuid, uuidv4Regex } from '@ember-nexus/app-core/Type/Definition'; +import { assign, fromPromise, setup } from 'xstate'; + +import { resolveService } from '../Decorator/index.js'; + +const enum getElementMachineTags { + Loading = 'LOADING', + Loaded = 'LOADED', + Error = 'ERROR', +} + +type HTMLElementWithOptionalOnServiceResolverLoaded = HTMLElement & { + onServiceResolverLoaded?(serviceResolver: ServiceResolver): void; +}; + +const getElementMachine = setup({ + actors: { + getServiceResolver: fromPromise< + ServiceResolver, + { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + } + >(({ input }) => { + return resolveService(input.htmlElement); + }), + getElement: fromPromise(({ input }) => { + const apiWrapper = input.serviceResolver.getServiceOrFail(ApiWrapper.identifier); + return apiWrapper.getElement(input.elementId); + }), + }, + guards: { + isValidElementId: ({ context }) => String(context.elementId).match(uuidv4Regex) !== null, + isElementIdEmpty: ({ context }) => + context.elementId === null || context.elementId === undefined || context.elementId === '', + }, + types: { + context: {} as { + elementId: null | Uuid; + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + element: null | Node | Relation; + error: null | unknown; + serviceResolver: null | ServiceResolver; + }, + input: {} as { + elementId: Uuid; + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + }, + events: {} as { type: 'reset'; elementId: Uuid }, + }, +}).createMachine({ + id: 'get-element-machine', + context: ({ input }) => ({ + elementId: input.elementId, + htmlElement: input.htmlElement, + element: null, + error: null, + serviceResolver: null, + }), + initial: 'Initial', + states: { + Initial: { + tags: [getElementMachineTags.Loading], + always: [ + { + target: 'GetServiceResolver', + }, + ], + }, + GetServiceResolver: { + tags: [getElementMachineTags.Loading], + invoke: { + src: 'getServiceResolver', + input: ({ context }) => ({ + htmlElement: context.htmlElement, + }), + onDone: { + target: 'ReadyToGetElement', + actions: [ + assign({ + serviceResolver: ({ event }) => event.output, + }), + ({ context }): void => { + context.htmlElement.onServiceResolverLoaded?.(context.serviceResolver!); + }, + ], + }, + onError: { + target: 'UnrecoverableError', + actions: assign({ + error: ({ event }) => event.error, + }), + }, + }, + }, + ReadyToGetElement: { + tags: [getElementMachineTags.Loading], + entry: assign({ + elementId: ({ context }) => context.elementId, + element: null, + error: null, + }), + always: [ + { + guard: 'isValidElementId', + target: 'GetElement', + }, + { + guard: 'isElementIdEmpty', + target: 'RecoverableError', + actions: assign({ + error: 'Element id can not be empty.', + }), + }, + { + target: 'RecoverableError', + actions: assign({ + error: 'Unable to parse element id as uuid.', + }), + }, + ], + }, + GetElement: { + tags: [getElementMachineTags.Loading], + invoke: { + src: 'getElement', + // @ts-expect-error error description + input: ({ context }) => ({ + elementId: context.elementId, + serviceResolver: context.serviceResolver, + }), + onDone: { + target: 'Loaded', + actions: assign({ + element: ({ event }) => event.output, + }), + }, + onError: { + target: 'RecoverableError', + actions: assign({ + error: ({ event }) => event.error, + }), + }, + }, + }, + Loaded: { + tags: [getElementMachineTags.Loaded], + on: { + reset: { + actions: assign({ + elementId: ({ event }) => event.elementId, + }), + target: 'ReadyToGetElement', + }, + }, + }, + RecoverableError: { + tags: [getElementMachineTags.Error], + on: { + reset: { + actions: assign({ + elementId: ({ event }) => event.elementId, + }), + target: 'ReadyToGetElement', + }, + }, + }, + UnrecoverableError: { + tags: [getElementMachineTags.Error], + }, + }, +}); + +export { getElementMachine, getElementMachineTags }; diff --git a/src/Machine/getServiceResolverMachine.ts b/src/Machine/getServiceResolverMachine.ts new file mode 100644 index 0000000..a5ae37b --- /dev/null +++ b/src/Machine/getServiceResolverMachine.ts @@ -0,0 +1,89 @@ +import { ServiceResolver } from '@ember-nexus/app-core/Service'; +import { assign, fromPromise, setup } from 'xstate'; + +import { resolveService } from '../Decorator/index.js'; + +const enum getServiceResolverMachineTags { + Loading = 'LOADING', + Loaded = 'LOADED', + Error = 'ERROR', +} + +type HTMLElementWithOptionalOnServiceResolverLoaded = HTMLElement & { + onServiceResolverLoaded?(serviceResolver: ServiceResolver): void; +}; + +const getServiceResolverMachine = setup({ + actors: { + getServiceResolver: fromPromise< + ServiceResolver, + { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + } + >(({ input }) => { + return resolveService(input.htmlElement); + }), + }, + types: { + context: {} as { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + error: null | unknown; + serviceResolver: null | ServiceResolver; + }, + input: {} as { + htmlElement: HTMLElementWithOptionalOnServiceResolverLoaded; + }, + }, +}).createMachine({ + id: 'get-service-resolver-machine', + context: ({ input }) => ({ + htmlElement: input.htmlElement, + error: null, + serviceResolver: null, + }), + initial: 'Initial', + states: { + Initial: { + tags: [getServiceResolverMachineTags.Loading], + always: [ + { + target: 'GetServiceResolver', + }, + ], + }, + GetServiceResolver: { + tags: [getServiceResolverMachineTags.Loading], + invoke: { + src: 'getServiceResolver', + input: ({ context }) => ({ + htmlElement: context.htmlElement, + }), + onDone: { + target: 'Loaded', + actions: [ + assign({ + serviceResolver: ({ event }) => event.output, + }), + ({ context }): void => { + context.htmlElement.onServiceResolverLoaded?.(context.serviceResolver!); + }, + ], + }, + onError: { + target: 'UnrecoverableError', + actions: assign({ + error: ({ event }) => event.error, + }), + }, + }, + }, + Loaded: { + tags: [getServiceResolverMachineTags.Loaded], + }, + UnrecoverableError: { + tags: [getServiceResolverMachineTags.Error], + }, + }, +}); + +export { getServiceResolverMachine, getServiceResolverMachineTags }; diff --git a/src/Machine/index.ts b/src/Machine/index.ts index 5c4a9b1..20fae76 100644 --- a/src/Machine/index.ts +++ b/src/Machine/index.ts @@ -1 +1,4 @@ -export * from './SingleElementMachine'; +export * from './getElementMachine.js'; +export * from './getServiceResolverMachine.js'; +export * as Page from './Page/index.js'; +export * from './toggleMachine.js'; diff --git a/src/Machine/toggleMachine.ts b/src/Machine/toggleMachine.ts new file mode 100644 index 0000000..451ba4d --- /dev/null +++ b/src/Machine/toggleMachine.ts @@ -0,0 +1,16 @@ +import { createMachine } from 'xstate'; + +const toggleMachine = createMachine({ + id: 'toggle', + types: {} as { + context: object; + events: { type: 'TOGGLE' }; + }, + initial: 'off', + states: { + off: { on: { TOGGLE: 'on' } }, + on: { on: { TOGGLE: 'off' } }, + }, +}); + +export { toggleMachine }; diff --git a/src/Page/LoginPage.ts b/src/Page/LoginPage.ts new file mode 100644 index 0000000..d2fb39d --- /dev/null +++ b/src/Page/LoginPage.ts @@ -0,0 +1,132 @@ +import { i18n } from 'i18next'; +import { LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { ActorRefFrom, SnapshotFrom } from 'xstate'; + +import { withStateMachine } from '../Decorator/index.js'; +import { withTranslation } from '../Decorator/index.js'; +import { loginPageMachine, loginPageMachineTags } from '../Machine/Page/index.js'; +import { indexStyles } from '../Style/index.js'; + +@customElement('ember-nexus-page-login') +@withTranslation() +@withStateMachine(loginPageMachine) +class LoginPage extends LitElement { + static styles = [unsafeCSS(indexStyles)]; + + i18n: i18n; + + state: SnapshotFrom; + send: ActorRefFrom['send']; + + get stateTag(): loginPageMachineTags { + return [...this.state.tags][0] as loginPageMachineTags; + } + + private onUniqueUserIdentifierChange(event: InputEvent): void { + const target = event.target as EventTarget; + if (!('value' in target)) { + return; + } + this.send({ + type: 'formUpdate', + uniqueUserIdentifier: target.value as string, + password: this.state.context.password, + }); + } + + private onPasswordChange(event: InputEvent): void { + const target = event.target as EventTarget; + if (!('value' in target)) { + return; + } + this.send({ + type: 'formUpdate', + uniqueUserIdentifier: this.state.context.uniqueUserIdentifier, + password: target.value as string, + }); + } + + private onClear(event: Event): void { + event.preventDefault(); + this.send({ + type: 'formClear', + }); + } + + private onSubmit(event: Event): void { + event.preventDefault(); + this.send({ + type: 'formSubmit', + }); + } + + render(): TemplateResult { + let loadingBar: string | TemplateResult = ''; + if (this.stateTag === loginPageMachineTags.SubmittingForm) { + loadingBar = html` + +

Logging in...

+ `; + } + let errorBlock: undefined | TemplateResult; + if (this.state.context.error) { + let errorMessage = this.state.context.error; + if (errorMessage instanceof Error) { + errorMessage = errorMessage.message; + } + errorBlock = html`

${errorMessage}

`; + } + const t = this.i18n?.t; + return html` +
+
+

${t?.('page.login.title') ?? 'Login'}

+
+ ${t?.('page.login.usernameLabel') ?? 'Email or username:'} + +
+
+ ${t?.('page.login.passwordLabel') ?? 'Password:'} + +
+
+ + +
+ ${errorBlock} ${loadingBar} +
+
+ `; + } +} + +export { LoginPage }; diff --git a/src/Page/index.ts b/src/Page/index.ts new file mode 100644 index 0000000..48663da --- /dev/null +++ b/src/Page/index.ts @@ -0,0 +1 @@ +export * from './LoginPage.js'; diff --git a/src/Service/LanguageService.ts b/src/Service/LanguageService.ts new file mode 100644 index 0000000..1354a1c --- /dev/null +++ b/src/Service/LanguageService.ts @@ -0,0 +1,28 @@ +import { EventDispatcher, ServiceResolver } from '@ember-nexus/app-core/Service'; + +import { LanguageChangeEvent } from '../Event/index.js'; +import { ServiceIdentifier } from '../Type/Enum/index.js'; + +class LanguageService { + static identifier: ServiceIdentifier = ServiceIdentifier.languageService; + + private activeLanguage: string; + + constructor(private eventDispatcher: EventDispatcher) {} + + static constructFromServiceResolver(serviceResolver: ServiceResolver): LanguageService { + return new LanguageService(serviceResolver.getServiceOrFail(EventDispatcher.identifier)); + } + + applyLanguage(language: string): this { + this.activeLanguage = language; + this.eventDispatcher.dispatchEvent(new LanguageChangeEvent(this.activeLanguage)); + return this; + } + + getActiveLanguage(): string { + return this.activeLanguage; + } +} + +export { LanguageService }; diff --git a/src/Service/ShikiJsonHighlighterService.ts b/src/Service/ShikiJsonHighlighterService.ts new file mode 100644 index 0000000..ac49514 --- /dev/null +++ b/src/Service/ShikiJsonHighlighterService.ts @@ -0,0 +1,48 @@ +import languageJson from '@shikijs/langs/json'; +import themeAndromeeda from '@shikijs/themes/andromeeda'; +import themeCatppuccinFrappe from '@shikijs/themes/catppuccin-frappe'; +import themeCatppuccinLatte from '@shikijs/themes/catppuccin-latte'; +import { HighlighterCore, ThemeRegistration } from 'shiki'; +import { createHighlighterCore } from 'shiki/core'; +import { createOnigurumaEngine } from 'shiki/engine/oniguruma'; + +import { ServiceIdentifier } from '../Type/Enum/index.js'; + +class ShikiJsonHighlighterService { + static identifier: ServiceIdentifier = ServiceIdentifier.shikiJsonHighlighterService; + static defaultTheme: string = 'catppuccin-latte'; + + private highlighterPromise: HighlighterCore; + + constructor() {} + + static constructFromServiceResolver(): ShikiJsonHighlighterService { + return new ShikiJsonHighlighterService(); + } + + makeAppThemeAware(theme: ThemeRegistration): ThemeRegistration { + return { + ...theme, + colors: { + ...theme.colors, + 'editor.background': 'var(--color-code-background)', + }, + tokenColors: theme.tokenColors?.filter((tokenColor) => !('background' in tokenColor.settings)) || [], + }; + } + + async getShikiHighlighter(): Promise { + if (this.highlighterPromise) { + return this.highlighterPromise; + } + const tmp = this.makeAppThemeAware(themeAndromeeda); + this.highlighterPromise = await createHighlighterCore({ + themes: [this.makeAppThemeAware(themeCatppuccinLatte), this.makeAppThemeAware(themeCatppuccinFrappe), tmp], + langs: [languageJson], + engine: createOnigurumaEngine(import('shiki/wasm')), + }); + return this.highlighterPromise; + } +} + +export { ShikiJsonHighlighterService }; diff --git a/src/Service/ThemeService.ts b/src/Service/ThemeService.ts new file mode 100644 index 0000000..13ba2ec --- /dev/null +++ b/src/Service/ThemeService.ts @@ -0,0 +1,49 @@ +import { EventDispatcher, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { Registry } from '@ember-nexus/app-core/Type/Definition'; + +import { ThemeChangeEvent } from '../Event/index.js'; +import { Theme } from '../Type/Definition/index.js'; +import { ServiceIdentifier } from '../Type/Enum/index.js'; + +class ThemeService { + static identifier: ServiceIdentifier = ServiceIdentifier.themeService; + + public themes: Registry; + private activeTheme: Theme; + private styleSheet: CSSStyleSheet; + + constructor(private eventDispatcher: EventDispatcher) { + this.themes = new Registry(); + this.styleSheet = new CSSStyleSheet(); + } + + static constructFromServiceResolver(serviceResolver: ServiceResolver): ThemeService { + return new ThemeService(serviceResolver.getServiceOrFail(EventDispatcher.identifier)); + } + + applyTheme(themeIdentifier: string): this { + const theme = this.themes.getEntry(themeIdentifier); + if (theme === null) { + return this; + } + this.activeTheme = theme; + const cssVars = Object.entries(theme.cssVariables) + .map(([key, value]) => ` --${key}: ${value};`) + .join('\n'); + const css = `:root {\n${cssVars}\n}`; + + this.styleSheet.replaceSync(css); + this.eventDispatcher.dispatchEvent(new ThemeChangeEvent(theme.name)); + return this; + } + + getStyleSheet(): CSSStyleSheet { + return this.styleSheet; + } + + getActiveTheme(): Theme { + return this.activeTheme; + } +} + +export { ThemeService }; diff --git a/src/Service/TranslationService.ts b/src/Service/TranslationService.ts new file mode 100644 index 0000000..e824fe2 --- /dev/null +++ b/src/Service/TranslationService.ts @@ -0,0 +1,77 @@ +import { EventDispatcher, ServiceResolver } from '@ember-nexus/app-core/Service'; +import { EventInterface } from '@ember-nexus/app-core/Type/Definition'; +import i18next, { i18n } from 'i18next'; + +import { LanguageService } from './LanguageService.js'; +import ar from '../Asset/Translation/ar.json'; +import de from '../Asset/Translation/de.json'; +import en from '../Asset/Translation/en.json'; +import es from '../Asset/Translation/es.json'; +import fr from '../Asset/Translation/fr.json'; +import hi from '../Asset/Translation/hi.json'; +import it from '../Asset/Translation/it.json'; +import ja from '../Asset/Translation/ja.json'; +import ko from '../Asset/Translation/ko.json'; +import no from '../Asset/Translation/no.json'; +import ru from '../Asset/Translation/ru.json'; +import sw from '../Asset/Translation/sw.json'; +import zhCN from '../Asset/Translation/zh-CN.json'; +import { LanguageChangeEvent } from '../Event/index.js'; +import { ServiceIdentifier } from '../Type/Enum/index.js'; + +class TranslationService { + static identifier: ServiceIdentifier = ServiceIdentifier.translationService; + + private i18nInstance: i18n; + + constructor( + languageService: LanguageService, + private eventDispatcher: EventDispatcher, + ) { + this.i18nInstance = i18next.createInstance( + { + lng: languageService.getActiveLanguage(), + debug: true, + fallbackLng: 'en', + resources: { + en: { translation: en }, + de: { translation: de }, + no: { translation: no }, + ko: { translation: ko }, + + ar: { translation: ar }, + es: { translation: es }, + fr: { translation: fr }, + hi: { translation: hi }, + it: { translation: it }, + ja: { translation: ja }, + ru: { translation: ru }, + sw: { translation: sw }, + 'zh-CN': { translation: zhCN }, + }, + }, + () => { + this.eventDispatcher.addListener(LanguageChangeEvent.identifier, this); + }, + ); + } + + static constructFromServiceResolver(serviceResolver: ServiceResolver): TranslationService { + return new TranslationService( + serviceResolver.getServiceOrFail(LanguageService.identifier), + serviceResolver.getServiceOrFail(EventDispatcher.identifier), + ); + } + + onEvent(event: EventInterface): void { + if (event.getIdentifier() === LanguageChangeEvent.identifier) { + this.i18nInstance.changeLanguage((event as LanguageChangeEvent).getLanguage()); + } + } + + getI18nInstance(): i18n { + return this.i18nInstance; + } +} + +export { TranslationService }; diff --git a/src/Service/index.ts b/src/Service/index.ts new file mode 100644 index 0000000..bf3118c --- /dev/null +++ b/src/Service/index.ts @@ -0,0 +1,4 @@ +export * from './LanguageService.js'; +export * from './ShikiJsonHighlighterService.js'; +export * from './ThemeService.js'; +export * from './TranslationService.js'; diff --git a/src/Style/CardComponentStyle.ts b/src/Style/CardComponentStyle.ts deleted file mode 100644 index 1082270..0000000 --- a/src/Style/CardComponentStyle.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { css } from 'lit'; - -const cardComponentStyle = css` - * { - box-sizing: border-box; - } - - :host { - width: 100%; - } - - .card-component { - display: flex; - flex-direction: column; - align-items: stretch; - width: 100%; - - font-weight: 400; - font-size: 1rem; - - color: #fff; - - border-radius: 0.5rem; - padding: 1rem; - } - - span.icon { - display: inline-block; - } - - .icon { - width: 1.5rem; - height: 1.5rem; - } - - .name { - margin: 0; - } - - .info { - display: flex; - gap: 0.5rem; - } -`; - -export { cardComponentStyle }; diff --git a/src/Style/FontStyle.ts b/src/Style/FontStyle.ts deleted file mode 100644 index d51fd84..0000000 --- a/src/Style/FontStyle.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { css } from 'lit'; - -const fontStyle = css` - .font-sans { - font-family: var(--font-sans, 'Roboto'), sans-serif; - } - .font-serif { - font-family: var(--font-serif, 'Noto Serif'), serif; - } - .font-mono { - font-family: var(--font-mono, 'Roboto Mono'), monospace; - } -`; - -export { fontStyle }; diff --git a/src/Style/FramelessComponentStyle.ts b/src/Style/FramelessComponentStyle.ts deleted file mode 100644 index b847b5d..0000000 --- a/src/Style/FramelessComponentStyle.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { css } from 'lit'; - -const framelessComponentStyle = css` - * { - box-sizing: border-box; - } - - :host { - width: 100%; - height: 100%; - } - - .frameless-component { - display: flex; - width: 100%; - height: 100%; - - font-weight: 400; - font-size: 1rem; - - color: #fff; - } - - .icon { - width: 1.5rem; - height: 1.5rem; - } -`; - -export { framelessComponentStyle }; diff --git a/src/Style/IconComponentStyle.ts b/src/Style/IconComponentStyle.ts deleted file mode 100644 index dc3fbfa..0000000 --- a/src/Style/IconComponentStyle.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { css } from 'lit'; - -const iconComponentStyle = css` - * { - box-sizing: border-box; - } - - :host { - aspect-ratio: 1; - min-height: 1.5em; - height: 2em; - container-type: size; - display: inline-block; - } - - .icon-component { - height: 100%; - aspect-ratio: 1; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - } - - .svg-icon { - width: 75%; - height: 75%; - } - - .svg-icon > svg { - display: block; - } - - .text-icon { - font-size: 50cqh; - } -`; - -export { iconComponentStyle }; diff --git a/src/Style/InlineTextComponentStyle.ts b/src/Style/InlineTextComponentStyle.ts deleted file mode 100644 index 7c6e39c..0000000 --- a/src/Style/InlineTextComponentStyle.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { css } from 'lit'; - -const inlineTextComponentStyle = css` - * { - box-sizing: border-box; - } - - .inline-text-component { - display: inline; - } -`; - -export { inlineTextComponentStyle }; diff --git a/src/Style/PillComponentStyle.ts b/src/Style/PillComponentStyle.ts deleted file mode 100644 index 83e13dc..0000000 --- a/src/Style/PillComponentStyle.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { css } from 'lit'; - -const pillComponentStyle = css` - * { - box-sizing: border-box; - } - - :host { - min-height: 1em; - height: 2em; - } - - .pill-component { - height: 100%; - border-radius: 10000px; - display: inline-flex; - gap: 0.5em; - align-items: center; - padding-right: 0.5em; - padding-left: 0.5em; - - font-weight: 400; - - //border: 1px solid gray; - border: 2px solid #e9e9ed; - - background-color: #fff; - - color: #000; - - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - } - - .pill-component.has-icon { - padding-left: 0; - } - - .content { - font-size: 1em; - text-wrap: nowrap; - } - - .svg-icon { - width: 1.5em; - height: 1.5em; - margin-left: 0.25em; - } - - .svg-icon > svg { - display: block; - } -`; - -export { pillComponentStyle }; diff --git a/src/Style/ShadowStyle.ts b/src/Style/ShadowStyle.ts deleted file mode 100644 index f115560..0000000 --- a/src/Style/ShadowStyle.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { css } from 'lit'; - -const shadowStyle = css` - .shadow { - box-shadow: - 0px 1.1px 1.1px rgba(0, 0, 0, 0.07), - 0px 2.8px 2.8px rgba(0, 0, 0, 0.048), - 0px 5.7px 5.7px rgba(0, 0, 0, 0.039), - 0px 11.7px 11.7px rgba(0, 0, 0, 0.031), - 0px 32px 32px rgba(0, 0, 0, 0.022); - } -`; - -export { shadowStyle }; diff --git a/src/Style/ThumbnailComponentStyle.ts b/src/Style/ThumbnailComponentStyle.ts deleted file mode 100644 index 6312b85..0000000 --- a/src/Style/ThumbnailComponentStyle.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { css } from 'lit'; - -const thumbnailComponentStyle = css` - * { - box-sizing: border-box; - } - - :host { - aspect-ratio: 1; - width: min(100%, 8em); - min-width: 4em; - container-type: size; - display: inline-block; - } - - .thumbnail-component { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - aspect-ratio: 1; - gap: 0.5em; - - font-weight: 400; - font-size: 1em; - - color: #fff; - - border-radius: 0.5em; - position: relative; - } - - @container (width < 6em) { - .thumbnail-component { - gap: 0; - } - .thumbnail-component .name { - font-size: 1.25em; - max-width: calc(100% - 0.5em); - } - .thumbnail-component .type { - font-size: 0.85em; - max-width: calc(100% - 0.5em); - } - } - - .name { - font-size: 1.5em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: calc(100% - 1em); - } - - .type { - font-size: 1em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: calc(100% - 1em); - } -`; - -export { thumbnailComponentStyle }; diff --git a/src/Style/TmpStyle.ts b/src/Style/TmpStyle.ts deleted file mode 100644 index a0c8b74..0000000 --- a/src/Style/TmpStyle.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { css } from 'lit'; - -const tmpStyle = css` - :host { - display: block; - container-type: normal; - } - - .card { - padding: 0.75em; - border-radius: 0.75em; - } - - span.icon { - display: inline-block; - } - - .icon { - width: 1.6em; - height: 1.6em; - flex-shrink: 0; - } - - .name { - margin: 0; - font-size: 1.2em; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .info { - display: flex; - flex-direction: column; - font-size: 0.8em; - } - - .info p { - margin: 0; - } - - .description { - display: flex; - gap: 0.5em; - } - - .description p { - margin: 0; - } - - .title { - display: flex; - justify-content: flex-start; - align-items: center; - gap: 0.25em; - margin-bottom: 0.25em; - } - - .font-mono { - font-family: 'Roboto Mono', monospace; - } -`; - -export { tmpStyle }; diff --git a/src/Style/index.css b/src/Style/index.css new file mode 100644 index 0000000..0fe5c56 --- /dev/null +++ b/src/Style/index.css @@ -0,0 +1,133 @@ +@import "tailwindcss"; + +@plugin "daisyui" { + themes: false; +} + +/*@plugin "daisyui" {*/ +/* themes: light --default, dark --prefersdark, emerald, dim;*/ +/*}*/ + +/*@plugin "daisyui/theme" {*/ +/* name: "light";*/ + +/* --color-base-100: oklch(100% 0 0);*/ +/* --color-base-200: oklch(98% 0 0);*/ +/* --color-base-300: oklch(95% 0 0);*/ + +/* --color-base-content: oklch(21% 0.006 285.885);*/ +/* --color-primary: #ff073a;*/ +/* --color-primary-content: #ffffff;*/ +/* --color-secondary: #1a1a1a;*/ +/* --color-secondary-content: #ffffff;*/ +/* --color-accent: oklch(77% 0.152 181.912);*/ +/* --color-accent-content: oklch(38% 0.063 188.416);*/ +/* --color-neutral: oklch(14% 0.005 285.823);*/ +/* --color-neutral-content: oklch(92% 0.004 286.32);*/ + +/* --color-info: #036fe3;*/ +/* --color-info-content: #fff;*/ +/* --color-success: #008539;*/ +/* --color-success-content: #fff;*/ +/* --color-warning: #ffc835;*/ +/* --color-warning-content: #000;*/ +/* --color-error: #da294a;*/ +/* --color-error-content: #000;*/ + +/* --radius-selector: 0.5rem;*/ +/* --radius-field: 0.25rem;*/ +/* --radius-box: 0.5rem;*/ +/* --size-selector: 0.25rem;*/ +/* --size-field: 0.25rem;*/ +/* --border: 1px;*/ +/* --depth: 1;*/ +/* --noise: 0;*/ + +/* --shiki-theme: 'catppuccin-latte';*/ +/* --color-code-background: var(--color-base-200);*/ +/*}*/ +/*@plugin "daisyui/theme" {*/ +/* name: "dark";*/ + +/* --color-base-100: oklch(25.33% 0.016 252.42);*/ +/* --color-base-200: oklch(23.26% 0.014 253.1);*/ +/* --color-base-300: oklch(21.15% 0.012 254.09);*/ + +/* --color-base-content: oklch(97.807% 0.029 256.847);*/ +/* --color-primary: #ff073a;*/ +/* --color-primary-content: oklch(96% 0.018 272.314);*/ +/* --color-secondary: #1a1a1a;*/ +/* --color-secondary-content: oklch(94% 0.028 342.258);*/ +/* --color-accent: oklch(77% 0.152 181.912);*/ +/* --color-accent-content: oklch(38% 0.063 188.416);*/ +/* --color-neutral: oklch(14% 0.005 285.823);*/ +/* --color-neutral-content: oklch(92% 0.004 286.32);*/ + +/* --color-info: oklch(74% 0.16 232.661);*/ +/* --color-info-content: oklch(29% 0.066 243.157);*/ +/* --color-success: oklch(76% 0.177 163.223);*/ +/* --color-success-content: oklch(37% 0.077 168.94);*/ +/* --color-warning: oklch(82% 0.189 84.429);*/ +/* --color-warning-content: oklch(41% 0.112 45.904);*/ +/* --color-error: oklch(71% 0.194 13.428);*/ +/* --color-error-content: oklch(27% 0.105 12.094);*/ + +/* --radius-selector: 0.5rem;*/ +/* --radius-field: 0.25rem;*/ +/* --radius-box: 0.5rem;*/ +/* --size-selector: 0.25rem;*/ +/* --size-field: 0.25rem;*/ +/* --border: 1px;*/ +/* --depth: 1;*/ +/* --noise: 0;*/ + +/* --shiki-theme: 'catppuccin-frappe';*/ +/* --color-code-background: var(--color-base-200);*/ +/*}*/ +/*@plugin "daisyui/theme" {*/ +/* name: "emerald";*/ +/* default: true;*/ +/* --shiki-theme: 'andromeeda';*/ +/* --color-code-background: 'oklch(0.28036 0.019 264.182)';*/ +/*}*/ +/*@plugin "daisyui/theme" {*/ +/* name: "dim";*/ +/* default: true;*/ +/* --shiki-theme: 'catppuccin-frappe';*/ +/* --color-code-background: var(--color-base-200);*/ +/*}*/ + +@theme { + --font-sans: 'Roboto'; + --font-mono: 'Roboto Mono'; +} + +html { + font-size: 20px; +} +html, :root { + box-sizing: border-box; +} +*, *:before, *:after { + box-sizing: inherit; +} + +svg.lucide { + width: 1em; + height: auto; +} + +.thumbnail { + aspect-ratio: 1; + width: min(100%, 8em); + min-width: 4em; + container-type: size; + display: inline-block; +} + +#g6Root, +#g6Root .g6-background, +#g6Root canvas { + border-bottom-left-radius: var(--radius-box); + border-bottom-right-radius: var(--radius-box); +} diff --git a/src/Style/index.ts b/src/Style/index.ts index 8187183..08c3545 100644 --- a/src/Style/index.ts +++ b/src/Style/index.ts @@ -1,9 +1,3 @@ -export * from './IconComponentStyle'; -export * from './CardComponentStyle'; -export * from './ThumbnailComponentStyle'; -export * from './FramelessComponentStyle'; -export * from './PillComponentStyle'; -export * from './ShadowStyle'; -export * from './FontStyle'; -export * from './InlineTextComponentStyle'; -export * from './TmpStyle'; +import indexStyles from './index.css?inline'; + +export { indexStyles }; diff --git a/src/Theme/dark.ts b/src/Theme/dark.ts new file mode 100644 index 0000000..f131abb --- /dev/null +++ b/src/Theme/dark.ts @@ -0,0 +1,43 @@ +import { Theme } from '../Type/Definition/index.js'; + +const dark: Theme = { + name: 'dark', + cssVariables: { + 'color-base-100': 'oklch(25.33% 0.016 252.42)', + 'color-base-200': 'oklch(23.26% 0.014 253.1)', + 'color-base-300': 'oklch(21.15% 0.012 254.09)', + + 'color-base-content': 'oklch(97.807% 0.029 256.847)', + 'color-primary': '#ff073a', + 'color-primary-content': 'oklch(96% 0.018 272.314)', + 'color-secondary': '#1a1a1a', + 'color-secondary-content': 'oklch(94% 0.028 342.258)', + 'color-accent': 'oklch(77% 0.152 181.912)', + 'color-accent-content': 'oklch(38% 0.063 188.416)', + 'color-neutral': 'oklch(14% 0.005 285.823)', + 'color-neutral-content': 'oklch(92% 0.004 286.32)', + + 'color-info': 'oklch(74% 0.16 232.661)', + 'color-info-content': 'oklch(29% 0.066 243.157)', + 'color-success': 'oklch(76% 0.177 163.223)', + 'color-success-content': 'oklch(37% 0.077 168.94)', + 'color-warning': 'oklch(82% 0.189 84.429)', + 'color-warning-content': 'oklch(41% 0.112 45.904)', + 'color-error': 'oklch(71% 0.194 13.428)', + 'color-error-content': 'oklch(27% 0.105 12.094)', + + 'radius-selector': '0.5rem', + 'radius-field': '0.25rem', + 'radius-box': '0.5rem', + 'size-selector': '0.25rem', + 'size-field': '0.25rem', + border: '1px', + depth: '1', + noise: '0', + + 'color-code-background': 'oklch(23.26% 0.014 253.1)', + }, + shikiTheme: 'catppuccin-frappe', +}; + +export { dark }; diff --git a/src/Theme/dim.ts b/src/Theme/dim.ts new file mode 100644 index 0000000..ed67470 --- /dev/null +++ b/src/Theme/dim.ts @@ -0,0 +1,43 @@ +import { Theme } from '../Type/Definition/index.js'; + +const dim: Theme = { + name: 'dim', + cssVariables: { + 'color-base-100': 'oklch(30.857% 0.023 264.149)', + 'color-base-200': 'oklch(28.036% 0.019 264.182)', + 'color-base-300': 'oklch(26.346% 0.018 262.177)', + + 'color-base-content': 'oklch(82.901% 0.031 222.959)', + 'color-primary': 'oklch(86.133% 0.141 139.549)', + 'color-primary-content': 'oklch(17.226% 0.028 139.549)', + 'color-secondary': 'oklch(73.375% 0.165 35.353)', + 'color-secondary-content': 'oklch(14.675% 0.033 35.353)', + 'color-accent': 'oklch(74.229% 0.133 311.379)', + 'color-accent-content': 'oklch(14.845% 0.026 311.379)', + 'color-neutral': 'oklch(24.731% 0.02 264.094)', + 'color-neutral-content': 'oklch(82.901% 0.031 222.959)', + + 'color-info': 'oklch(86.078% 0.142 206.182)', + 'color-info-content': 'oklch(17.215% 0.028 206.182)', + 'color-success': 'oklch(86.171% 0.142 166.534)', + 'color-success-content': 'oklch(17.234% 0.028 166.534)', + 'color-warning': 'oklch(86.163% 0.142 94.818)', + 'color-warning-content': 'oklch(17.232% 0.028 94.818)', + 'color-error': 'oklch(82.418% 0.099 33.756)', + 'color-error-content': 'oklch(16.483% 0.019 33.756)', + + 'radius-selector': '1rem', + 'radius-field': '0.5rem', + 'radius-box': '1rem', + 'size-selector': '0.25rem', + 'size-field': '0.25rem', + border: '1px', + depth: '0', + noise: '0', + + 'color-code-background': 'oklch(28.036% 0.019 264.182)', + }, + shikiTheme: 'catppuccin-frappe', +}; + +export { dim }; diff --git a/src/Theme/emerald.ts b/src/Theme/emerald.ts new file mode 100644 index 0000000..946589c --- /dev/null +++ b/src/Theme/emerald.ts @@ -0,0 +1,43 @@ +import { Theme } from '../Type/Definition/index.js'; + +const emerald: Theme = { + name: 'emerald', + cssVariables: { + 'color-base-100': 'oklch(100% 0 0)', + 'color-base-200': 'oklch(93% 0 0)', + 'color-base-300': 'oklch(86% 0 0)', + + 'color-base-content': 'oklch(35.519% 0.032 262.988)', + 'color-primary': 'oklch(76.662% 0.135 153.45)', + 'color-primary-content': 'oklch(33.387% 0.04 162.24)', + 'color-secondary': 'oklch(61.302% 0.202 261.294)', + 'color-secondary-content': 'oklch(100% 0 0)', + 'color-accent': 'oklch(72.772% 0.149 33.2)', + 'color-accent-content': 'oklch(0% 0 0)', + 'color-neutral': 'oklch(35.519% 0.032 262.988)', + 'color-neutral-content': 'oklch(98.462% 0.001 247.838)', + + 'color-info': 'oklch(72.06% 0.191 231.6)', + 'color-info-content': 'oklch(0% 0 0)', + 'color-success': 'oklch(64.8% 0.15 160)', + 'color-success-content': 'oklch(0% 0 0)', + 'color-warning': 'oklch(84.71% 0.199 83.87)', + 'color-warning-content': 'oklch(0% 0 0)', + 'color-error': 'oklch(71.76% 0.221 22.18)', + 'color-error-content': 'oklch(0% 0 0)', + + 'radius-selector': '1rem', + 'radius-field': '0.5rem', + 'radius-box': '1rem', + 'size-selector': '0.25rem', + 'size-field': '0.25rem', + border: '1px', + depth: '0', + noise: '0', + + 'color-code-background': 'oklch(0.28036 0.019 264.182)', + }, + shikiTheme: 'andromeeda', +}; + +export { emerald }; diff --git a/src/Theme/index.ts b/src/Theme/index.ts new file mode 100644 index 0000000..ece43c1 --- /dev/null +++ b/src/Theme/index.ts @@ -0,0 +1,4 @@ +export * from './dark.js'; +export * from './dim.js'; +export * from './emerald.js'; +export * from './light.js'; diff --git a/src/Theme/light.ts b/src/Theme/light.ts new file mode 100644 index 0000000..60ef68c --- /dev/null +++ b/src/Theme/light.ts @@ -0,0 +1,43 @@ +import { Theme } from '../Type/Definition/index.js'; + +const light: Theme = { + name: 'light', + cssVariables: { + 'color-base-100': 'oklch(100% 0 0)', + 'color-base-200': 'oklch(98% 0 0)', + 'color-base-300': 'oklch(95% 0 0)', + + 'color-base-content': 'oklch(21% 0.006 285.885)', + 'color-primary': '#ff073a', + 'color-primary-content': '#ffffff', + 'color-secondary': '#1a1a1a', + 'color-secondary-content': '#ffffff', + 'color-accent': 'oklch(77% 0.152 181.912)', + 'color-accent-content': 'oklch(38% 0.063 188.416)', + 'color-neutral': 'oklch(14% 0.005 285.823)', + 'color-neutral-content': 'oklch(92% 0.004 286.32)', + + 'color-info': '#036fe3', + 'color-info-content': '#fff', + 'color-success': '#008539', + 'color-success-content': '#fff', + 'color-warning': '#ffc835', + 'color-warning-content': '#000', + 'color-error': '#da294a', + 'color-error-content': '#000', + + 'radius-selector': '0.5rem', + 'radius-field': '0.25rem', + 'radius-box': '0.5rem', + 'size-selector': '0.25rem', + 'size-field': '0.25rem', + border: '1px', + depth: '1', + noise: '0', + + 'color-code-background': 'oklch(98% 0 0)', + }, + shikiTheme: 'catppuccin-latte', +}; + +export { light }; diff --git a/src/Type/Definition/Constructor.ts b/src/Type/Definition/Constructor.ts new file mode 100644 index 0000000..cb4b351 --- /dev/null +++ b/src/Type/Definition/Constructor.ts @@ -0,0 +1,4 @@ +/* eslint @typescript-eslint/no-explicit-any: "off" */ +type Constructor = new (...args: any[]) => T; + +export { Constructor }; diff --git a/src/Type/Definition/ElementCapableWebComponent.ts b/src/Type/Definition/ElementCapableWebComponent.ts new file mode 100644 index 0000000..5543b62 --- /dev/null +++ b/src/Type/Definition/ElementCapableWebComponent.ts @@ -0,0 +1,8 @@ +/* eslint @typescript-eslint/no-explicit-any: "off" */ +import { LifecycleCapableWebComponent } from './LifecycleCapableWebComponent.js'; + +interface ElementCapableWebComponent extends LifecycleCapableWebComponent { + elementId: string; +} + +export { ElementCapableWebComponent }; diff --git a/src/Type/Definition/LifecycleCapableWebComponent.ts b/src/Type/Definition/LifecycleCapableWebComponent.ts new file mode 100644 index 0000000..403f520 --- /dev/null +++ b/src/Type/Definition/LifecycleCapableWebComponent.ts @@ -0,0 +1,10 @@ +/* eslint @typescript-eslint/no-explicit-any: "off" */ +import { PropertyDeclaration } from '@lit/reactive-element'; + +interface LifecycleCapableWebComponent extends HTMLElement { + connectedCallback?(): void; + disconnectedCallback?(): void; + requestUpdate?(name?: PropertyKey, oldValue?: unknown, options?: PropertyDeclaration): void; +} + +export { LifecycleCapableWebComponent }; diff --git a/src/Type/Definition/Rule.ts b/src/Type/Definition/Rule.ts new file mode 100644 index 0000000..afa160d --- /dev/null +++ b/src/Type/Definition/Rule.ts @@ -0,0 +1,6 @@ +type Rule = { + type: 'exact' | 'fuzzy'; + identifier: string; +}; + +export { Rule }; diff --git a/src/Type/Definition/Ruleset.ts b/src/Type/Definition/Ruleset.ts new file mode 100644 index 0000000..5f19988 --- /dev/null +++ b/src/Type/Definition/Ruleset.ts @@ -0,0 +1,5 @@ +import { Rule } from './Rule.js'; + +type Ruleset = Array; + +export { Ruleset }; diff --git a/src/Type/Definition/ServiceResolverCapableWebComponent.ts b/src/Type/Definition/ServiceResolverCapableWebComponent.ts new file mode 100644 index 0000000..d332185 --- /dev/null +++ b/src/Type/Definition/ServiceResolverCapableWebComponent.ts @@ -0,0 +1,16 @@ +/* eslint @typescript-eslint/no-explicit-any: "off" */ +import { ServiceResolver } from '@ember-nexus/app-core/Service'; + +import { LifecycleCapableWebComponent } from './LifecycleCapableWebComponent.js'; + +interface ServiceResolverCapableWebComponent extends LifecycleCapableWebComponent { + state: { + context: { + serviceResolver: ServiceResolver | null; + }; + }; + + onServiceResolverLoaded?(serviceResolver: ServiceResolver): void; +} + +export { ServiceResolverCapableWebComponent }; diff --git a/src/Type/Definition/Theme.ts b/src/Type/Definition/Theme.ts new file mode 100644 index 0000000..610a27d --- /dev/null +++ b/src/Type/Definition/Theme.ts @@ -0,0 +1,41 @@ +type Theme = { + name: string; + cssVariables: { + 'color-base-100': string; + 'color-base-200': string; + 'color-base-300': string; + + 'color-base-content': string; + 'color-primary': string; + 'color-primary-content': string; + 'color-secondary': string; + 'color-secondary-content': string; + 'color-accent': string; + 'color-accent-content': string; + 'color-neutral': string; + 'color-neutral-content': string; + + 'color-info': string; + 'color-info-content': string; + 'color-success': string; + 'color-success-content': string; + 'color-warning': string; + 'color-warning-content': string; + 'color-error': string; + 'color-error-content': string; + + 'radius-selector': string; + 'radius-field': string; + 'radius-box': string; + 'size-selector': string; + 'size-field': string; + border: string; + depth: string; + noise: string; + + 'color-code-background': string; + }; + shikiTheme: string; +}; + +export { Theme }; diff --git a/src/Type/Definition/TranslationCapableWebComponent.ts b/src/Type/Definition/TranslationCapableWebComponent.ts new file mode 100644 index 0000000..aa74398 --- /dev/null +++ b/src/Type/Definition/TranslationCapableWebComponent.ts @@ -0,0 +1,9 @@ +import { i18n } from 'i18next'; + +import { ServiceResolverCapableWebComponent } from './ServiceResolverCapableWebComponent.js'; + +interface TranslationCapableWebComponent extends ServiceResolverCapableWebComponent { + i18n: i18n; +} + +export { TranslationCapableWebComponent }; diff --git a/src/Type/Definition/index.ts b/src/Type/Definition/index.ts new file mode 100644 index 0000000..f61901f --- /dev/null +++ b/src/Type/Definition/index.ts @@ -0,0 +1,8 @@ +export * from './Constructor.js'; +export * from './ElementCapableWebComponent.js'; +export * from './LifecycleCapableWebComponent.js'; +export * from './Rule.js'; +export * from './Ruleset.js'; +export * from './ServiceResolverCapableWebComponent.js'; +export * from './Theme.js'; +export * from './TranslationCapableWebComponent.js'; diff --git a/src/Type/Enum/EventIdentifier.ts b/src/Type/Enum/EventIdentifier.ts new file mode 100644 index 0000000..ccde6aa --- /dev/null +++ b/src/Type/Enum/EventIdentifier.ts @@ -0,0 +1,6 @@ +enum EventIdentifier { + themeChangeEvent = 'ember-nexus.app-plugin-experimental.event.theme-change', + languageChangeEvent = 'ember-nexus.app-plugin-experimental.event.language-change', +} + +export { EventIdentifier }; diff --git a/src/Type/Enum/ServiceIdentifier.ts b/src/Type/Enum/ServiceIdentifier.ts new file mode 100644 index 0000000..def05ad --- /dev/null +++ b/src/Type/Enum/ServiceIdentifier.ts @@ -0,0 +1,10 @@ +enum ServiceIdentifier { + // services + propertyExtractorService = 'ember-nexus.app-plugin-experimental.service.property-extractor-service', + shikiJsonHighlighterService = 'ember-nexus.app-plugin-experimental.service.shiki-json-highlighter-service', + themeService = 'ember-nexus.app-plugin-experimental.service.theme-service', + languageService = 'ember-nexus.app-plugin-experimental.service.language-service', + translationService = 'ember-nexus.app-plugin-experimental.service.translation-service', +} + +export { ServiceIdentifier }; diff --git a/src/Type/Enum/index.ts b/src/Type/Enum/index.ts new file mode 100644 index 0000000..8e6d18a --- /dev/null +++ b/src/Type/Enum/index.ts @@ -0,0 +1,2 @@ +export * from './EventIdentifier.js'; +export * from './ServiceIdentifier.js'; diff --git a/src/Type/index.ts b/src/Type/index.ts index a719bff..3152035 100644 --- a/src/Type/index.ts +++ b/src/Type/index.ts @@ -1 +1,3 @@ -export * from './Constants'; +export * from './Constants.js'; +export * as Definition from './Definition/index.js'; +export * as Enum from './Enum/index.js'; diff --git a/src/index.ts b/src/index.ts index c199a01..3c359fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,11 @@ -export * as Component from './Component'; -export * as Helper from './Helper'; -export * as Machine from './Machine'; -export * as Style from './Style'; -export * as Type from './Type'; +export * as Component from './Component/index.js'; +export * as Decorator from './Decorator/index.js'; +export * as Event from './Event/index.js'; +export * as Helper from './Helper/index.js'; +export * from './init.js'; +export * as Machine from './Machine/index.js'; +export * as Page from './Page/index.js'; +export * as Service from './Service/index.js'; +export * as Style from './Style/index.js'; +export * as Theme from './Theme/index.js'; +export * as Type from './Type/index.js'; diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000..01d36ed --- /dev/null +++ b/src/init.ts @@ -0,0 +1,24 @@ +import { ServiceResolver } from '@ember-nexus/app-core/Service'; + +import { LanguageService, ShikiJsonHighlighterService, ThemeService, TranslationService } from './Service/index.js'; +import { dark, dim, emerald, light } from './Theme/index.js'; + +function init(serviceResolver: ServiceResolver): void { + const services = [ShikiJsonHighlighterService, ThemeService, LanguageService, TranslationService]; + for (let i = 0; i < services.length; i++) { + serviceResolver.setService(services[i].identifier, services[i].constructFromServiceResolver(serviceResolver)); + } + + const themeService = serviceResolver.getServiceOrFail(ThemeService.identifier); + + const themes = [light, dark, emerald, dim]; + for (let i = 0; i < themes.length; i++) { + themeService.themes.setEntry(themes[i].name, themes[i]); + } + themeService.applyTheme(light.name); + const styleSheet = themeService.getStyleSheet(); + + document.adoptedStyleSheets = [...document.adoptedStyleSheets, styleSheet]; +} + +export { init }; diff --git a/storybook/Component/Debug/Default.stories.ts b/storybook/Component/Debug/Default.stories.ts new file mode 100644 index 0000000..dc2e29f --- /dev/null +++ b/storybook/Component/Debug/Default.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Debug', + component: 'ember-nexus-debug', + render: ({elementId}) => html` +
+ + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '56fda20c-b238-4034-b555-1df47c47e17a', + } +}; diff --git a/storybook/Component/Default/Card/Card.mdx b/storybook/Component/Default/Card/Card.mdx index d52c487..f2b79f5 100644 --- a/storybook/Component/Default/Card/Card.mdx +++ b/storybook/Component/Default/Card/Card.mdx @@ -1,4 +1,4 @@ -import { Canvas, Meta } from '@storybook/blocks'; +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; import * as DefaultStories from './Default.stories'; diff --git a/storybook/Component/Default/Card/ColorVariants.stories.ts b/storybook/Component/Default/Card/ColorVariants.stories.ts index 3497b94..9d4fd65 100644 --- a/storybook/Component/Default/Card/ColorVariants.stories.ts +++ b/storybook/Component/Default/Card/ColorVariants.stories.ts @@ -1,44 +1,44 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; +import {ServiceResolver} from "@ember-nexus/app-core/Service"; +import {ElementCache} from "@ember-nexus/app-core/Cache"; -function getColorDataString(colorName: string, colorValue: string): string { - return JSON.stringify({ - type: 'Data', - data: { - name: `Color ${colorName}`, - description: `Description of color with name ${colorName} and value ${colorValue}.`, - color: colorValue - } - }); -} const colors = [ - {name: 'white', value: '#ffffff'}, - {name: 'black', value: '#000000'}, - {name: 'yellow', value: '#FFD800'}, - {name: 'orange', value: '#FF8200'}, - {name: 'red', value: '#FF073A'}, - {name: 'purple', value: '#6F00FF'}, - {name: 'blue', value: '#007BFF'}, - {name: 'green', value: '#00A550'}, + {name: 'default', id: '00000000-0000-4000-8000-000000000000'}, + {name: 'white', value: '#ffffff', id: '00000000-0000-4000-8000-000000000001'}, + {name: 'black', value: '#000000', id: '00000000-0000-4000-8000-000000000002'}, + {name: 'yellow', value: '#FFD800', id: '00000000-0000-4000-8000-000000000003'}, + {name: 'orange', value: '#FF8200', id: '00000000-0000-4000-8000-000000000004'}, + {name: 'red', value: '#FF073A', id: '00000000-0000-4000-8000-000000000005'}, + {name: 'purple', value: '#6F00FF', id: '00000000-0000-4000-8000-000000000006'}, + {name: 'blue', value: '#007BFF', id: '00000000-0000-4000-8000-000000000007'}, + {name: 'green', value: '#00A550', id: '00000000-0000-4000-8000-000000000008'}, ]; +const serviceResolver = (window as any).serviceResolver as ServiceResolver; +const elementCache = serviceResolver.getServiceOrFail(ElementCache.identifier); +colors.map((color) => elementCache.set(color.id, { + data: { + id: color.id, + type: 'Color', + data: { + name: `Color ${color.name}`, + description: `Description of color with name ${color.name} and value ${color?.value ?? '"none"'}.`, + ...(color?.value && {color: color.value}) + } + }, + etag: undefined +})); + + const meta: Meta = { title: 'Component/Default/Card', component: 'ember-nexus-default-card', render: () => html` -
+
${colors.map((color) => - html`
- - - - -
` + html`` )}
`, diff --git a/storybook/Component/Default/Card/Default.stories.ts b/storybook/Component/Default/Card/Default.stories.ts index 2920dcd..8818315 100644 --- a/storybook/Component/Default/Card/Default.stories.ts +++ b/storybook/Component/Default/Card/Default.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; @@ -7,7 +7,7 @@ const meta: Meta = { title: 'Component/Default/Card', component: 'ember-nexus-default-card', render: ({elementId}) => html` -
+
diff --git a/storybook/Component/Default/Card/Error.stories.ts b/storybook/Component/Default/Card/Error.stories.ts index b76d970..edb8ca8 100644 --- a/storybook/Component/Default/Card/Error.stories.ts +++ b/storybook/Component/Default/Card/Error.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; @@ -7,7 +7,7 @@ const meta: Meta = { title: 'Component/Default/Card', component: 'ember-nexus-default-card', render: ({elementId}) => html` -
+
diff --git a/storybook/Component/Default/Card/SizeVariants.stories.ts b/storybook/Component/Default/Card/SizeVariants.stories.ts index a81317f..225c279 100644 --- a/storybook/Component/Default/Card/SizeVariants.stories.ts +++ b/storybook/Component/Default/Card/SizeVariants.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; @@ -7,7 +7,7 @@ const meta: Meta = { title: 'Component/Default/Card', component: 'ember-nexus-default-card', render: ({elementId}) => html` -
+
html` -
+
${texts.map((text) => html`
= { title: 'Component/Default', component: 'ember-nexus-default', render: ({elementId}) => html` -
+
diff --git a/storybook/Component/Default/Icon/Default.stories.ts b/storybook/Component/Default/Icon/Default.stories.ts index df26fcf..bf771be 100644 --- a/storybook/Component/Default/Icon/Default.stories.ts +++ b/storybook/Component/Default/Icon/Default.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Icon/SizeVariants.stories.ts b/storybook/Component/Default/Icon/SizeVariants.stories.ts index 2c25f99..1973859 100644 --- a/storybook/Component/Default/Icon/SizeVariants.stories.ts +++ b/storybook/Component/Default/Icon/SizeVariants.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/InlineText/Default.stories.ts b/storybook/Component/Default/InlineText/Default.stories.ts index 670a6bf..9de4d99 100644 --- a/storybook/Component/Default/InlineText/Default.stories.ts +++ b/storybook/Component/Default/InlineText/Default.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/InlineText/SizeVariants.stories.ts b/storybook/Component/Default/InlineText/SizeVariants.stories.ts index e5ad064..5acd566 100644 --- a/storybook/Component/Default/InlineText/SizeVariants.stories.ts +++ b/storybook/Component/Default/InlineText/SizeVariants.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Pill/Default.stories.ts b/storybook/Component/Default/Pill/Default.stories.ts index 843cd43..4dc3db4 100644 --- a/storybook/Component/Default/Pill/Default.stories.ts +++ b/storybook/Component/Default/Pill/Default.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Pill/SizeVariants.stories.ts b/storybook/Component/Default/Pill/SizeVariants.stories.ts index aab8231..d64fc38 100644 --- a/storybook/Component/Default/Pill/SizeVariants.stories.ts +++ b/storybook/Component/Default/Pill/SizeVariants.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Thumbnail/Default.stories.ts b/storybook/Component/Default/Thumbnail/Default.stories.ts index 25881d4..ee22445 100644 --- a/storybook/Component/Default/Thumbnail/Default.stories.ts +++ b/storybook/Component/Default/Thumbnail/Default.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Thumbnail/RestrictedSpace.stories.ts b/storybook/Component/Default/Thumbnail/RestrictedSpace.stories.ts index 9bee0a9..16d6cc5 100644 --- a/storybook/Component/Default/Thumbnail/RestrictedSpace.stories.ts +++ b/storybook/Component/Default/Thumbnail/RestrictedSpace.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Default/Thumbnail/SizeVariants.stories.ts b/storybook/Component/Default/Thumbnail/SizeVariants.stories.ts index 0375785..96cdfc8 100644 --- a/storybook/Component/Default/Thumbnail/SizeVariants.stories.ts +++ b/storybook/Component/Default/Thumbnail/SizeVariants.stories.ts @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from '@storybook/web-components'; +import type { Meta, StoryObj } from '@storybook/web-components-vite'; import { html } from 'lit'; type CustomArgs = { elementId: string }; diff --git a/storybook/Component/Graph/Default.stories.ts b/storybook/Component/Graph/Default.stories.ts new file mode 100644 index 0000000..248c649 --- /dev/null +++ b/storybook/Component/Graph/Default.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +const meta: Meta = { + title: 'Component/Graph', + component: 'ember-nexus-graph', + render: () => html` +
+ + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/storybook/Component/Task/Card/Card.mdx b/storybook/Component/Task/Card/Card.mdx new file mode 100644 index 0000000..3eda1e9 --- /dev/null +++ b/storybook/Component/Task/Card/Card.mdx @@ -0,0 +1,11 @@ +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; + +import * as DefaultStories from './Default.stories'; + + + +# `ember-nexus-task-card` + +An card element used for displaying a single task and its state (open, blocked or done). + + diff --git a/storybook/Component/Task/Card/Default.stories.ts b/storybook/Component/Task/Card/Default.stories.ts new file mode 100644 index 0000000..dfc5bc6 --- /dev/null +++ b/storybook/Component/Task/Card/Default.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Card', + component: 'ember-nexus-task-card', + render: ({elementId}) => html` +
+ + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Component/Task/Card/Error.stories.ts b/storybook/Component/Task/Card/Error.stories.ts new file mode 100644 index 0000000..087bbf2 --- /dev/null +++ b/storybook/Component/Task/Card/Error.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Card', + component: 'ember-nexus-task-card', + render: ({elementId}) => html` +
+ + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Error: Story = { + args: { + elementId: '40400000-0000-4000-8000-000000000404', + } +}; diff --git a/storybook/Component/Task/Card/States.stories.ts b/storybook/Component/Task/Card/States.stories.ts new file mode 100644 index 0000000..56c9d2a --- /dev/null +++ b/storybook/Component/Task/Card/States.stories.ts @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Card', + component: 'ember-nexus-task-card', + render: () => html` +
+ + + + + + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const States: Story = {}; diff --git a/storybook/Component/Task/Default.stories.ts b/storybook/Component/Task/Default.stories.ts new file mode 100644 index 0000000..bac50e9 --- /dev/null +++ b/storybook/Component/Task/Default.stories.ts @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task', + component: 'ember-nexus-task', + render: ({elementId}) => html` +
+ + + + + + + + + + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Component/Task/Icon/Default.stories.ts b/storybook/Component/Task/Icon/Default.stories.ts new file mode 100644 index 0000000..0690de0 --- /dev/null +++ b/storybook/Component/Task/Icon/Default.stories.ts @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Icon', + component: 'ember-nexus-task-icon', + render: ({elementId}) => html` + + + `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Component/Task/InlineText/Default.stories.ts b/storybook/Component/Task/InlineText/Default.stories.ts new file mode 100644 index 0000000..4dcea3b --- /dev/null +++ b/storybook/Component/Task/InlineText/Default.stories.ts @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/InlineText', + component: 'ember-nexus-task-inline-text', + render: ({elementId}) => html` + + + `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Component/Task/Pill/Default.stories.ts b/storybook/Component/Task/Pill/Default.stories.ts new file mode 100644 index 0000000..ba9fd1d --- /dev/null +++ b/storybook/Component/Task/Pill/Default.stories.ts @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Pill', + component: 'ember-nexus-task-pill', + render: ({elementId}) => html` + + + `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Component/Task/Thumbnail/Default.stories.ts b/storybook/Component/Task/Thumbnail/Default.stories.ts new file mode 100644 index 0000000..07ef5ab --- /dev/null +++ b/storybook/Component/Task/Thumbnail/Default.stories.ts @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + +type CustomArgs = { elementId: string }; + +const meta: Meta = { + title: 'Component/Task/Thumbnail', + component: 'ember-nexus-task-thumbnail', + render: ({elementId}) => html` + + + `, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + elementId: '976a753d-c47f-4305-a469-6c717eceb87f', + } +}; diff --git a/storybook/Page/Login.stories.ts b/storybook/Page/Login.stories.ts new file mode 100644 index 0000000..d83dd3c --- /dev/null +++ b/storybook/Page/Login.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/web-components-vite'; +import { html } from 'lit'; + + +const meta: Meta = { + title: 'Page', + component: 'ember-nexus-page-login', + render: () => html` +
+ + +
+ `, +}; + +export default meta; +type Story = StoryObj; + +export const Login: Story = {}; diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..1328348 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,31 @@ +import type { UserConfig } from 'vite'; +import { resolve } from 'path'; +import tailwindcss from '@tailwindcss/vite'; + +export default { + base: './', + build: { + outDir: 'dist/browser', + sourcemap: true, + minify: 'terser', + lib: { + entry: './src/index.ts', + formats: ['es'] + }, + cssCodeSplit: true, + rollupOptions: { + input: [ + resolve(__dirname, './src/index.ts'), + resolve(__dirname, './src/Style/index.css'), + ], + output: { + entryFileNames: "[name].js", + chunkFileNames: '[name].js', + assetFileNames: `[name].[ext]` + } + } + }, + plugins: [ + tailwindcss() + ], +} satisfies UserConfig; diff --git a/webpack.config.cjs b/webpack.config.cjs index c48c75c..7f0d327 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -9,7 +9,7 @@ module.exports = { rules: [ { test: /\.ts$/, - exclude: [/node_modules/], + exclude: [/node_modules/, /\.d\.ts$/], use: [ { loader: 'ts-loader', @@ -48,7 +48,10 @@ module.exports = { ], }, resolve: { - extensions: ['.ts', '.js'] + extensions: ['.ts', '.js'], + extensionAlias: { + '.js': ['.js', '.ts'], + } }, experiments: { outputModule: true, @@ -81,7 +84,5 @@ module.exports = { }), new CssMinimizerPlugin() ], - }, - plugins: [ - ] + } };