├── .browserslistrc ├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── DMP_2024.yml └── workflows │ ├── CD.yml │ ├── CI.yml │ └── lint.yml ├── .gitignore ├── .markdownlint.jsonc ├── .npmrc ├── .prettierrc ├── .vscode └── settings.json ├── @types ├── app.d.ts ├── assets.d.ts ├── components │ ├── editor.d.ts │ ├── index.d.ts │ ├── menu.d.ts │ ├── painter.d.ts │ └── singer.d.ts ├── env.d.ts ├── events.d.ts └── i18n.d.ts ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── .storybook │ ├── main.ts │ ├── preview-head.html │ └── preview.ts ├── env │ ├── .env │ ├── .env.development │ └── .env.production ├── index.html ├── package.json ├── public │ ├── browserconfig.xml │ ├── favicon.ico │ ├── icons │ │ ├── android-icon-144x144.png │ │ ├── android-icon-192x192.png │ │ ├── android-icon-36x36.png │ │ ├── android-icon-48x48.png │ │ ├── android-icon-72x72.png │ │ ├── android-icon-96x96.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── apple-icon-precomposed.png │ │ ├── apple-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ └── ms-icon-70x70.png │ ├── logo.png │ ├── logo.svg │ ├── manifest.json │ └── robots.txt ├── src │ ├── components.ts │ ├── config │ │ ├── Config.tsx │ │ ├── ConfigPage.tsx │ │ ├── index.scss │ │ ├── index.tsx │ │ └── preset │ │ │ └── preset-0.ts │ ├── index.ts │ ├── splash │ │ ├── index.scss │ │ ├── index.stories.tsx │ │ └── index.tsx │ └── utils │ │ └── misc.ts ├── tools │ ├── scripts │ │ └── stats.ts │ └── vite.config.ts └── tsconfig.json ├── coverage.sh ├── docker-compose.yml ├── docs ├── ARCHITECTURE.md ├── CONTRIBUTING.md ├── DEV.md ├── functional-requirements.md ├── images │ ├── architecture │ │ └── components.png │ └── wireframe.jpg └── webpack-choices.md ├── lerna.json ├── lib ├── assets │ ├── index.ts │ ├── package.json │ ├── res │ │ ├── audio │ │ │ ├── guitar.wav │ │ │ ├── piano.wav │ │ │ └── snare.wav │ │ ├── image │ │ │ ├── icon │ │ │ │ ├── build.svg │ │ │ │ ├── close.svg │ │ │ │ ├── code.svg │ │ │ │ ├── exportDrawing.svg │ │ │ │ ├── help.svg │ │ │ │ ├── loadProject.svg │ │ │ │ ├── mouse.svg │ │ │ │ ├── pin.svg │ │ │ │ ├── reset.svg │ │ │ │ ├── run.svg │ │ │ │ ├── saveProjectHTML.svg │ │ │ │ ├── startRecording.svg │ │ │ │ ├── stop.svg │ │ │ │ ├── stopRecording.svg │ │ │ │ ├── unpin.svg │ │ │ │ └── uploadFile.svg │ │ │ └── logo.png │ │ └── index.ts │ ├── src │ │ ├── index.ts │ │ └── loaders.ts │ └── tsconfig.json ├── components │ ├── .storybook │ │ ├── main.ts │ │ ├── preview-head.html │ │ └── preview.ts │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── SImage │ │ │ └── index.tsx │ │ ├── SImageRaster │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── SImageVector │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ ├── WCheckbox │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── WIconButton │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── WTextButton │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ ├── WToggleSwitch │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ └── WToggleSwitchRounded │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ └── tsconfig.json ├── config │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── utils.ts │ └── tsconfig.json ├── events │ ├── index.ts │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── i18n │ ├── index.ts │ ├── lang │ │ ├── en.ts │ │ └── es.ts │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── transport │ ├── index.ts │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json └── view │ ├── index.ts │ ├── package.json │ ├── src │ ├── components │ │ ├── index.scss │ │ ├── index.tsx │ │ └── toolbar │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── resources │ │ │ ├── pin.svg │ │ │ └── unpin.svg │ └── index.ts │ └── tsconfig.json ├── modules ├── code-builder │ ├── .storybook │ │ ├── main.ts │ │ ├── preview-head.html │ │ └── preview.ts │ ├── package.json │ ├── playground │ │ ├── index.html │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── Collision │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ └── WorkSpace │ │ │ │ ├── BrickFactory.tsx │ │ │ │ ├── BricksCoordsStore.ts │ │ │ │ ├── data.ts │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ └── vite.config.ts │ ├── src │ │ ├── @types │ │ │ ├── brick.d.ts │ │ │ └── collision.d.ts │ │ ├── brick │ │ │ ├── README.md │ │ │ ├── design0 │ │ │ │ ├── BrickBlock.ts │ │ │ │ ├── BrickData.ts │ │ │ │ ├── BrickExpression.ts │ │ │ │ ├── BrickStatement.ts │ │ │ │ ├── components │ │ │ │ │ ├── BrickBlock.tsx │ │ │ │ │ ├── BrickData.tsx │ │ │ │ │ ├── BrickExpression.tsx │ │ │ │ │ └── BrickStatement.tsx │ │ │ │ ├── stories │ │ │ │ │ ├── BrickBlock.stories.ts │ │ │ │ │ ├── BrickData.stories.ts │ │ │ │ │ ├── BrickExpression.stories.ts │ │ │ │ │ └── BrickStatement.stories.ts │ │ │ │ └── utils │ │ │ │ │ ├── path.ts │ │ │ │ │ └── spec │ │ │ │ │ └── path.spec.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── stories │ │ │ │ ├── brickBlock.ts │ │ │ │ ├── brickData.ts │ │ │ │ ├── brickExpression.ts │ │ │ │ ├── brickStatement.ts │ │ │ │ └── components │ │ │ │ ├── BrickBlock.tsx │ │ │ │ ├── BrickData.tsx │ │ │ │ ├── BrickExpression.tsx │ │ │ │ ├── BrickStatement.tsx │ │ │ │ └── BrickWrapper.tsx │ │ ├── collision │ │ │ ├── Brute.ts │ │ │ ├── QuadTree.ts │ │ │ ├── index.ts │ │ │ └── utils │ │ │ │ └── index.ts │ │ └── index.ts │ └── tsconfig.json ├── editor │ ├── package.json │ ├── src │ │ ├── @types │ │ │ └── index.ts │ │ ├── core │ │ │ ├── errors.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── view │ │ │ ├── components │ │ │ ├── button │ │ │ │ ├── index.scss │ │ │ │ └── index.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ │ └── index.ts │ └── tsconfig.json ├── menu │ ├── .storybook │ │ ├── main.ts │ │ ├── preview-head.html │ │ └── preview.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── view │ │ │ ├── components │ │ │ ├── index.scss │ │ │ ├── index.stories.tsx │ │ │ └── index.tsx │ │ │ └── index.ts │ └── tsconfig.json ├── painter │ ├── package.json │ ├── src │ │ ├── @types │ │ │ └── index.d.ts │ │ ├── core │ │ │ ├── sketchP5.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── painter.ts │ │ └── view │ │ │ ├── components │ │ │ ├── index.scss │ │ │ ├── index.tsx │ │ │ └── utils │ │ │ │ └── background.ts │ │ │ ├── index.ts │ │ │ └── sprite.ts │ └── tsconfig.json └── singer │ ├── package.json │ ├── playground │ ├── index.html │ ├── index.tsx │ ├── pages │ │ └── Voice.tsx │ └── vite.config.ts │ ├── src │ ├── @types │ │ ├── currentPitch.d.ts │ │ ├── errors.d.ts │ │ ├── keySignature.d.ts │ │ ├── scale.d.ts │ │ ├── synthUtils.d.ts │ │ ├── temperament.d.ts │ │ └── voice.d.ts │ ├── core │ │ ├── README.md │ │ ├── currentPitch.ts │ │ ├── errors.ts │ │ ├── keySignature.ts │ │ ├── musicUtils.ts │ │ ├── scale.ts │ │ ├── synthUtils.ts │ │ ├── temperament.ts │ │ ├── tests │ │ │ ├── currentPitch.test.ts │ │ │ ├── keySignature.test.ts │ │ │ ├── musicUtils.test.ts │ │ │ ├── scale.test.ts │ │ │ ├── temperament.test.ts │ │ │ └── voice.test.ts │ │ └── voice.ts │ ├── index.ts │ └── singer.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── res ├── scss │ ├── base.scss │ ├── sizes.scss │ └── wrappers.scss └── themes │ └── light.scss ├── tsconfig.json └── vitest.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production] 2 | >0.2% 3 | not dead 4 | not op_mini all 5 | 6 | [development] 7 | last 1 chrome version 8 | last 1 firefox version 9 | last 1 safari version 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .gitmodules 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/archive/**/* 2 | src/components/editor-next/**/* 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | es2021: true, 6 | }, 7 | 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module', 12 | }, 13 | 14 | plugins: ['react-refresh', 'prettier'], 15 | 16 | extends: [ 17 | 'eslint:recommended', 18 | 'plugin:@typescript-eslint/recommended', 19 | 'plugin:react-hooks/recommended', 20 | 'prettier', 21 | ], 22 | 23 | rules: { 24 | 'max-len': [ 25 | 'warn', 26 | { 27 | code: 100, 28 | ignoreTrailingComments: true, 29 | ignoreComments: true, 30 | ignoreStrings: true, 31 | ignoreTemplateLiterals: true, 32 | }, 33 | ], 34 | 35 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 36 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 37 | 'no-duplicate-case': 'error', 38 | 'no-irregular-whitespace': 'warn', 39 | 'no-mixed-spaces-and-tabs': 'warn', 40 | 'no-trailing-spaces': [ 41 | 'warn', 42 | { 43 | skipBlankLines: true, 44 | ignoreComments: true, 45 | }, 46 | ], 47 | 'no-unused-vars': 'off', 48 | 49 | 'prefer-const': ['off'], 50 | 'semi': ['error', 'always'], 51 | 52 | 'prettier/prettier': 'warn', 53 | }, 54 | 55 | overrides: [ 56 | { 57 | files: ['**/*.ts', '**/*.tsx'], 58 | rules: { 59 | '@typescript-eslint/ban-ts-comment': 'off', 60 | '@typescript-eslint/no-non-null-assertion': 'off', 61 | '@typescript-eslint/no-unused-vars': [ 62 | 'warn', 63 | { 64 | argsIgnorePattern: '^_', 65 | varsIgnorePattern: '^_', 66 | }, 67 | ], 68 | 'no-use-before-define': 'off', 69 | '@typescript-eslint/no-use-before-define': ['error', 'nofunc'], 70 | }, 71 | }, 72 | 73 | { 74 | files: ['**/*.json'], 75 | 76 | extends: ['plugin:json/recommended'], 77 | 78 | rules: { 79 | 'prettier/prettier': 'off', 80 | }, 81 | }, 82 | 83 | { 84 | files: ['**/*.spec.ts', '**/*.test.ts'], 85 | 86 | globals: { 87 | suite: true, 88 | test: true, 89 | describe: true, 90 | it: true, 91 | expectTypeOf: true, 92 | assertType: true, 93 | expect: true, 94 | assert: true, 95 | vitest: true, 96 | vi: true, 97 | beforeAll: true, 98 | afterAll: true, 99 | beforeEach: true, 100 | afterEach: true, 101 | }, 102 | }, 103 | ], 104 | }; 105 | -------------------------------------------------------------------------------- /.github/workflows/CD.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Documentation:https://help.github.com/en/articles/workflow-syntax-for-github-actions 3 | 4 | name: Continuous Deployment 5 | 6 | on: 7 | push: 8 | branches: [develop] 9 | 10 | jobs: 11 | deploy: 12 | name: Build and Deploy app to GitHub Pages 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout the code base 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 16.x 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '16' 26 | 27 | - name: Install dependencies 28 | run: | 29 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc 30 | npm ci 31 | npx lerna bootstrap 32 | rm -rf .npmrc 33 | 34 | - name: Build for Production 35 | run: npm run build:gh 36 | 37 | - name: Deploy to GitHub Pages 38 | uses: peaceiris/actions-gh-pages@v3 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | publish_dir: ./app/dist 42 | force_orphan: true 43 | user_name: 'github-actions[bot]' 44 | user_email: 'github-actions[bot]@users.noreply.github.com' 45 | full_commit_message: 'deploy: publish app to GitHub Pages' 46 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Documentation:https://help.github.com/en/articles/workflow-syntax-for-github-actions 3 | 4 | name: Continuous Integration 5 | 6 | on: 7 | pull_request: 8 | 9 | jobs: 10 | type-check: 11 | name: Verify TypeScript types 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout the code base 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup Node.js 16.x 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: '16' 25 | 26 | - name: Install dependencies 27 | run: | 28 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc 29 | npm ci 30 | npx lerna bootstrap 31 | rm -rf .npmrc 32 | 33 | - name: Run Typescript 34 | run: npm run check 35 | 36 | smoke-test: 37 | name: Run Smoke Test (build) 38 | 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - name: Checkout the code base 43 | uses: actions/checkout@v3 44 | with: 45 | fetch-depth: 0 46 | 47 | - name: Setup Node.js 16.x 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: '16' 51 | 52 | - name: Install dependencies 53 | run: | 54 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc 55 | npm ci 56 | npx lerna bootstrap 57 | rm -rf .npmrc 58 | 59 | - name: Build for Production 60 | run: npm run build 61 | 62 | source-test: 63 | name: Run Tests on Source Code 64 | 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - name: Checkout the code base 69 | uses: actions/checkout@v3 70 | with: 71 | fetch-depth: 0 72 | 73 | - name: Setup Node.js 16.x 74 | uses: actions/setup-node@v3 75 | with: 76 | node-version: '16' 77 | 78 | # - name: Install Cypress dependencies 79 | # run: sudo apt update && sudo apt install -y libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb 80 | 81 | - name: Install dependencies 82 | run: | 83 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc 84 | npm ci 85 | npx lerna bootstrap 86 | rm -rf .npmrc 87 | 88 | - name: Run Unit Tests 89 | run: npm run test 90 | 91 | # - name: Cypress run 92 | # uses: cypress-io/github-action@v4 93 | # with: 94 | # build: npm run build:root 95 | # start: npm run serve 96 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Documentation: https://help.github.com/en/articles/workflow-syntax-for-github-actions 3 | 4 | name: Linting 5 | 6 | on: 7 | pull_request: 8 | branches: [develop] 9 | 10 | jobs: 11 | lint: 12 | name: Lint Code Base 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout the code base 18 | uses: actions/checkout@v3 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Node.js 16.x 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: '16' 26 | 27 | - name: Install dependencies 28 | run: | 29 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc 30 | npm ci 31 | npx lerna bootstrap 32 | rm -rf .npmrc 33 | 34 | - name: Lint files 35 | run: | 36 | npm run lint 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | .pnp 4 | .pnp.js 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | dist 11 | build 12 | 13 | # linter 14 | .eslintcache 15 | 16 | # storybook 17 | storybook 18 | 19 | # misc 20 | .DS_Store 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | lerna-debug.log* 30 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @sugarlabs:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "quoteProps": "consistent", 8 | "trailingComma": "all", 9 | "bracketSpacing": true, 10 | "arrowParens": "always", 11 | "endOfLine": "lf", 12 | "overrides": [ 13 | { 14 | "files": [ 15 | "*.json", 16 | "*.jsonc", 17 | "*.yml", 18 | "*.html", 19 | "*.css", 20 | "*.scss", 21 | "*.tsx" 22 | ], 23 | "options": { 24 | "tabWidth": 2 25 | } 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [100], 3 | "editor.renderWhitespace": "boundary", 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "javascript.updateImportsOnFileMove.enabled": "always", 6 | "typescript.updateImportsOnFileMove.enabled": "always", 7 | "editor.formatOnSave": true, 8 | "editor.formatOnType": false, 9 | "editor.selectionHighlight": true, 10 | "editor.tabSize": 4, 11 | "[json]": { 12 | "editor.tabSize": 2 13 | }, 14 | "[html]": { 15 | "editor.suggest.insertMode": "insert" 16 | }, 17 | "[scss]": { 18 | "editor.tabSize": 2 19 | }, 20 | "[typescriptreact]": { 21 | "editor.tabSize": 2 22 | }, 23 | "[dart]": { 24 | "editor.selectionHighlight": false, 25 | "editor.suggest.snippetsPreventQuickSuggestions": false, 26 | "editor.suggestSelection": "first", 27 | "editor.tabCompletion": "onlySnippets", 28 | "editor.wordBasedSuggestions": false 29 | }, 30 | "[markdown]": { 31 | "editor.wordWrap": "on", 32 | "editor.quickSuggestions": { 33 | "comments": "off", 34 | "strings": "off", 35 | "other": "off" 36 | }, 37 | "editor.formatOnSave": false 38 | }, 39 | "[jsonc]": { 40 | "editor.defaultFormatter": "vscode.json-language-features" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /@types/app.d.ts: -------------------------------------------------------------------------------- 1 | import type { TI18nLang } from './i18n'; 2 | import type { TComponentId } from './components'; 3 | import type { TFeatureFlagMenu } from './components/menu'; 4 | import type { TComponentDefinitionElementsPainter } from './components/painter'; 5 | import type { TComponentDefinitionElementsSinger } from './components/singer'; 6 | 7 | /** Type definition of an app configuration preset's component configuration. */ 8 | export type TAppComponentConfig = 9 | | { 10 | id: 'editor'; 11 | } 12 | | { 13 | id: 'menu'; 14 | flags: TFeatureFlagMenu; 15 | } 16 | | { 17 | id: 'painter'; 18 | elements?: TComponentDefinitionElementsPainter[] | true; 19 | } 20 | | { 21 | id: 'singer'; 22 | elements?: TComponentDefinitionElementsSinger[] | true; 23 | }; 24 | 25 | /** Type defintion of an app configuration preset. */ 26 | export interface IAppConfig { 27 | /** Configuration name. */ 28 | name: string; 29 | /** Configuration description. */ 30 | desc: string; 31 | /** Global flags. */ 32 | env: { 33 | /** i18n language. */ 34 | lang: TI18nLang; 35 | }; 36 | /** List of components to load */ 37 | components: TAppComponentConfig[]; 38 | } 39 | 40 | /** Type definition for the map of items imported at app load. */ 41 | export type TAppImportMap = { 42 | lang: Partial>; 43 | assets: Record; 44 | components: Partial>; 45 | }; 46 | -------------------------------------------------------------------------------- /@types/assets.d.ts: -------------------------------------------------------------------------------- 1 | /** Type definition for each asset item's manifest. */ 2 | export type TAssetManifest = { 3 | /** Path to the asset file relative to `src/assets/` */ 4 | path: string; 5 | /** Metadata associated with the asset file. */ 6 | meta?: { 7 | [key: string]: boolean | number | string; 8 | }; 9 | }; 10 | 11 | /** File type of asset. */ 12 | export type TAssetType = 13 | | 'audio/mp3' 14 | | 'audio/ogg' 15 | | 'audio/wave' 16 | | 'image/gif' 17 | | 'image/jpg' 18 | | 'image/png' 19 | | 'image/svg+xml'; 20 | 21 | /** Type definition for an asset. */ 22 | export type TAsset = { 23 | /** File type of asset. */ 24 | type: TAssetType; 25 | /** Asset data. */ 26 | data: string; 27 | } & Pick; 28 | -------------------------------------------------------------------------------- /@types/components/editor.d.ts: -------------------------------------------------------------------------------- 1 | import type { TAsset } from '../assets'; 2 | 3 | export type TInjectedEditor = { 4 | flags: undefined; 5 | i18n: Record<'editor.build' | 'editor.help', string>; 6 | assets: Record< 7 | | 'image.icon.build' 8 | | 'image.icon.help' 9 | | 'image.icon.pin' 10 | | 'image.icon.unpin' 11 | | 'image.icon.code' 12 | | 'image.icon.close', 13 | TAsset 14 | >; 15 | }; 16 | -------------------------------------------------------------------------------- /@types/components/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IElementSpecification } from '@sugarlabs/musicblocks-v4-lib'; 2 | import type { TAsset } from '../assets'; 3 | 4 | /** Interface representing a component's API. */ 5 | export interface IComponent { 6 | /** Mounts the component (loads subcomponents, mounts DOM elements, etc.). */ 7 | mount(): Promise; 8 | /** Sets up the component — initializes component after it is mounted. */ 9 | setup(): Promise; 10 | /** Items injected into the component after load. */ 11 | injected: { 12 | /** Feature flags. */ 13 | flags?: { [flag: string]: boolean }; 14 | /** i18n strings. */ 15 | i18n?: { [key: string]: string }; 16 | /** Asset entries. */ 17 | assets?: { [assetId: string]: TAsset }; 18 | }; 19 | /** Syntax elements exposed. */ 20 | elements?: Record; 21 | } 22 | 23 | // ------------------------------------------------------------------------------------------------- 24 | 25 | /** Component identifier string. */ 26 | export type TComponentId = 'menu' | 'editor' | 'painter' | 'singer'; 27 | 28 | /** Type definition for each component's definition object. */ 29 | export interface IComponentDefinition { 30 | /** Dependent components. */ 31 | dependencies: { 32 | /** List of identifiers of required dependent components. */ 33 | required: TComponentId[]; 34 | /** List of identifiers of optional dependent components. */ 35 | optional: TComponentId[]; 36 | }; 37 | /** Feature flag map. */ 38 | flags: { 39 | [flag: string]: 'boolean'; 40 | }; 41 | /** i18n string identifier - description map. */ 42 | strings: { 43 | [string: string]: string; 44 | }; 45 | /** Assets used. */ 46 | assets: string[]; 47 | } 48 | 49 | /** 50 | * Type definition for each component's definition object extended with it's exposed Syntax elements. 51 | */ 52 | export interface IComponentDefinitionExtended extends IComponentDefinition { 53 | elements?: Omit; 54 | } 55 | 56 | /** Type that represents the map of each component identifier with related fields. */ 57 | export type TComponentManifest = Record< 58 | TComponentId, 59 | { 60 | /** Display name of the component. */ 61 | name: string; 62 | /** Description of the component. */ 63 | desc: string; 64 | /** Component definition. */ 65 | definition: IComponentDefinition; 66 | /** Import function. */ 67 | importFunc(): Promise; 68 | } 69 | >; 70 | -------------------------------------------------------------------------------- /@types/components/menu.d.ts: -------------------------------------------------------------------------------- 1 | import type { TAsset } from '../assets'; 2 | 3 | /** Type definition for feature flag toggles for the Menu component. */ 4 | export type TFeatureFlagMenu = { 5 | uploadFile: boolean; 6 | recording: boolean; 7 | exportDrawing: boolean; 8 | loadProject: boolean; 9 | saveProject: boolean; 10 | }; 11 | 12 | /** Type definition for the i18n identifiers defined by the Menu component. */ 13 | export type TI18nMenu = 'menu.run' | 'menu.stop' | 'menu.reset'; 14 | 15 | /** Type definition for the asset identifiers defined by the Menu component. */ 16 | export type TAssetIdentifierMenu = 17 | | 'image.icon.run' 18 | | 'image.icon.stop' 19 | | 'image.icon.reset' 20 | | 'image.icon.saveProjectHTML' 21 | | 'image.icon.exportDrawing' 22 | | 'image.icon.startRecording' 23 | | 'image.icon.stopRecording' 24 | | 'image.icon.uploadFile' 25 | | 'image.icon.loadProject'; 26 | 27 | /** Type definition for the items injected into the Menu component after load. */ 28 | export type TInjectedMenu = { 29 | flags: Record; 30 | i18n: Record; 31 | assets: Record; 32 | }; 33 | 34 | /** Type definition for the events emitted by the Menu component. */ 35 | export type TEventMenu = 36 | | 'menu.run' 37 | | 'menu.stop' 38 | | 'menu.reset' 39 | | 'menu.uploadFile' 40 | | 'menu.startRecording' 41 | | 'menu.stopRecording' 42 | | 'menu.exportDrawing' 43 | | 'menu.loadProject' 44 | | 'menu.saveProject'; 45 | 46 | /** Type definition for the props to the Menu React component. */ 47 | export type TPropsMenu = { 48 | injected: TInjectedMenu; 49 | states: Record<'running', boolean>; 50 | handlers: Partial>; 51 | }; 52 | -------------------------------------------------------------------------------- /@types/components/painter.d.ts: -------------------------------------------------------------------------------- 1 | import type { TAsset } from '../assets'; 2 | 3 | export type TComponentDefinitionElementsPainter = 4 | | 'move-forward' 5 | | 'move-backward' 6 | | 'turn-left' 7 | | 'turn-right' 8 | | 'set-xy' 9 | | 'set-heading' 10 | | 'draw-arc' 11 | | 'set-color' 12 | | 'set-thickness' 13 | | 'pen-up' 14 | | 'pen-down' 15 | | 'set-background' 16 | | 'clear'; 17 | 18 | export type TInjectedPainter = { 19 | flags: undefined; 20 | i18n: undefined; 21 | assets: Record<'image.icon.mouse', TAsset>; 22 | }; 23 | -------------------------------------------------------------------------------- /@types/components/singer.d.ts: -------------------------------------------------------------------------------- 1 | import { TAsset } from '../assets'; 2 | 3 | export type TComponentDefinitionElementsSinger = 4 | | 'test-synth' 5 | | 'play-note' 6 | | 'reset-notes-played' 7 | | 'play-generic' 8 | | 'play-interval'; 9 | 10 | export type TInjectedSinger = { 11 | flags: undefined; 12 | i18n: undefined; 13 | assets: Record<'audio.guitar' | 'audio.piano' | 'audio.snare', TAsset>; 14 | }; 15 | -------------------------------------------------------------------------------- /@types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.png'; 4 | declare module '*.svg' { 5 | const content: string; 6 | export default content; 7 | } 8 | 9 | declare module '*.jsonc'; 10 | 11 | declare module '*.wasm'; 12 | -------------------------------------------------------------------------------- /@types/events.d.ts: -------------------------------------------------------------------------------- 1 | import type { TEventMenu } from './components/menu'; 2 | 3 | /** Type definition for event names. */ 4 | export type TEvent = TEventMenu; 5 | -------------------------------------------------------------------------------- /@types/i18n.d.ts: -------------------------------------------------------------------------------- 1 | /** Type representing the allowed i18n language name strings. */ 2 | export type TI18nLang = 'en' | 'es'; 3 | 4 | /** Type representing the schema of a i18n language file which stores the individual strings. */ 5 | export type TI18nFile = { 6 | [key: string]: string; 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.1.0 [2022-03-05] 4 | 5 | - Updates README text. 6 | - Adds the feature to author programs in the _Editor_ using _YAML_. 7 | - Updates vulnerable dependent package `url-parse` version. 8 | 9 | ## 4.0.1 [2022-02-15] 10 | 11 | - Updates core dependency `musicblocks-v4-lib` version from `1.0.1` to prerelease `0.2.0`. 12 | 13 | ## 4.0.0 [2022-02-13] 14 | 15 | - Sets up a _Node.js_ project for _React_ using `create-react-app`. 16 | - Bundles the _Programming Framework_ ([musicblocks-v4-lib](https://github.com/sugarlabs/musicblocks-v4-lib)). 17 | - Adds a basic _View Framework_ that creates a _workspace_ wrapper, and _toolbar_ buttons and wrappers. 18 | - Adds the _Config_ component that creates a dependency tree of components and dynamically loads them. 19 | - Adds the _Painter_ component with a single SVG _sprite_. 20 | - Adds basic _Syntax Elements_ for _Painter_ instructions. 21 | - Adds a basic _Editor_ to author programs using _Painter_ instructions. 22 | - Displays the _Painter_ instructions API in the _Editor_. 23 | - Adds a basic _Menu_ to run and reset programs authored in the _Editor_. 24 | - Adds project documentation. 25 | - Adds `lint` and `CD` workflows. 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base from official Node (Alpine LTS) image 2 | FROM node:lts-alpine 3 | 4 | # Install system dependencies for native modules and Python distutils 5 | RUN apk update && apk add --no-cache \ 6 | build-base \ 7 | python3 \ 8 | py3-setuptools 9 | 10 | # Install simple http server for serving static content 11 | RUN npm install -g http-server 12 | 13 | # Install TypeScript compiler 14 | RUN npm install -g typescript 15 | 16 | # Install ts-node (to run/debug .ts files without manual transpiling) 17 | RUN npm install -g ts-node 18 | 19 | # Set /app as working directory (in development mode for mounting source code) 20 | WORKDIR /app 21 | 22 | # Override default CMD for image ("node"): launch the shell 23 | CMD sh 24 | 25 | # Listen on ports 26 | EXPOSE 5173 4173 27 | 28 | # Add label for GitHub container registry 29 | LABEL org.opencontainers.image.description='An initial development image based on the official \ 30 | Node.js (on Alpine LTS) image, and further configured with a HTTP server, TypeScript compiler, \ 31 | and ts-node. This is merely to provide an execution sandbox and does not contain source files.' 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Music Blocks (v4) 2 | 3 | A complete overhaul of [Music Blocks](https://github.com/sugarlabs/musicblocks). 4 | 5 | ## Tech Stack 6 | 7 | Music Blocks (v4) is a client-side web application written in _TypeScript_. _React_ is used to render 8 | UI components, however, the project is set up to independently use any _JavaScript_ UI library/framework 9 | or the _JS DOM API_ directly. It is bundled using _Vite_. 10 | 11 | - Application 12 | - TypeScript 4 13 | - React 17 14 | - SCSS 15 | 16 | - Tooling 17 | - Node.js 18 | - Vite 4 19 | - ESLint 20 | - Docker 21 | 22 | - Testing 23 | - Jest 24 | - Cypress 25 | 26 | ## Development 27 | 28 | See [**full development guide**](./docs/DEV.md). 29 | 30 | ## Contributing 31 | 32 | There is a [Music Blocks (v4)](https://github.com/orgs/sugarlabs/projects/9) _GitHub project_ which 33 | is used for task management. You can visit it from the 34 | [projects](https://github.com/sugarlabs/musicblocks-v4/projects?query=is%3Aopen) tab at the top of 35 | the repository. In addition, please visit the 36 | [discussions](https://github.com/sugarlabs/musicblocks-v4/discussions) tab at the top of the repository 37 | to follow and/or discuss about the planning progress. 38 | 39 | Parallel development of the programming framework will be done in the 40 | [**musicblocks-v4-lib**](https://github.com/sugarlabs/musicblocks-v4-lib) repository as mentioned 41 | above. For updates, follow the `develop` branch and the feature branches that branch out of it. 42 | Please look out for _Issues_ tab of both repositories. 43 | 44 | **Note:** There is no need to ask permission to work on an issue. You should check for pull requests 45 | linked to an issue you are addressing; if there are none, then assume nobody has done anything. Begin 46 | to fix the problem, test, make your commits, push your commits, then make a pull request. Mention an 47 | issue number in the pull request, but not the commit message. These practices allow the competition 48 | of ideas (Sugar Labs is a meritocracy). 49 | 50 | See [**full contributing guide**](./docs/CONTRIBUTING.md). 51 | -------------------------------------------------------------------------------- /app/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfigExport } from 'vite'; 2 | 3 | import path from 'path'; 4 | import { mergeConfig } from 'vite'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | function resolve(rootPath: string) { 9 | return path.resolve(__dirname, '..', rootPath); 10 | } 11 | export default { 12 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(tsx|ts|jsx|js)'], 13 | addons: [ 14 | '@storybook/addon-links', 15 | '@storybook/addon-essentials', 16 | '@storybook/addon-interactions', 17 | ], 18 | framework: { 19 | name: '@storybook/react-vite', 20 | options: {}, 21 | }, 22 | docs: { 23 | autodocs: 'tag', 24 | }, 25 | async viteFinal(config: UserConfigExport) { 26 | return mergeConfig(config, { 27 | resolve: { 28 | alias: { 29 | '@': resolve('src'), 30 | '@res': resolve('../res'), 31 | }, 32 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 33 | }, 34 | 35 | envDir: resolve('env'), 36 | 37 | build: { 38 | chunkSizeWarningLimit: 1024, 39 | }, 40 | }); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /app/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /app/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '@res/scss/base.scss'; 2 | 3 | export const parameters = { 4 | actions: { 5 | argTypesRegex: '^on[A-Z].*', 6 | }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /app/env/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_VIEW_OVERLAY_TRANSITION=1000 2 | VITE_APP_VIEW_OVERLAY_TRANSITION_BUFFER=50 3 | VITE_APP_SPLASH_MIN_DELAY=1000 4 | -------------------------------------------------------------------------------- /app/env/.env.development: -------------------------------------------------------------------------------- 1 | // Configuration preset file number to enable 2 | VITE_CONFIG_PRESET = 0 3 | -------------------------------------------------------------------------------- /app/env/.env.production: -------------------------------------------------------------------------------- 1 | // Configuration preset file number to enable 2 | VITE_CONFIG_PRESET = 0 3 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-app", 3 | "version": "4.2.0", 4 | "description": "Music Blocks (v4) main application", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "serve": "vite -c tools/vite.config.ts --host --port 5173", 9 | "stats": "ts-node -O \"{ \\\"types\\\": [\\\"node\\\"] }\" tools/scripts/stats", 10 | "build": "vite build -c tools/vite.config.ts --base / && npm run stats", 11 | "build:gh": "vite build -c tools/vite.config.ts --base /musicblocks-v4/ && npm run stats", 12 | "preview": "vite preview -c tools/vite.config.ts --host --port 4173 --base /", 13 | "preview:gh": "vite preview -c tools/vite.config.ts --host --port 4173 --base /musicblocks-v4/", 14 | "visualize": "npm run preview -- --open http://localhost:4173/stats.html", 15 | "predeploy": "npm run build:gh", 16 | "deploy": "gh-pages -d dist", 17 | "storybook": "storybook dev -p 6006 --no-open", 18 | "check": "tsc", 19 | "lint": "eslint src" 20 | }, 21 | "dependencies": { 22 | "@sugarlabs/mb4-assets": "*", 23 | "@sugarlabs/mb4-events": "*", 24 | "@sugarlabs/mb4-transport": "*", 25 | "@sugarlabs/mb4-view": "*", 26 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0", 27 | "react": "^18.0.0", 28 | "react-dom": "^18.0.0", 29 | "web-vitals": "^2.1.4" 30 | }, 31 | "devDependencies": { 32 | "gh-pages": "^3.2.3", 33 | "jsonc-parser": "^3.2.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #ffffff 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /app/public/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /app/public/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /app/public/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /app/public/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /app/public/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /app/public/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/apple-icon.png -------------------------------------------------------------------------------- /app/public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /app/public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /app/public/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /app/public/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /app/public/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /app/public/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /app/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/app/public/logo.png -------------------------------------------------------------------------------- /app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Music Blocks", 3 | "name": "Music Blocks - A Musical Microworld", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "icons/android-icon-36x36.png", 12 | "sizes": "36x36", 13 | "type": "image/png", 14 | "density": "0.75" 15 | }, 16 | { 17 | "src": "icons/android-icon-48x48.png", 18 | "sizes": "48x48", 19 | "type": "image/png", 20 | "density": "1.0" 21 | }, 22 | { 23 | "src": "icons/android-icon-72x72.png", 24 | "sizes": "72x72", 25 | "type": "image/png", 26 | "density": "1.5" 27 | }, 28 | { 29 | "src": "icons/android-icon-96x96.png", 30 | "sizes": "96x96", 31 | "type": "image/png", 32 | "density": "2.0" 33 | }, 34 | { 35 | "src": "icons/android-icon-144x144.png", 36 | "sizes": "144x144", 37 | "type": "image/png", 38 | "density": "3.0" 39 | }, 40 | { 41 | "src": "icons/android-icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "density": "4.0" 45 | } 46 | ], 47 | "start_url": ".", 48 | "display": "standalone", 49 | "theme_color": "#1b9cfc", 50 | "background_color": "#ffffff" 51 | } 52 | -------------------------------------------------------------------------------- /app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/src/components.ts: -------------------------------------------------------------------------------- 1 | import type { TComponentManifest } from '#/@types/components'; 2 | 3 | const manifest: TComponentManifest = { 4 | editor: { 5 | name: 'Editor', 6 | desc: 'Code editor for programs', 7 | definition: { 8 | dependencies: { 9 | optional: ['menu'], 10 | required: [], 11 | }, 12 | flags: {}, 13 | strings: { 14 | 'editor.build': 'build button - build the program', 15 | 'editor.help': 'help button - show syntax information', 16 | }, 17 | assets: [ 18 | 'image.icon.build', 19 | 'image.icon.help', 20 | 'image.icon.pin', 21 | 'image.icon.unpin', 22 | 'image.icon.code', 23 | 'image.icon.close', 24 | ], 25 | }, 26 | importFunc: () => import('@sugarlabs/mb4-module-editor'), 27 | }, 28 | menu: { 29 | name: 'Menu', 30 | desc: 'Menubar of the Primary Toolbar', 31 | definition: { 32 | dependencies: { 33 | optional: [], 34 | required: [], 35 | }, 36 | flags: { 37 | uploadFile: 'boolean', 38 | recording: 'boolean', 39 | exportDrawing: 'boolean', 40 | loadProject: 'boolean', 41 | saveProject: 'boolean', 42 | }, 43 | strings: { 44 | 'menu.run': 'run button - to start the program execution', 45 | 'menu.stop': 'stop button - to stop the program execution', 46 | 'menu.reset': 'reset button - clear program states', 47 | }, 48 | assets: [ 49 | 'image.icon.run', 50 | 'image.icon.stop', 51 | 'image.icon.reset', 52 | 'image.icon.saveProjectHTML', 53 | 'image.icon.exportDrawing', 54 | 'image.icon.startRecording', 55 | 'image.icon.stopRecording', 56 | 'image.icon.uploadFile', 57 | 'image.icon.loadProject', 58 | ], 59 | }, 60 | importFunc: () => import('@sugarlabs/mb4-module-menu'), 61 | }, 62 | painter: { 63 | name: 'Painter', 64 | desc: 'Allows creation of artwork in the workspace', 65 | definition: { 66 | dependencies: { 67 | optional: ['menu'], 68 | required: [], 69 | }, 70 | flags: {}, 71 | strings: {}, 72 | assets: [ 73 | 'image.icon.mouse', 74 | // 75 | ], 76 | }, 77 | importFunc: () => import('@sugarlabs/mb4-module-painter'), 78 | }, 79 | singer: { 80 | name: 'Singer', 81 | desc: 'Allows creation of music in the workspace', 82 | definition: { 83 | dependencies: { 84 | optional: ['menu'], 85 | required: [], 86 | }, 87 | flags: {}, 88 | strings: {}, 89 | assets: [], 90 | }, 91 | importFunc: () => import('@sugarlabs/mb4-module-singer'), 92 | }, 93 | }; 94 | 95 | export default manifest; 96 | -------------------------------------------------------------------------------- /app/src/config/ConfigPage.tsx: -------------------------------------------------------------------------------- 1 | import type { IAppConfig } from '#/@types/app'; 2 | import type { IComponentDefinitionExtended, TComponentId } from '#/@types/components'; 3 | 4 | import Config from './Config'; 5 | 6 | // -- stylesheet ----------------------------------------------------------------------------------- 7 | 8 | import './index.scss'; 9 | 10 | // -- component definition ------------------------------------------------------------------------- 11 | 12 | export default function (props: { 13 | /** App configurations. */ 14 | config: IAppConfig; 15 | /** Map of component definitions. */ 16 | definitions: Partial>; 17 | /** Callback for when configurations are updated. */ 18 | handlerUpdate: (config: IAppConfig) => unknown; 19 | }): JSX.Element { 20 | // --------------------------------------------------------------------------- 21 | 22 | return ( 23 |
24 |
25 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/src/config/preset/preset-0.ts: -------------------------------------------------------------------------------- 1 | import type { IAppConfig } from '#/@types/app'; 2 | 3 | export const appConfig: IAppConfig = { 4 | name: 'Production', 5 | desc: 'Production Configuration', 6 | env: { 7 | lang: 'en', 8 | }, 9 | components: [ 10 | { 11 | id: 'menu', 12 | flags: { 13 | uploadFile: false, 14 | recording: false, 15 | exportDrawing: false, 16 | loadProject: false, 17 | saveProject: false, 18 | }, 19 | }, 20 | { 21 | id: 'editor', 22 | }, 23 | { 24 | id: 'painter', 25 | elements: true, 26 | }, 27 | ], 28 | }; 29 | 30 | export default appConfig; 31 | -------------------------------------------------------------------------------- /app/src/splash/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/themes/light'; 2 | @import '@res/scss/sizes'; 3 | 4 | #splash-container { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | gap: 24px; 10 | width: 100%; 11 | height: 100%; 12 | padding-bottom: 196px; 13 | background-color: $c-bg-white; 14 | 15 | #splash-logo { 16 | width: 120px; 17 | height: 120px; 18 | height: auto; 19 | } 20 | 21 | #splash-progress-bar { 22 | width: 320px; 23 | height: 8px; 24 | border-radius: $s-border-radius; 25 | background-color: $c-bg-grey; 26 | 27 | #splash-progress { 28 | height: 100%; 29 | border-radius: $s-border-radius; 30 | background-color: $mb-accent-light; 31 | transition: width 0.25s ease; 32 | } 33 | } 34 | } 35 | 36 | #stories-splash-wrapper { 37 | width: 100%; 38 | height: 100vh; 39 | } 40 | -------------------------------------------------------------------------------- /app/src/splash/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Splash } from '.'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export default { 8 | title: 'View/Splash', 9 | component: Splash, 10 | decorators: [ 11 | (Story) => ( 12 |
13 | 14 |
15 | ), 16 | ], 17 | argTypes: { 18 | progress: { 19 | options: [0, 20, 40, 60, 80, 100], 20 | control: { type: 'select' }, 21 | }, 22 | }, 23 | parameters: { 24 | layout: 'fullscreen', 25 | }, 26 | render: (args, { loaded: { logo } }) => , 27 | loaders: [ 28 | async () => { 29 | const { importAssets, getAsset } = await import('@sugarlabs/mb4-assets'); 30 | const assetManifest = (await import('@sugarlabs/mb4-assets')).default; 31 | await importAssets( 32 | Object.entries(assetManifest).map(([identifier, manifest]) => ({ identifier, manifest })), 33 | () => undefined, 34 | ); 35 | return { 36 | logo: getAsset('image.logo'), 37 | }; 38 | }, 39 | ], 40 | } as Meta; 41 | 42 | type Story = StoryObj; 43 | 44 | // ------------------------------------------------------------------------------------------------- 45 | 46 | export const SplashProgress: Story = { 47 | args: { 48 | progress: 0, 49 | }, 50 | name: 'Progress', 51 | }; 52 | -------------------------------------------------------------------------------- /app/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | /** 4 | * If you want to start measuring performance in your app, pass a function to log results 5 | * (e.g.: reportWebVitals(console.log)) 6 | * @param onPerfEntry 7 | */ 8 | export function reportWebVitals(onPerfEntry?: ReportHandler): void { 9 | if (onPerfEntry && onPerfEntry instanceof Function) { 10 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 11 | getCLS(onPerfEntry); 12 | getFID(onPerfEntry); 13 | getFCP(onPerfEntry); 14 | getLCP(onPerfEntry); 15 | getTTFB(onPerfEntry); 16 | }); 17 | } 18 | } 19 | 20 | /** 21 | * Used to load service worker in production only. 22 | */ 23 | export function loadServiceWorker() { 24 | const BASE_URL = import.meta.env.BASE_URL ?? window?.location?.pathname ?? '/'; 25 | const ENABLE_SW = import.meta.env.DEV ? false : true; 26 | 27 | if ('serviceWorker' in navigator && ENABLE_SW) { 28 | window.addEventListener('load', () => { 29 | navigator.serviceWorker 30 | .register(`${BASE_URL}sw.js`) 31 | .then((registration) => { 32 | console.log('SW registered: ', registration); 33 | }) 34 | .catch((registrationError) => { 35 | console.log('SW registration failed: ', registrationError); 36 | }); 37 | }); 38 | } 39 | } 40 | 41 | /** 42 | * Used to construct a string URL to the location of the WASM module which is included as an asset. 43 | * 44 | * Usage: 45 | * import wasmModule from '/path/to/file.wasm'; 46 | * const urlToWasmFile = constructWasmUrl(wasmModule); 47 | * 48 | * @param importPath the WASM module import 49 | * @returns URL to WASM module included as asset 50 | */ 51 | export function constructWasmUrl(importPath: string): string { 52 | if (import.meta.env.PROD) { 53 | return window.location.origin + window.location.pathname + importPath; 54 | } else { 55 | return importPath; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx", 6 | "./.storybook/**/*.ts", 7 | "./.storybook/**/*.tsx" 8 | ], 9 | "compilerOptions": { 10 | "baseUrl": ".", 11 | "paths": { 12 | "@/*": [ 13 | "./src/*" 14 | ], 15 | "#/@types/*": [ 16 | "../@types/*" 17 | ], 18 | "@res/*": [ 19 | "../res/*" 20 | ] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | npx lerna run coverage 6 | 7 | rm -rf coverage 8 | mkdir coverage 9 | 10 | rm -rf coverage-temp 11 | mkdir coverage-temp 12 | 13 | cp modules/singer/coverage/coverage-final.json coverage-temp/coverage-module-singer.json 14 | 15 | npx nyc merge coverage-temp coverage-temp/merged-coverage.json 16 | npx nyc report -t coverage-temp --report-dir coverage --reporter=html --reporter=cobertura --reporter=text 17 | 18 | rm -rf coverage-temp 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | musicblocks: 3 | build: ./ 4 | image: ghcr.io/sugarlabs/musicblocks:4-dev 5 | ports: 6 | - '5173:5173' # development server 7 | - '4173:4173' # production preview server 8 | volumes: 9 | - ./:/app/ 10 | container_name: musicblocks-4-dev 11 | stdin_open: true 12 | tty: true 13 | -------------------------------------------------------------------------------- /docs/images/architecture/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/docs/images/architecture/components.png -------------------------------------------------------------------------------- /docs/images/wireframe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/docs/images/wireframe.jpg -------------------------------------------------------------------------------- /docs/webpack-choices.md: -------------------------------------------------------------------------------- 1 | # Various Architectural Choices and the thought processes behind them 2 | 3 | ## Production Build Optimizations 4 | 5 | - Setup webpack bundle analyzer - the largest bundles are 6 | - `p5` (extensively used) 7 | - `esprima` (which is a peerdep of `js-yaml`, only used in one place). 8 | - Also had a look at bundle compression methods - `gzip` and `brotli` 9 | - the drawbacks are these need to be enabled at a web server level 10 | - support varies in static files hosts. Will be using compression-webpack-plugin for this. 11 | - `p5` - production build lib - `p5.min.js` - 800KB -> compressed to 197KB 12 | - GitHub pages supports `gzip` but doesnt support `brotli` 13 | - Did not consider mangling output code (uglify.js and the likes) 14 | - Used `webpack-compression-plugin` with gzip and a threshold of 50KB to compress files 15 | 16 | ## Compression Algorithm Choice: `gzip` vs `brotli` 17 | 18 | ### gzip 19 | 20 | - generic file compression format 21 | - can be used to compress build files for serving a site 22 | - decent compression ratio 23 | - fast compression/decompression 24 | - needs to be enabled at web server level 25 | 26 | ### brotli 27 | 28 | - generic lossless compression algorithm built specifically for web pages 29 | - larger compression ratio than gzip 30 | - faster compression/decompression than gzip 31 | - lesser support from static site hosts (GitHub pages doesn’t support brotli yet) 32 | 33 | ## Final Choice: `gzip` 34 | 35 | ### Reasons 36 | 37 | - supported by GitHub pages 38 | - older and more supported by a large variety of systems 39 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "npmClient": "npm", 4 | "useWorkspaces": true, 5 | "version": "4.2.0" 6 | } 7 | -------------------------------------------------------------------------------- /lib/assets/index.ts: -------------------------------------------------------------------------------- 1 | export { getAsset, getAssets, importAsset, importAssets } from './src'; 2 | 3 | export { default } from './res'; 4 | -------------------------------------------------------------------------------- /lib/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-assets", 3 | "version": "4.2.0", 4 | "description": "Asset Management Utilities", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/assets/res/audio/guitar.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/lib/assets/res/audio/guitar.wav -------------------------------------------------------------------------------- /lib/assets/res/audio/piano.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/lib/assets/res/audio/piano.wav -------------------------------------------------------------------------------- /lib/assets/res/audio/snare.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/lib/assets/res/audio/snare.wav -------------------------------------------------------------------------------- /lib/assets/res/image/icon/build.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/close.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/code.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/exportDrawing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/help.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/loadProject.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/pin.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/reset.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/run.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 33 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/saveProjectHTML.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/startRecording.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/stop.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 33 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/stopRecording.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/unpin.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/assets/res/image/icon/uploadFile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/assets/res/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/lib/assets/res/image/logo.png -------------------------------------------------------------------------------- /lib/assets/src/loaders.ts: -------------------------------------------------------------------------------- 1 | import { TAsset, TAssetType } from '#/@types/assets'; 2 | 3 | /** 4 | * Stores the loader map. 5 | * @description A loader transforms the asset data from `base64` if required. 6 | */ 7 | const loaders: Partial Promise>> = { 8 | 'image/svg+xml': async (asset: TAsset) => { 9 | return fetch(asset.data) 10 | .then((res) => res.text()) 11 | .then((res) => ({ ...asset, data: res })); 12 | }, 13 | }; 14 | 15 | export default loaders; 16 | -------------------------------------------------------------------------------- /lib/assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/components/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfigExport } from 'vite'; 2 | 3 | import path from 'path'; 4 | import { mergeConfig } from 'vite'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | function resolve(rootPath: string) { 9 | return path.resolve(__dirname, '..', rootPath); 10 | } 11 | 12 | export default { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(tsx|ts|jsx|js)'], 14 | addons: [ 15 | '@storybook/addon-links', 16 | '@storybook/addon-essentials', 17 | '@storybook/addon-interactions', 18 | ], 19 | framework: { 20 | name: '@storybook/react-vite', 21 | options: {}, 22 | }, 23 | docs: { 24 | autodocs: 'tag', 25 | }, 26 | async viteFinal(config: UserConfigExport) { 27 | return mergeConfig(config, { 28 | resolve: { 29 | alias: { 30 | '@': resolve('src'), 31 | '@res': resolve('../../res'), 32 | }, 33 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 34 | }, 35 | 36 | envDir: resolve('env'), 37 | 38 | build: { 39 | chunkSizeWarningLimit: 1024, 40 | }, 41 | }); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /lib/components/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /lib/components/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '@res/scss/base.scss'; 2 | 3 | export const parameters = { 4 | actions: { 5 | argTypesRegex: '^on[A-Z].*', 6 | }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as WToggleSwitch } from './src/WToggleSwitch'; 2 | export { default as WToggleSwitchRounded } from './src/WToggleSwitchRounded'; 3 | export { default as WTextButton } from './src/WTextButton'; 4 | export { default as WIconButton } from './src/WIconButton'; 5 | 6 | export { default as SImage } from './src/SImage'; 7 | export { default as SImageVector } from './src/SImageVector'; 8 | export { default as SImageRaster } from './src/SImageRaster'; 9 | -------------------------------------------------------------------------------- /lib/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-components", 3 | "version": "4.2.0", 4 | "description": "Library of common React pure components", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "storybook": "storybook dev -p 6006", 9 | "check": "tsc --types 'vite/client'", 10 | "lint": "eslint src" 11 | }, 12 | "dependencies": { 13 | "@sugarlabs/mb4-assets": "*" 14 | }, 15 | "peerDependencies": { 16 | "react": "~18.x", 17 | "react-dom": "~18.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/components/src/SImage/index.tsx: -------------------------------------------------------------------------------- 1 | import type { TAsset } from '#/@types/assets'; 2 | 3 | // -- ui items ------------------------------------------------------------------------------------- 4 | 5 | import { SImageRaster, SImageVector } from '../..'; 6 | 7 | // -- component definition ------------------------------------------------------------------------- 8 | 9 | /** 10 | * React component definition for a generic Image component. 11 | */ 12 | export default function (props: { 13 | /** Image asset. */ 14 | asset: TAsset; 15 | }): JSX.Element { 16 | const type = props.asset.type; 17 | 18 | // --------------------------------------------------------------------------- 19 | 20 | return ( 21 | <> 22 | {type === 'image/svg+xml' && } 23 | {type.startsWith('image') && type !== 'image/svg+xml' && ( 24 | 25 | )} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/components/src/SImageRaster/index.scss: -------------------------------------------------------------------------------- 1 | .l-image-raster { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /lib/components/src/SImageRaster/index.tsx: -------------------------------------------------------------------------------- 1 | // -- stylesheet ----------------------------------------------------------------------------------- 2 | 3 | import './index.scss'; 4 | 5 | // -- component definition ------------------------------------------------------------------------- 6 | 7 | /** 8 | * React component definition for Image Raster component. 9 | */ 10 | export default function (props: { 11 | /** Source string as URL data. */ 12 | content: string; 13 | }): JSX.Element { 14 | // --------------------------------------------------------------------------- 15 | 16 | return ( 17 | <> 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/components/src/SImageVector/index.scss: -------------------------------------------------------------------------------- 1 | .l-image-vector { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | svg { 9 | width: inherit; 10 | height: inherit; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/components/src/SImageVector/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | // -- stylesheet ----------------------------------------------------------------------------------- 4 | 5 | import './index.scss'; 6 | 7 | // -- component definition ------------------------------------------------------------------------- 8 | 9 | /** 10 | * React component definition for Image Vector component. 11 | */ 12 | export default function (props: { 13 | /** SVG content string. */ 14 | content: string; 15 | }): JSX.Element { 16 | const wrapper = useRef(null); 17 | 18 | useEffect(() => { 19 | const _wrapper = wrapper.current! as HTMLDivElement; 20 | _wrapper.innerHTML = props.content; 21 | }); 22 | 23 | // --------------------------------------------------------------------------- 24 | 25 | return ( 26 | <> 27 |
28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /lib/components/src/WCheckbox/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/sizes.scss'; 2 | @import '@res/themes/light.scss'; 3 | 4 | .w-checkbox { 5 | width: 24px; 6 | height: 24px; 7 | border: 2px solid $c-bg-white; 8 | border-radius: $s-border-radius; 9 | background-color: $c-bg-grey; 10 | cursor: pointer; 11 | 12 | .w-checkbox-checkmark { 13 | display: none; 14 | position: relative; 15 | left: 2px; 16 | bottom: 2px; 17 | width: 10.5px; 18 | height: 15px; 19 | font-size: 20px; 20 | color: $text-light; 21 | } 22 | } 23 | 24 | .w-checkbox-active { 25 | background-color: $mb-accent; 26 | 27 | .w-checkbox-checkmark { 28 | display: block; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/components/src/WCheckbox/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { linkTo } from '@storybook/addon-links'; 4 | import { default as WCheckbox } from '.'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | export default { 9 | title: 'Common/WCheckbox', 10 | component: WCheckbox, 11 | parameters: { 12 | layout: 'centered', 13 | }, 14 | } as Meta; 15 | 16 | type Story = StoryObj; 17 | 18 | // ------------------------------------------------------------------------------------------------- 19 | 20 | export const Active: Story = { 21 | args: { 22 | active: true, 23 | handlerClick: linkTo('Common/WCheckbox', 'Inactive'), 24 | }, 25 | }; 26 | 27 | export const Inactive: Story = { 28 | args: { 29 | active: false, 30 | handlerClick: linkTo('Common/WCheckbox', 'Active'), 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/components/src/WCheckbox/index.tsx: -------------------------------------------------------------------------------- 1 | // -- stylesheet ----------------------------------------------------------------------------------- 2 | 3 | import './index.scss'; 4 | 5 | // -- component definition ------------------------------------------------------------------------- 6 | 7 | /** 8 | * React component definition for Checkbox widget. 9 | */ 10 | export default function (props: { 11 | /** Whether state is `on` (active) or `off`. */ 12 | active: boolean; 13 | /** Callback function for click event. */ 14 | handlerClick: CallableFunction; 15 | }): JSX.Element { 16 | // --------------------------------------------------------------------------- 17 | 18 | return ( 19 |
props.handlerClick()} 22 | > 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /lib/components/src/WIconButton/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/sizes.scss'; 2 | @import '@res/themes/light.scss'; 3 | 4 | .w-button-icon { 5 | display: grid; 6 | place-items: center; 7 | border-radius: $s-border-radius; 8 | border: none; 9 | outline: none; 10 | background-color: $background-light; 11 | cursor: pointer; 12 | 13 | svg { 14 | .path-fill { 15 | fill: $mb-accent; 16 | } 17 | } 18 | 19 | &.w-button-icon-lg { 20 | width: 40px; 21 | height: 40px; 22 | padding: 4px; 23 | } 24 | 25 | &.w-button-icon-sm { 26 | width: 24px; 27 | height: 24px; 28 | padding: 2px; 29 | } 30 | } 31 | 32 | #stories-button-wrapper { 33 | height: max-content; 34 | padding: 0.5rem; 35 | background-color: $mb-accent-dark; 36 | } 37 | -------------------------------------------------------------------------------- /lib/components/src/WIconButton/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { default as WIconButton } from '.'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export default { 8 | title: 'Common/WIconButton', 9 | component: WIconButton, 10 | decorators: [ 11 | (Story) => ( 12 |
13 | 14 |
15 | ), 16 | ], 17 | parameters: { 18 | layout: 'centered', 19 | }, 20 | argTypes: { 21 | size: { 22 | options: ['small', 'big'], 23 | control: { type: 'select' }, 24 | }, 25 | }, 26 | loaders: [ 27 | async () => { 28 | const { importAssets, getAsset } = await import('@sugarlabs/mb4-assets'); 29 | const assetManifest = (await import('@sugarlabs/mb4-assets')).default; 30 | await importAssets( 31 | Object.entries(assetManifest).map(([identifier, manifest]) => ({ identifier, manifest })), 32 | () => undefined, 33 | ); 34 | 35 | return { 36 | asset: getAsset('image.icon.build'), 37 | }; 38 | }, 39 | ], 40 | render: (args, { loaded: { asset } }) => , 41 | } as Meta; 42 | 43 | type Story = StoryObj; 44 | 45 | // ------------------------------------------------------------------------------------------------- 46 | 47 | export const ButtonSmall: Story = { 48 | args: { 49 | size: 'small', 50 | handlerClick: () => undefined, 51 | }, 52 | name: 'Size - small', 53 | }; 54 | 55 | export const ButtonBig: Story = { 56 | args: { 57 | size: 'big', 58 | handlerClick: () => undefined, 59 | }, 60 | name: 'Size - big', 61 | }; 62 | -------------------------------------------------------------------------------- /lib/components/src/WIconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import type { TAsset } from '#/@types/assets'; 2 | 3 | // -- ui items ------------------------------------------------------------------------------------- 4 | 5 | import { SImage } from '../..'; 6 | 7 | import './index.scss'; 8 | 9 | // -- component definition ------------------------------------------------------------------------- 10 | 11 | /** 12 | * React component definition for Icon Button component. 13 | */ 14 | export default function (props: { 15 | /** Choosable button size */ 16 | size: `big` | `small`; 17 | /** Asset entry. */ 18 | asset: TAsset; 19 | /** Callback function for click event. */ 20 | handlerClick: CallableFunction; 21 | }): JSX.Element { 22 | // --------------------------------------------------------------------------- 23 | 24 | return ( 25 | <> 26 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/components/src/WTextButton/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/sizes.scss'; 2 | @import '@res/themes/light.scss'; 3 | 4 | .w-button { 5 | height: 32px; 6 | border: none; 7 | color: $mb-accent; 8 | background-color: $background-light; 9 | border-radius: $s-border-radius; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | font-size: 0.9rem; 14 | } 15 | 16 | #stories-button-wrapper { 17 | height: max-content; 18 | padding: 0.5rem; 19 | background-color: $mb-accent-dark; 20 | } 21 | -------------------------------------------------------------------------------- /lib/components/src/WTextButton/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { default as WTextButton } from '.'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export default { 8 | title: 'Common/WTextButton', 9 | component: WTextButton, 10 | decorators: [ 11 | (Story) => ( 12 |
13 | 14 |
15 | ), 16 | ], 17 | parameters: { 18 | layout: 'centered', 19 | }, 20 | } as Meta; 21 | 22 | type Story = StoryObj; 23 | 24 | // ------------------------------------------------------------------------------------------------- 25 | 26 | export const Button: Story = { 27 | args: { 28 | content: 'BUTTON', 29 | handlerClick: () => undefined, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /lib/components/src/WTextButton/index.tsx: -------------------------------------------------------------------------------- 1 | // -- stylesheet ----------------------------------------------------------------------------------- 2 | 3 | import './index.scss'; 4 | 5 | // -- component definition ------------------------------------------------------------------------- 6 | 7 | /** 8 | * React component definition for Text Button widget. 9 | */ 10 | export default function (props: { 11 | /** The content of the button. */ 12 | content: string; 13 | /** Callback function for click event. */ 14 | handlerClick: CallableFunction; 15 | }): JSX.Element { 16 | // --------------------------------------------------------------------------- 17 | 18 | return ( 19 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitch/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/sizes.scss'; 2 | @import '@res/themes/light.scss'; 3 | 4 | .w-toggle { 5 | width: 44px; 6 | height: 24px; 7 | padding: 2px; 8 | border-radius: calc($s-border-radius + 2px); 9 | background-color: $c-bg-white; 10 | cursor: pointer; 11 | 12 | * { 13 | border-radius: $s-border-radius; 14 | } 15 | 16 | .w-toggle-track { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | background-color: $c-bg-grey; 21 | transition: background-color 0.5s ease; 22 | 23 | .w-toggle-handle { 24 | position: absolute; 25 | left: 0; 26 | width: 20px; 27 | height: 20px; 28 | border: 1px solid $mb-accent-light; 29 | outline: 1px solid $mb-accent-light; 30 | background-color: $c-bg-white; 31 | transition: left 0.25s ease; 32 | } 33 | } 34 | 35 | &.w-toggle-active { 36 | .w-toggle-track { 37 | background-color: $mb-accent; 38 | 39 | .w-toggle-handle { 40 | left: calc(100% - 20px); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitch/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { linkTo } from '@storybook/addon-links'; 4 | import { default as WToggleSwitch } from '.'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | export default { 9 | title: 'Common/WToggleSwitch', 10 | component: WToggleSwitch, 11 | parameters: { 12 | layout: 'centered', 13 | }, 14 | } as Meta; 15 | 16 | type Story = StoryObj; 17 | 18 | // ------------------------------------------------------------------------------------------------- 19 | 20 | export const Active: Story = { 21 | args: { 22 | active: true, 23 | handlerClick: linkTo('Common/WToggleSwitch', 'Inactive'), 24 | }, 25 | }; 26 | 27 | export const Inactive: Story = { 28 | args: { 29 | active: false, 30 | handlerClick: linkTo('Common/WToggleSwitch', 'Active'), 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | // -- stylesheet ----------------------------------------------------------------------------------- 2 | 3 | import './index.scss'; 4 | 5 | // -- component definition ------------------------------------------------------------------------- 6 | 7 | /** 8 | * React component definition for Toggle Switch widget. 9 | */ 10 | export default function (props: { 11 | /** Whether state is `on` (active) or `off`. */ 12 | active: boolean; 13 | /** Callback function for click event. */ 14 | handlerClick: CallableFunction; 15 | }): JSX.Element { 16 | // --------------------------------------------------------------------------- 17 | 18 | return ( 19 |
props.handlerClick()} 22 | > 23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitchRounded/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/sizes.scss'; 2 | @import '@res/themes/light.scss'; 3 | 4 | .w-toggle-r { 5 | width: 40px; 6 | height: 24px; 7 | padding: 2px; 8 | border-radius: 20px; 9 | background-color: $c-bg-white; 10 | cursor: pointer; 11 | 12 | .w-toggle-r-track { 13 | position: relative; 14 | width: 100%; 15 | height: 100%; 16 | border-radius: 18px; 17 | box-shadow: inset 0px 0px 4px $c-shadow; 18 | background-color: $c-bg-grey; 19 | transition: background-color 0.5s ease; 20 | 21 | .w-toggle-r-handle { 22 | position: absolute; 23 | top: 2px; 24 | left: 2px; 25 | width: 16px; 26 | height: 16px; 27 | border-radius: 8px; 28 | box-shadow: 0px 0px 4px $c-shadow; 29 | background-color: $c-bg-white; 30 | transition: left 0.25s ease; 31 | } 32 | } 33 | 34 | &.w-toggle-r-active { 35 | .w-toggle-r-track { 36 | background-color: $mb-accent; 37 | 38 | .w-toggle-r-handle { 39 | left: calc(100% - 18px); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitchRounded/index.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { linkTo } from '@storybook/addon-links'; 4 | import { WToggleSwitchRounded } from '../..'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | export default { 9 | title: 'Common/WToggleSwitchRounded', 10 | component: WToggleSwitchRounded, 11 | parameters: { 12 | layout: 'centered', 13 | }, 14 | } as Meta; 15 | 16 | type Story = StoryObj; 17 | 18 | // ------------------------------------------------------------------------------------------------- 19 | 20 | export const Active: Story = { 21 | args: { 22 | active: true, 23 | handlerClick: linkTo('Common/WToggleSwitchRounded', 'Inactive'), 24 | }, 25 | }; 26 | 27 | export const Inactive: Story = { 28 | args: { 29 | active: false, 30 | handlerClick: linkTo('Common/WToggleSwitchRounded', 'Active'), 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /lib/components/src/WToggleSwitchRounded/index.tsx: -------------------------------------------------------------------------------- 1 | // -- stylesheet ----------------------------------------------------------------------------------- 2 | 3 | import './index.scss'; 4 | 5 | // -- component definition ------------------------------------------------------------------------- 6 | 7 | /** 8 | * React component definition for Toggle Switch widget. 9 | */ 10 | export default function Toggle(props: { 11 | /** Whether state is `on` (active) or `off`. */ 12 | active: boolean; 13 | /** Callback function for click event. */ 14 | handlerClick: CallableFunction; 15 | }): JSX.Element { 16 | // --------------------------------------------------------------------------- 17 | 18 | return ( 19 |
props.handlerClick()} 22 | > 23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /lib/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ], 16 | "@res/*": [ 17 | "../../res/*" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/config/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getComponent, 3 | importComponent, 4 | importComponents, 5 | mountComponents, 6 | setupComponents, 7 | registerElements, 8 | } from './src'; 9 | 10 | export { serializeComponentDependencies } from './src/utils'; 11 | -------------------------------------------------------------------------------- /lib/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-config", 3 | "version": "4.2.0", 4 | "description": "Application feature configurator", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | }, 11 | "dependencies": { 12 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/events/index.ts: -------------------------------------------------------------------------------- 1 | export { hearEvent, emitEvent } from './src'; 2 | -------------------------------------------------------------------------------- /lib/events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-events", 3 | "version": "4.2.0", 4 | "description": "Event Handling Utilities", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/events/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { TEvent } from '#/@types/events'; 2 | 3 | // -- private variables ---------------------------------------------------------------------------- 4 | 5 | /** Map of event names and corresponding list of callbacks. */ 6 | const _eventTable: Partial> = {}; 7 | 8 | // -- public functions ----------------------------------------------------------------------------- 9 | 10 | export function hearEvent(event: 'menu.run', callback: () => unknown): void; 11 | export function hearEvent(event: 'menu.stop', callback: () => unknown): void; 12 | export function hearEvent(event: 'menu.reset', callback: () => unknown): void; 13 | export function hearEvent(event: 'menu.uploadFile', callback: (e: Event) => unknown): void; 14 | export function hearEvent(event: 'menu.startRecording', callback: () => unknown): void; 15 | export function hearEvent(event: 'menu.stopRecording', callback: () => unknown): void; 16 | export function hearEvent(event: 'menu.exportDrawing', callback: () => unknown): void; 17 | export function hearEvent(event: 'menu.loadProject', callback: (e: Event) => unknown): void; 18 | export function hearEvent(event: 'menu.saveProject', callback: () => unknown): void; 19 | /** 20 | * Adds a callback to an event. 21 | * @param event event name 22 | * @param callback callback to be called when the event is emitted 23 | */ 24 | export function hearEvent(event: TEvent, callback: CallableFunction): void { 25 | if (!(event in _eventTable)) _eventTable[event] = []; 26 | 27 | _eventTable[event]!.push(callback); 28 | } 29 | 30 | // ----------------------------------------------------------------------------- 31 | 32 | export function emitEvent(event: 'menu.run'): void; 33 | export function emitEvent(event: 'menu.stop'): void; 34 | export function emitEvent(event: 'menu.reset'): void; 35 | /** 36 | * Emits an event. 37 | * @param event event name 38 | * @param args arguments to the event callbacks 39 | */ 40 | export function emitEvent(event: TEvent, ...args: unknown[]): void { 41 | if (!(event in _eventTable) || _eventTable[event]!.length === 0) return; 42 | 43 | _eventTable[event]!.forEach((callback) => callback(...args)); 44 | } 45 | -------------------------------------------------------------------------------- /lib/events/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export { getStrings, importStrings } from './src'; 2 | -------------------------------------------------------------------------------- /lib/i18n/lang/en.ts: -------------------------------------------------------------------------------- 1 | import type { TI18nFile } from '#/@types/i18n'; 2 | 3 | const strings: TI18nFile = { 4 | 'editor.build': 'build', 5 | 'editor.help': 'help', 6 | 7 | 'menu.reset': 'reset', 8 | 'menu.run': 'run', 9 | 'menu.stop': 'stop', 10 | }; 11 | 12 | export default strings; 13 | -------------------------------------------------------------------------------- /lib/i18n/lang/es.ts: -------------------------------------------------------------------------------- 1 | import type { TI18nFile } from '#/@types/i18n'; 2 | 3 | const strings: TI18nFile = { 4 | 'editor.build': 'construir', 5 | 'editor.help': 'ayuda', 6 | 7 | 'menu.reset': 'reiniciar', 8 | 'menu.run': 'tocar', 9 | 'menu.stop': 'detener', 10 | }; 11 | 12 | export default strings; 13 | -------------------------------------------------------------------------------- /lib/i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-i18n", 3 | "version": "4.2.0", 4 | "description": "Internationalisation (i18n)", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/i18n/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { TI18nFile, TI18nLang } from '#/@types/i18n'; 2 | 3 | // -- private variables ---------------------------------------------------------------------------- 4 | 5 | let _strings: TI18nFile = {}; 6 | let _stringsEN: TI18nFile = {}; 7 | 8 | // -- public functions ----------------------------------------------------------------------------- 9 | 10 | /** 11 | * Loads dynamically the string file of requested language. 12 | * @param lang - requested language 13 | */ 14 | export async function importStrings(lang: TI18nLang = 'en') { 15 | _stringsEN = (await import('../lang/en')).default as TI18nFile; 16 | _strings = 17 | lang === 'en' 18 | ? { ..._stringsEN } 19 | : ((await import(`../lang/${lang}.ts`)).default as TI18nFile); 20 | } 21 | 22 | /** 23 | * Returns i18n string map corresponding to string keys 24 | * @param keys list of string keys 25 | */ 26 | export function getStrings(keys: string[]): { [key: string]: string } { 27 | return Object.fromEntries( 28 | keys.map((key) => [ 29 | key, 30 | key in _strings ? _strings[key] : key in _stringsEN ? _stringsEN[key] : key, 31 | ]), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/transport/index.ts: -------------------------------------------------------------------------------- 1 | export { loadProject, saveProjectHTML, uploadFileInLocalStorage } from './src'; 2 | -------------------------------------------------------------------------------- /lib/transport/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-transport", 3 | "version": "4.2.0", 4 | "description": "Utilities for export and import", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | }, 11 | "dependencies": { 12 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/transport/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/view/index.ts: -------------------------------------------------------------------------------- 1 | export { createItem } from './src'; 2 | export { initView, setView, mountViewOverlay, unmountViewOverlay } from './src/components'; 3 | export { setToolbarExtended, unsetToolbarExtended } from './src/components/toolbar'; 4 | -------------------------------------------------------------------------------- /lib/view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-view", 3 | "version": "4.2.0", 4 | "description": "View SDK", 5 | "private": "true", 6 | "main": "index.ts", 7 | "scripts": { 8 | "check": "tsc", 9 | "lint": "eslint src" 10 | }, 11 | "peerDependencies": { 12 | "react": "~18.x", 13 | "react-dom": "~18.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/view/src/components/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/scss/base.scss'; 2 | 3 | #mb-root { 4 | position: relative; 5 | width: 100%; 6 | height: 100%; 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | #root-overlay { 13 | position: absolute; 14 | z-index: 9999; 15 | top: 0; 16 | width: 100%; 17 | height: 100%; 18 | 19 | #root-overlay-body { 20 | position: relative; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | } 25 | 26 | #root-main { 27 | position: relative; 28 | display: flex; 29 | flex-direction: row; 30 | width: 100%; 31 | height: 100%; 32 | 33 | #workspace { 34 | flex-shrink: 1; 35 | position: relative; 36 | width: 100%; 37 | height: 100%; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/view/src/components/toolbar/resources/pin.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/view/src/components/toolbar/resources/unpin.svg: -------------------------------------------------------------------------------- 1 | 10 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/view/src/index.ts: -------------------------------------------------------------------------------- 1 | // -- private functions ---------------------------------------------------------------------------- 2 | 3 | function _createToolbarItem( 4 | type: 'container' | 'button', 5 | position: 'cluster-a' | 'cluster-b', 6 | ): HTMLElement { 7 | const item = document.createElement(type === 'container' ? 'div' : 'button'); 8 | document.getElementById(`toolbar-${position}`)!.appendChild(item); 9 | 10 | item.classList.add('toolbar-cluster-item'); 11 | if (type === 'button') { 12 | item.classList.add('toolbar-cluster-item-btn'); 13 | } 14 | 15 | return item; 16 | } 17 | 18 | function _createWorkspaceItem(): HTMLElement { 19 | const wrapper = document.createElement('div'); 20 | document.getElementById('workspace')!.appendChild(wrapper); 21 | 22 | return wrapper; 23 | } 24 | 25 | // -- public functions ----------------------------------------------------------------------------- 26 | 27 | /** 28 | * Creates a new component in the UI framework. 29 | * @param params component parameters 30 | * @returns DOM element of the new component 31 | */ 32 | export function createItem( 33 | params: 34 | | { 35 | /** location to mount component */ 36 | location: 'workspace'; 37 | } 38 | | { 39 | /** location to mount component */ 40 | location: 'toolbar'; 41 | /** whether component is a toolbar container item or a toolbar button */ 42 | type: 'container' | 'button'; 43 | /** cluster (group of component items) */ 44 | position: 'cluster-a' | 'cluster-b'; 45 | }, 46 | ): HTMLElement { 47 | if (params.location === 'toolbar') { 48 | return _createToolbarItem(params.type, params.position); 49 | } /* if (params.location === 'workspace') */ else { 50 | return _createWorkspaceItem(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ], 16 | "@res/*": [ 17 | "../../res/*" 18 | ] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /modules/code-builder/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfigExport } from 'vite'; 2 | 3 | import path from 'path'; 4 | import { mergeConfig } from 'vite'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | function resolve(rootPath: string) { 9 | return path.resolve(__dirname, '..', rootPath); 10 | } 11 | 12 | export default { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(tsx|ts|jsx|js)'], 14 | addons: [ 15 | '@storybook/addon-links', 16 | '@storybook/addon-essentials', 17 | '@storybook/addon-interactions', 18 | ], 19 | framework: { 20 | name: '@storybook/react-vite', 21 | options: {}, 22 | }, 23 | docs: { 24 | autodocs: 'tag', 25 | }, 26 | async viteFinal(config: UserConfigExport) { 27 | return mergeConfig(config, { 28 | resolve: { 29 | alias: { 30 | '@': resolve('src'), 31 | '@res': resolve('../../res'), 32 | }, 33 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 34 | }, 35 | }); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /modules/code-builder/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /modules/code-builder/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '@res/scss/base.scss'; 2 | 3 | export const parameters = { 4 | actions: { 5 | argTypesRegex: '^on[A-Z].*', 6 | }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /modules/code-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-module-code-builder", 3 | "version": "4.2.0", 4 | "description": "Graphical project builder using drag & drop code bricks", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "test": "vitest", 9 | "coverage": "vitest run --coverage", 10 | "lint": "eslint src", 11 | "storybook": "storybook dev -p 6501 --no-open", 12 | "playground": "vite playground --host --port 5601" 13 | }, 14 | "peerDependencies": { 15 | "react": "~18.x", 16 | "react-dom": "~18.x" 17 | }, 18 | "dependencies": { 19 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0", 20 | "react-aria": "^3.26.0", 21 | "zustand": "^4.3.9" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /modules/code-builder/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground — Code Builder 8 | 9 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /modules/code-builder/playground/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; 3 | 4 | import PageCollision from './pages/Collision'; 5 | import WorkSpace from './pages/WorkSpace'; 6 | 7 | const router = createBrowserRouter([ 8 | { 9 | path: '/collision', 10 | element: , 11 | }, 12 | { 13 | path: '/workspace', 14 | element: , 15 | }, 16 | { 17 | path: '/', 18 | element: , 19 | }, 20 | ]); 21 | 22 | const root = createRoot(document.getElementById('playground-root') as HTMLElement); 23 | root.render(); 24 | -------------------------------------------------------------------------------- /modules/code-builder/playground/pages/Collision/index.scss: -------------------------------------------------------------------------------- 1 | #collision-space-wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: stretch; 5 | gap: 8px; 6 | 7 | box-sizing: border-box; 8 | width: 100vw; 9 | height: 100vh; 10 | padding: 8px 16px 16px 16px; 11 | background-color: grey; 12 | 13 | * { 14 | box-sizing: border-box; 15 | } 16 | 17 | #collision-space-controls { 18 | flex-grow: 0; 19 | flex-shrink: 0; 20 | 21 | display: flex; 22 | flex-direction: row; 23 | align-items: center; 24 | gap: 16px; 25 | 26 | margin: 0; 27 | padding: 0; 28 | list-style: none; 29 | 30 | .collision-space-control { 31 | display: flex; 32 | flex-direction: row; 33 | align-items: center; 34 | gap: 8px; 35 | 36 | > * { 37 | text-transform: uppercase; 38 | font-weight: bold; 39 | } 40 | 41 | label { 42 | color: white; 43 | } 44 | 45 | input[type='number'] { 46 | border: unset; 47 | border-radius: 4px; 48 | outline: none; 49 | text-align: end; 50 | } 51 | 52 | button { 53 | border: unset; 54 | border-radius: 4px; 55 | outline: none; 56 | background-color: white; 57 | cursor: pointer; 58 | 59 | &:hover, 60 | &:focus { 61 | background-color: lightblue; 62 | } 63 | } 64 | } 65 | } 66 | 67 | #collision-space { 68 | flex-grow: 0; 69 | flex-shrink: 1; 70 | 71 | position: relative; 72 | width: 100%; 73 | height: 100%; 74 | background-color: black; 75 | 76 | .collision-space-object { 77 | position: absolute; 78 | border: 2px solid lime; 79 | background-color: aliceblue; 80 | transition: background-color 0.25s ease, border 0.25s ease; 81 | 82 | &.collision-space-object-active { 83 | border: 2px solid gold; 84 | background-color: royalblue; 85 | } 86 | 87 | &.collision-space-object-target { 88 | border: 2px solid red; 89 | background: none; 90 | cursor: crosshair; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /modules/code-builder/playground/pages/WorkSpace/BricksCoordsStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | 3 | type CoordsState = { 4 | allCoords: { 5 | brickId: string; 6 | coords: { 7 | x: number; 8 | y: number; 9 | }; 10 | }[]; 11 | setCoords: (brickId: string, coords: { x: number; y: number }) => void; 12 | getCoords: (brickId: string) => { x: number; y: number } | undefined; 13 | }; 14 | 15 | const useBricksCoordsStore = create((set, get) => ({ 16 | allCoords: [ 17 | { brickId: '1', coords: { x: 50, y: 50 } }, 18 | { brickId: '2', coords: { x: 68, y: 92 } }, 19 | { brickId: '3', coords: { x: 68, y: 134 } }, 20 | { brickId: '4', coords: { x: 68, y: 176 } }, 21 | { brickId: '5', coords: { x: 86, y: 218 } }, 22 | { brickId: '6', coords: { x: 68, y: 302 } }, 23 | ], 24 | setCoords: (brickId: string, coords: { x: number; y: number }) => 25 | set( 26 | (state: { 27 | allCoords: { 28 | brickId: string; 29 | coords: { 30 | x: number; 31 | y: number; 32 | }; 33 | }[]; 34 | }) => ({ 35 | allCoords: state.allCoords.map((item) => 36 | item.brickId === brickId ? { brickId, coords } : item, 37 | ), 38 | }), 39 | ), 40 | getCoords: (brickId: string) => 41 | get().allCoords.find((item) => item.brickId === brickId)?.coords, 42 | })); 43 | 44 | export const useBricksCoords = () => { 45 | const allCoords = useBricksCoordsStore((state) => state.allCoords); 46 | const setCoords = useBricksCoordsStore((state) => state.setCoords); 47 | const getCoords = useBricksCoordsStore((state) => state.getCoords); 48 | 49 | return { allCoords, setCoords, getCoords }; 50 | }; 51 | -------------------------------------------------------------------------------- /modules/code-builder/playground/pages/WorkSpace/index.tsx: -------------------------------------------------------------------------------- 1 | import BrickFactory from './BrickFactory'; 2 | import { WORKSPACES_DATA } from './data'; 3 | import type { Brick } from './data'; 4 | 5 | function RenderBricks({ brickData }: { brickData: Brick }) { 6 | return ( 7 | <> 8 | 9 | {brickData.children && 10 | brickData.children?.length > 0 && 11 | brickData.children.map((child) => )} 12 | 13 | ); 14 | } 15 | 16 | function WorkSpace() { 17 | return ( 18 |
19 | {WORKSPACES_DATA.map((workspace) => ( 20 | 28 | {workspace.data.map((brick) => { 29 | return ; 30 | })} 31 | 32 | ))} 33 |
34 | ); 35 | } 36 | 37 | export default WorkSpace; 38 | -------------------------------------------------------------------------------- /modules/code-builder/playground/pages/WorkSpace/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Brick } from './data'; 2 | 3 | export function getBelowBricksIds(arr: Brick[], item: string): string[] { 4 | let result: string[] = []; 5 | 6 | function recursiveSearch(arr: Brick[], item: string) { 7 | arr.forEach((element, index) => { 8 | if (element.id === item) { 9 | arr.slice(index + 1, arr.length).map((el) => { 10 | result = result.concat(el.childBricks); 11 | result = result.concat(el.id); 12 | }); 13 | return; 14 | } 15 | if (Array.isArray(element.children)) { 16 | recursiveSearch(element.children, item); 17 | } 18 | }); 19 | } 20 | 21 | recursiveSearch(arr, item); 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /modules/code-builder/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | // 9 | react(), 10 | ], 11 | 12 | resolve: { 13 | alias: { 14 | '@': path.resolve(__dirname, '..', 'src'), 15 | }, 16 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /modules/code-builder/src/@types/collision.d.ts: -------------------------------------------------------------------------------- 1 | export type TCollisionObject = { 2 | /** unique ID of the object in the collision space */ 3 | id: string; 4 | /** x-coordinate of the centre of the object */ 5 | x: number; 6 | /** y-coordinate of the centre of the object */ 7 | y: number; 8 | /** width of the object */ 9 | width: number; 10 | /** height of the object */ 11 | height: number; 12 | }; 13 | 14 | export interface ICollisionSpace { 15 | /** 16 | * Sets collision space properties. 17 | */ 18 | setOptions(options: { 19 | /** whether objects are circles or rectangles */ 20 | objType: 'circle' | 'rect'; 21 | /** 22 | * a number between (0-1] that determines when collision is counted 23 | * @description 24 | * value greater than 0 means just starting to overlap, while 1 mean full overlap; 25 | * for circles, calculation is based on distance between the centres 26 | * for rectanges, calculation is based on overlapped area 27 | */ 28 | colThres: number; 29 | }): void; 30 | 31 | /** 32 | * Adds objects to the collision space. 33 | */ 34 | addObjects(objects: TCollisionObject[]): void; 35 | 36 | /** 37 | * Removes objects from the collision space. 38 | */ 39 | delObjects(objects: TCollisionObject[]): void; 40 | 41 | /** 42 | * Returns objects in the collision space that are colliding with the input object. 43 | */ 44 | checkCollision(object: TCollisionObject): string[]; 45 | 46 | /** 47 | * Clears the collision space of all objects. 48 | */ 49 | reset(): void; 50 | } 51 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/BrickData.ts: -------------------------------------------------------------------------------- 1 | import type { TBrickArgDataType, TBrickColor, TBrickCoords, TBrickExtent } from '@/@types/brick'; 2 | 3 | import { BrickModelData } from '../model'; 4 | import { generatePath } from './utils/path'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | /** 9 | * @class 10 | * Final class that defines a data brick. 11 | */ 12 | export default class BrickData extends BrickModelData { 13 | readonly _pathResults: ReturnType; 14 | 15 | constructor(params: { 16 | // intrinsic 17 | name: string; 18 | label: string; 19 | glyph: string; 20 | dataType: TBrickArgDataType; 21 | dynamic: boolean; 22 | value?: boolean | number | string; 23 | input?: 'boolean' | 'number' | 'string' | 'options'; 24 | // style 25 | colorBg: TBrickColor; 26 | colorFg: TBrickColor; 27 | outline: TBrickColor; 28 | scale: number; 29 | }) { 30 | super(params); 31 | this._pathResults = generatePath({ 32 | hasNest: false, 33 | hasNotchArg: true, 34 | hasNotchInsTop: false, 35 | hasNotchInsBot: false, 36 | scale: this._scale, 37 | innerLengthX: 100, 38 | argHeights: [], 39 | }); 40 | } 41 | 42 | public get SVGpath(): string { 43 | return this._pathResults.path; 44 | } 45 | 46 | public get bBoxBrick(): { extent: TBrickExtent; coords: TBrickCoords } { 47 | return this._pathResults.bBoxBrick; 48 | } 49 | 50 | public get bBoxNotchArg(): { extent: TBrickExtent; coords: TBrickCoords } { 51 | return this._pathResults.bBoxNotchArg!; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/BrickExpression.ts: -------------------------------------------------------------------------------- 1 | import type { TBrickArgDataType, TBrickColor, TBrickCoords, TBrickExtent } from '@/@types/brick'; 2 | 3 | import { BrickModelExpression } from '../model'; 4 | import { generatePath } from './utils/path'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | /** 9 | * @class 10 | * Final class that defines a expression brick. 11 | */ 12 | export default class BrickExpression extends BrickModelExpression { 13 | readonly _pathResults: ReturnType; 14 | 15 | constructor(params: { 16 | // intrinsic 17 | name: string; 18 | label: string; 19 | glyph: string; 20 | dataType: TBrickArgDataType; 21 | args: Record< 22 | string, 23 | { 24 | label: string; 25 | dataType: TBrickArgDataType; 26 | meta: unknown; 27 | } 28 | >; 29 | // style 30 | colorBg: TBrickColor; 31 | colorFg: TBrickColor; 32 | outline: TBrickColor; 33 | scale: number; 34 | }) { 35 | super(params); 36 | const argsKeys = Object.keys(this._args); 37 | this._pathResults = generatePath({ 38 | hasNest: false, 39 | hasNotchArg: true, 40 | hasNotchInsTop: false, 41 | hasNotchInsBot: false, 42 | scale: this._scale, 43 | innerLengthX: 100, 44 | argHeights: Array.from({ length: argsKeys.length }, () => 17), 45 | }); 46 | } 47 | 48 | public get SVGpath(): string { 49 | return this._pathResults.path; 50 | } 51 | 52 | public get bBoxBrick(): { extent: TBrickExtent; coords: TBrickCoords } { 53 | return this._pathResults.bBoxBrick; 54 | } 55 | 56 | public get bBoxArgs(): Record { 57 | const argsKeys = Object.keys(this._args); 58 | const result: Record = {}; 59 | 60 | argsKeys.forEach((key, index) => { 61 | result[key] = { extent: { width: 0, height: 0 }, coords: { x: 0, y: 0 } }; 62 | const argX = this._pathResults.bBoxArgs.coords[index].x; 63 | const argY = this._pathResults.bBoxArgs.coords[index].y; 64 | 65 | result[key].extent = this._pathResults.bBoxArgs.extent; 66 | result[key].coords = { x: argX, y: argY }; 67 | }); 68 | 69 | return result; 70 | } 71 | 72 | public get bBoxNotchArg(): { extent: TBrickExtent; coords: TBrickCoords } { 73 | return this._pathResults.bBoxNotchArg!; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/BrickStatement.ts: -------------------------------------------------------------------------------- 1 | import type { TBrickArgDataType, TBrickColor, TBrickCoords, TBrickExtent } from '@/@types/brick'; 2 | 3 | import { BrickModelStatement } from '../model'; 4 | import { generatePath } from './utils/path'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | /** 9 | * @class 10 | * Final class that defines a statement brick. 11 | */ 12 | export default class BrickStatement extends BrickModelStatement { 13 | readonly _pathResults: ReturnType; 14 | 15 | constructor(params: { 16 | // intrinsic 17 | name: string; 18 | label: string; 19 | glyph: string; 20 | args: Record< 21 | string, 22 | { 23 | label: string; 24 | dataType: TBrickArgDataType; 25 | meta: unknown; 26 | } 27 | >; 28 | // style 29 | colorBg: TBrickColor; 30 | colorFg: TBrickColor; 31 | outline: TBrickColor; 32 | scale: number; 33 | connectAbove: boolean; 34 | connectBelow: boolean; 35 | }) { 36 | super(params); 37 | const argsKeys = Object.keys(this._args); 38 | this._pathResults = generatePath({ 39 | hasNest: false, 40 | hasNotchArg: false, 41 | hasNotchInsTop: true, 42 | hasNotchInsBot: true, 43 | scale: this._scale, 44 | innerLengthX: 100, 45 | argHeights: Array.from({ length: argsKeys.length }, () => 17), 46 | }); 47 | } 48 | 49 | public get SVGpath(): string { 50 | return this._pathResults.path; 51 | } 52 | 53 | public get bBoxBrick(): { extent: TBrickExtent; coords: TBrickCoords } { 54 | return this._pathResults.bBoxBrick; 55 | } 56 | 57 | public get bBoxArgs(): Record { 58 | const argsKeys = Object.keys(this._args); 59 | const result: Record = {}; 60 | 61 | argsKeys.forEach((key, index) => { 62 | result[key] = { extent: { width: 0, height: 0 }, coords: { x: 0, y: 0 } }; 63 | const argX = this._pathResults.bBoxArgs.coords[index].x; 64 | const argY = this._pathResults.bBoxArgs.coords[index].y; 65 | 66 | result[key].extent = this._pathResults.bBoxArgs.extent; 67 | result[key].coords = { x: argX, y: argY }; 68 | }); 69 | 70 | return result; 71 | } 72 | 73 | public get bBoxNotchInsTop(): { extent: TBrickExtent; coords: TBrickCoords } { 74 | return this._pathResults.bBoxNotchInsTop!; 75 | } 76 | 77 | public get bBoxNotchInsBot(): { extent: TBrickExtent; coords: TBrickCoords } { 78 | return this._pathResults.bBoxNotchInsBot!; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/components/BrickBlock.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, JSX } from 'react'; 2 | import type { Brick } from 'playground/pages/WorkSpace/data'; 3 | 4 | // ------------------------------------------------------------------------------------------------- 5 | 6 | export default function ({ 7 | brickData, 8 | moveProps, 9 | coords, 10 | color, 11 | }: { 12 | brickData: Brick; 13 | moveProps: DOMAttributes; 14 | coords: { x: number; y: number }; 15 | color: string; 16 | }): JSX.Element { 17 | return ( 18 | 23 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/components/BrickData.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, JSX } from 'react'; 2 | import type { Brick } from 'playground/pages/WorkSpace/data'; 3 | 4 | export default function ({ 5 | brickData, 6 | moveProps, 7 | coords, 8 | color, 9 | }: { 10 | brickData: Brick; 11 | moveProps: DOMAttributes; 12 | coords: { x: number; y: number }; 13 | color: string; 14 | }): JSX.Element { 15 | return ( 16 | 21 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/components/BrickExpression.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, JSX } from 'react'; 2 | import type { Brick } from 'playground/pages/WorkSpace/data'; 3 | 4 | export default function ({ 5 | brickData, 6 | moveProps, 7 | coords, 8 | color, 9 | }: { 10 | brickData: Brick; 11 | moveProps: DOMAttributes; 12 | coords: { x: number; y: number }; 13 | color: string; 14 | }): JSX.Element { 15 | return ( 16 | 21 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/components/BrickStatement.tsx: -------------------------------------------------------------------------------- 1 | import type { DOMAttributes, JSX } from 'react'; 2 | import type { Brick } from 'playground/pages/WorkSpace/data'; 3 | 4 | export default function ({ 5 | brickData, 6 | moveProps, 7 | coords, 8 | color, 9 | }: { 10 | brickData: Brick; 11 | moveProps: DOMAttributes; 12 | coords: { x: number; y: number }; 13 | color: string; 14 | }): JSX.Element { 15 | return ( 16 | 21 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/stories/BrickBlock.stories.ts: -------------------------------------------------------------------------------- 1 | import { MetaData, Story } from '../../stories/brickBlock'; 2 | import MBrickBlock from '../BrickBlock'; 3 | import CBrickBlock from '../components/BrickBlock'; 4 | 5 | export default { 6 | title: 'Design 0/Block Brick', 7 | ...MetaData, 8 | }; 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | 12 | export const NoArgs: Story = { 13 | args: { 14 | Component: CBrickBlock, 15 | prototype: MBrickBlock, 16 | label: 'Block', 17 | args: [], 18 | colorBg: 'yellow', 19 | colorFg: 'black', 20 | outline: 'red', 21 | scale: 1, 22 | }, 23 | }; 24 | 25 | export const WithArgs: Story = { 26 | args: { 27 | Component: CBrickBlock, 28 | prototype: MBrickBlock, 29 | label: 'Block', 30 | args: ['Label 1'], 31 | colorBg: 'yellow', 32 | colorFg: 'black', 33 | outline: 'red', 34 | scale: 1, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/stories/BrickData.stories.ts: -------------------------------------------------------------------------------- 1 | import { MetaData, Story } from '../../stories/brickData'; 2 | import MBrickData from '../BrickData'; 3 | import CBrickData from '../components/BrickData'; 4 | 5 | export default { 6 | title: 'Design 0/Data Brick', 7 | ...MetaData, 8 | }; 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | 12 | export const Static: Story = { 13 | args: { 14 | Component: CBrickData, 15 | prototype: MBrickData, 16 | label: 'Data', 17 | colorBg: 'yellow', 18 | colorFg: 'black', 19 | outline: 'red', 20 | scale: 1, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/stories/BrickExpression.stories.ts: -------------------------------------------------------------------------------- 1 | import { MetaData, Story } from '../../stories/brickExpression'; 2 | import MBrickExpression from '../BrickExpression'; 3 | import CBrickExpression from '../components/BrickExpression'; 4 | 5 | export default { 6 | title: 'Design 0/Expression Brick', 7 | ...MetaData, 8 | }; 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | 12 | export const WithArgs: Story = { 13 | args: { 14 | Component: CBrickExpression, 15 | prototype: MBrickExpression, 16 | label: 'Expression', 17 | args: ['Label 1'], 18 | colorBg: 'yellow', 19 | colorFg: 'black', 20 | outline: 'red', 21 | scale: 1, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/design0/stories/BrickStatement.stories.ts: -------------------------------------------------------------------------------- 1 | import { MetaData, Story } from '../../stories/brickStatement'; 2 | import MBrickStatement from '../BrickStatement'; 3 | import CBrickStatement from '../components/BrickStatement'; 4 | 5 | export default { 6 | title: 'Design 0/Statement Brick', 7 | ...MetaData, 8 | }; 9 | 10 | // ------------------------------------------------------------------------------------------------- 11 | 12 | export const NoArgs: Story = { 13 | args: { 14 | Component: CBrickStatement, 15 | prototype: MBrickStatement, 16 | label: 'Statement', 17 | args: [], 18 | colorBg: 'yellow', 19 | colorFg: 'black', 20 | outline: 'red', 21 | scale: 1, 22 | }, 23 | }; 24 | 25 | export const WithArgs: Story = { 26 | args: { 27 | Component: CBrickStatement, 28 | prototype: MBrickStatement, 29 | label: 'Statement', 30 | args: ['Label 1'], 31 | colorBg: 'yellow', 32 | colorFg: 'black', 33 | outline: 'red', 34 | scale: 1, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ModelBrickData } from './design0/BrickData'; 2 | export { default as ModelBrickExpression } from './design0/BrickExpression'; 3 | export { default as ModelBrickStatement } from './design0/BrickStatement'; 4 | export { default as ModelBrickBlock } from './design0/BrickBlock'; 5 | 6 | export { default as BrickData } from './design0/components/BrickData'; 7 | export { default as BrickExpression } from './design0/components/BrickExpression'; 8 | export { default as BrickStatement } from './design0/components/BrickStatement'; 9 | export { default as BrickBlock } from './design0/components/BrickBlock'; 10 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/brickBlock.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import CBrickBlock from './components/BrickBlock'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export const MetaData: Meta = { 8 | component: CBrickBlock, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | }; 13 | 14 | export type Story = StoryObj; 15 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/brickData.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import CBrickData from './components/BrickData'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export const MetaData: Meta = { 8 | component: CBrickData, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | }; 13 | 14 | export type Story = StoryObj; 15 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/brickExpression.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import CBrickExpression from './components/BrickExpression'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export const MetaData: Meta = { 8 | component: CBrickExpression, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | }; 13 | 14 | export type Story = StoryObj; 15 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/brickStatement.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import CBrickStatement from './components/BrickStatement'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export const MetaData: Meta = { 8 | component: CBrickStatement, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | }; 13 | 14 | export type Story = StoryObj; 15 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/components/BrickData.tsx: -------------------------------------------------------------------------------- 1 | import BrickWrapper from './BrickWrapper'; 2 | import type { JSX } from 'react'; 3 | import type { IBrickData, TBrickArgDataType, TBrickColor } from '@/@types/brick'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export default function (props: { 8 | Component: (props: { instance: IBrickData; visualIndicators?: JSX.Element }) => JSX.Element; 9 | prototype: new (params: { 10 | name: string; 11 | label: string; 12 | glyph: string; 13 | dataType: TBrickArgDataType; 14 | dynamic: boolean; 15 | value?: boolean | number | string; 16 | input?: 'boolean' | 'number' | 'string' | 'options'; 17 | colorBg: TBrickColor; 18 | colorFg: TBrickColor; 19 | outline: TBrickColor; 20 | scale: number; 21 | }) => IBrickData; 22 | label: string; 23 | colorBg: string; 24 | colorFg: string; 25 | outline: string; 26 | scale: number; 27 | }): JSX.Element { 28 | const { Component, prototype, label, colorBg, colorFg, outline, scale } = props; 29 | 30 | const instance = new prototype({ 31 | label, 32 | colorBg, 33 | colorFg, 34 | outline, 35 | scale, 36 | glyph: '', 37 | dynamic: false, 38 | dataType: 'any', 39 | name: '', 40 | }); 41 | 42 | const VisualIndicators = () => ( 43 | <> 44 | {/* Overall Bounding Box of the Brick */} 45 | 53 | 54 | {/* Left notch bounding box */} 55 | 63 | 64 | ); 65 | 66 | return ( 67 | 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/components/BrickExpression.tsx: -------------------------------------------------------------------------------- 1 | import BrickWrapper from './BrickWrapper'; 2 | import type { JSX } from 'react'; 3 | import type { IBrickExpression, TBrickArgDataType, TBrickColor } from '@/@types/brick'; 4 | 5 | // ------------------------------------------------------------------------------------------------- 6 | 7 | export default function (props: { 8 | Component: (props: { instance: IBrickExpression; visualIndicators?: JSX.Element }) => JSX.Element; 9 | prototype: new (params: { 10 | name: string; 11 | label: string; 12 | glyph: string; 13 | dataType: TBrickArgDataType; 14 | args: Record< 15 | string, 16 | { 17 | label: string; 18 | dataType: TBrickArgDataType; 19 | meta: unknown; 20 | } 21 | >; 22 | colorBg: TBrickColor; 23 | colorFg: TBrickColor; 24 | outline: TBrickColor; 25 | scale: number; 26 | }) => IBrickExpression; 27 | label: string; 28 | args: string[]; 29 | colorBg: string; 30 | colorFg: string; 31 | outline: string; 32 | scale: number; 33 | }): JSX.Element { 34 | const { Component, prototype, label, args, colorBg, colorFg, outline, scale } = props; 35 | 36 | const instance = new prototype({ 37 | label, 38 | args: Object.fromEntries( 39 | args.map<[string, { label: string; dataType: TBrickArgDataType; meta: unknown }]>((name) => [ 40 | name, 41 | { label: name, dataType: 'any', meta: undefined }, 42 | ]), 43 | ), 44 | colorBg, 45 | colorFg, 46 | outline, 47 | scale, 48 | glyph: '', 49 | dataType: 'any', 50 | name: '', 51 | }); 52 | 53 | const VisualIndicators = () => ( 54 | <> 55 | {/* Overall Bounding Box of the Brick */} 56 | 64 | 65 | {/* Right args bounding box */} 66 | {Object.keys(instance.bBoxArgs).map((name, i) => { 67 | const arg = instance.bBoxArgs[name]; 68 | 69 | return ( 70 | 79 | ); 80 | })} 81 | 82 | {/* Left notch bounding box */} 83 | 91 | 92 | ); 93 | 94 | return ( 95 | 96 | 97 | 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /modules/code-builder/src/brick/stories/components/BrickWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react'; 2 | 3 | // ------------------------------------------------------------------------------------------------- 4 | 5 | export default function (props: PropsWithChildren): JSX.Element { 6 | return ( 7 | 8 | {props.children} 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /modules/code-builder/src/collision/Brute.ts: -------------------------------------------------------------------------------- 1 | import type { ICollisionSpace, TCollisionObject } from '@/@types/collision'; 2 | 3 | import { checkCollision } from './utils'; 4 | 5 | export default class implements ICollisionSpace { 6 | private _width; 7 | private _height; 8 | private _objType: 'circle' | 'rect' = 'circle'; 9 | private _colThres = 0; 10 | 11 | private _objects: TCollisionObject[] = []; 12 | 13 | constructor(width: number, height: number) { 14 | this._width = width; 15 | this._height = height; 16 | } 17 | 18 | public setOptions(options: { objType: 'circle' | 'rect'; colThres: number }): void { 19 | const { objType, colThres } = options; 20 | 21 | this._objType = objType; 22 | this._colThres = colThres; 23 | } 24 | 25 | public addObjects(objects: TCollisionObject[]): void { 26 | objects.forEach(({ id, x, y, width, height }) => { 27 | if ( 28 | x > width >> 1 && 29 | x < this._width - (width >> 1) && 30 | y > height >> 1 && 31 | y < this._height - (height >> 1) 32 | ) { 33 | this._objects.push({ id, x, y, width, height }); 34 | } 35 | }); 36 | } 37 | 38 | public delObjects(objects: TCollisionObject[]): void { 39 | const objectIds = objects.map(({ id }) => id); 40 | 41 | this._objects = this._objects.filter(({ id }) => !objectIds.includes(id)); 42 | } 43 | 44 | public checkCollision(object: TCollisionObject): string[] { 45 | return this._objects 46 | .filter((_object) => { 47 | const objA = { ...object }; 48 | const objB = { ..._object }; 49 | 50 | return checkCollision(objA, objB, { 51 | objType: this._objType, 52 | colThres: this._colThres, 53 | }); 54 | }) 55 | .map(({ id }) => id); 56 | } 57 | 58 | public reset(): void { 59 | this._objects = []; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /modules/code-builder/src/collision/QuadTree.ts: -------------------------------------------------------------------------------- 1 | import type { ICollisionSpace, TCollisionObject } from '@/@types/collision'; 2 | 3 | import Quadtree from 'quadtree-lib'; 4 | import { checkCollision } from './utils'; 5 | 6 | const QUADTREEMAXELEMS = 4; 7 | 8 | export default class implements ICollisionSpace { 9 | private _width; 10 | private _height; 11 | private _objType: 'circle' | 'rect' = 'circle'; 12 | private _colThres = 0; 13 | 14 | private _tree?: Quadtree; 15 | private _objMap: Map = new Map(); 16 | 17 | constructor(width: number, height: number) { 18 | this._width = width; 19 | this._height = height; 20 | 21 | this._tree = new Quadtree({ 22 | width, 23 | height, 24 | maxElements: QUADTREEMAXELEMS, 25 | }); 26 | } 27 | 28 | public setOptions(options: { objType: 'circle' | 'rect'; colThres: number }): void { 29 | const { objType, colThres } = options; 30 | 31 | this._objType = objType; 32 | this._colThres = colThres; 33 | } 34 | 35 | public addObjects(objects: TCollisionObject[]): void { 36 | objects.forEach(({ id, x, y, width, height }) => { 37 | if ( 38 | x > width >> 1 && 39 | x < this._width - (width >> 1) && 40 | y > height >> 1 && 41 | y < this._height - (height >> 1) 42 | ) { 43 | const object = { id, x, y, width, height }; 44 | 45 | this._objMap.set(id, object); 46 | this._tree!.push(object); 47 | } 48 | }); 49 | } 50 | 51 | public delObjects(objects: TCollisionObject[]): void { 52 | const objectIds = objects.map(({ id }) => id); 53 | 54 | objectIds.forEach((id) => { 55 | const object = this._objMap.get(id)!; 56 | this._tree!.remove(object); 57 | this._objMap.delete(id); 58 | }); 59 | } 60 | 61 | public checkCollision(object: TCollisionObject): string[] { 62 | return this._tree!.colliding(object, (objA, objB) => { 63 | return checkCollision(objA, objB, { 64 | objType: this._objType, 65 | colThres: this._colThres, 66 | }); 67 | }).map(({ id }) => id); 68 | } 69 | 70 | public reset(): void { 71 | this._tree!.clear(); 72 | this._objMap.clear(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /modules/code-builder/src/collision/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CollisionSpaceBrute } from './Brute'; 2 | export { default as CollisionSpaceQuadTree } from './QuadTree'; 3 | -------------------------------------------------------------------------------- /modules/code-builder/src/collision/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { TCollisionObject } from '@/@types/collision'; 2 | 3 | function _checkCollisionCircle( 4 | objA: TCollisionObject, 5 | objB: TCollisionObject, 6 | threshold: number, 7 | ): boolean { 8 | // width should be equal to height though 9 | const sizeObjA = Math.min(objA.width, objA.height); 10 | const sizeObjB = Math.min(objB.width, objB.height); 11 | 12 | const distance = Math.sqrt(Math.pow(objA.x - objB.x, 2) + Math.pow(objA.y - objB.y, 2)); 13 | 14 | return ( 15 | distance < ((sizeObjA >> 1) + (sizeObjB >> 1)) * (1 - Math.max(0, Math.min(1, threshold))) 16 | ); 17 | } 18 | 19 | function _checkCollisionRect( 20 | objA: TCollisionObject, 21 | objB: TCollisionObject, 22 | threshold: number, 23 | ): boolean { 24 | const [ax1, ax2, ax3, ax4] = [ 25 | objA.x - (objA.width >> 1), 26 | objA.x + (objA.width >> 1), 27 | objA.x - (objA.width >> 1), 28 | objA.x + (objA.width >> 1), 29 | ]; 30 | const [ay1, ay2, ay3, ay4] = [ 31 | objA.y - (objA.height >> 1), 32 | objA.y - (objA.height >> 1), 33 | objA.y + (objA.height >> 1), 34 | objA.y + (objA.height >> 1), 35 | ]; 36 | const [bx1, bx2, bx3, bx4] = [ 37 | objB.x - (objB.width >> 1), 38 | objB.x + (objB.width >> 1), 39 | objB.x - (objB.width >> 1), 40 | objB.x + (objB.width >> 1), 41 | ]; 42 | const [by1, by2, by3, by4] = [ 43 | objB.y - (objB.height >> 1), 44 | objB.y - (objB.height >> 1), 45 | objB.y + (objB.height >> 1), 46 | objB.y + (objB.height >> 1), 47 | ]; 48 | 49 | const [cx1, cx2, cx3, cx4] = [ 50 | Math.max(ax1, bx1), 51 | Math.min(ax2, bx2), 52 | Math.max(ax3, bx3), 53 | Math.min(ax4, bx4), 54 | ]; 55 | const [cy1, cy2, cy3, cy4] = [ 56 | Math.max(ay1, by1), 57 | Math.max(ay2, by2), 58 | Math.min(ay3, by3), 59 | Math.min(ay4, by4), 60 | ]; 61 | 62 | if (cx1 < cx2 && cx3 < cx4 && cy1 < cy3 && cy2 < cy4) { 63 | const areaA = (ax2 - ax1) * (ay3 - ay1); 64 | const areaB = (bx2 - bx1) * (by3 - by1); 65 | const areaC = (cx2 - cx1) * (cy3 - cy1); 66 | 67 | return areaC > Math.min(areaA, areaB) * Math.max(0, Math.min(1, threshold)); 68 | } 69 | 70 | return false; 71 | } 72 | 73 | /** 74 | * Checks whether two objects are colliding 75 | * @param objA object 1 76 | * @param objB object 2 77 | * @param options collision properties 78 | */ 79 | export function checkCollision( 80 | objA: TCollisionObject, 81 | objB: TCollisionObject, 82 | options: { 83 | objType: 'circle' | 'rect'; 84 | colThres: number; 85 | }, 86 | ): boolean { 87 | const { objType, colThres } = options; 88 | 89 | return (objType === 'circle' ? _checkCollisionCircle : _checkCollisionRect)( 90 | objA, 91 | objB, 92 | colThres, 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /modules/code-builder/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarlabs/musicblocks-v4/f7834e888abc78d4ad6317c1becc1b08e81f6284/modules/code-builder/src/index.ts -------------------------------------------------------------------------------- /modules/code-builder/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-module-editor", 3 | "version": "4.2.0", 4 | "description": "YAML based program editor", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "check": "tsc --types 'vite/client'", 9 | "lint": "eslint src" 10 | }, 11 | "peerDependencies": { 12 | "react": "~18.x", 13 | "react-dom": "~18.x" 14 | }, 15 | "dependencies": { 16 | "@sugarlabs/mb4-events": "*", 17 | "@sugarlabs/mb4-view": "*", 18 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0", 19 | "js-yaml": "^3.14.1" 20 | }, 21 | "devDependencies": { 22 | "@types/js-yaml": "^4.0.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /modules/editor/src/@types/index.ts: -------------------------------------------------------------------------------- 1 | /** Reresents a literal code argument. */ 2 | export type ICodeArgumentLiteral = boolean | number | string; 3 | 4 | /** Represents the interface for a code argument snapshot object. */ 5 | export interface ICodeArgumentObj { 6 | [key: string]: ICodeArgumentLiteral | ICodeArgumentObj; 7 | } 8 | 9 | /** Represents the interface for a code argument element. */ 10 | export type ICodeArgument = ICodeArgumentLiteral | ICodeArgumentObj; 11 | 12 | /** Represents the interface for a code instruction element object. */ 13 | export interface ICodeInstructionObj { 14 | [instruction: string]: ICodeArgument; 15 | } 16 | 17 | /** Represents the interface for a code instruction element. */ 18 | export type ICodeInstruction = string | ICodeInstructionObj; 19 | -------------------------------------------------------------------------------- /modules/editor/src/core/errors.ts: -------------------------------------------------------------------------------- 1 | abstract class SyntaxError extends Error { 2 | private _name: string; 3 | private _message: string; 4 | 5 | constructor(name: string, message: string) { 6 | super(message); 7 | this._name = name; 8 | this._message = message; 9 | } 10 | 11 | public toString(): string { 12 | return `${this._name}: ${this._message}`; 13 | } 14 | 15 | public get type(): string { 16 | return this._name; 17 | } 18 | } 19 | 20 | export class InvalidInstructionError extends SyntaxError { 21 | constructor(message: string) { 22 | super('InvalidInstructionError', message); 23 | } 24 | } 25 | 26 | export class InvalidArgumentError extends SyntaxError { 27 | constructor(message: string) { 28 | super('InvalidArgumentError', message); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/editor/src/view/components/button/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/themes/light'; 2 | 3 | #editor-toolbar-btn { 4 | padding: 0.25rem; 5 | 6 | svg { 7 | width: 100%; 8 | height: 100%; 9 | 10 | &.editor-toolbar-btn-img-hidden { 11 | display: none; 12 | } 13 | 14 | .path-fill { 15 | fill: $mb-accent; 16 | transition: all 0.25s ease; 17 | } 18 | } 19 | 20 | &:hover { 21 | svg { 22 | .path-fill { 23 | fill: $mb-accent-light; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /modules/editor/src/view/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import { injected } from '../../..'; 2 | 3 | // -- stylesheet ----------------------------------------------------------------------------------- 4 | 5 | import './index.scss'; 6 | 7 | // -- private variables ---------------------------------------------------------------------------- 8 | 9 | let _container: HTMLElement; 10 | let _svgCode: string; 11 | let _svgClose: string; 12 | 13 | // -- component definition ------------------------------------------------------------------------- 14 | 15 | /** 16 | * Loads the SVG icons for the editor's toolbar button. 17 | * @param container DOM element of the editor's toolbar button 18 | */ 19 | export function setup(container: HTMLElement): void { 20 | container.id = 'editor-toolbar-btn'; 21 | _container = container; 22 | 23 | _svgCode = injected.assets['image.icon.code'].data; 24 | _svgClose = injected.assets['image.icon.close'].data; 25 | 26 | setButtonImg('code'); 27 | } 28 | 29 | /** 30 | * Sets the SVG icon for the editor's toolbar button. 31 | * @param icon icon name 32 | */ 33 | export function setButtonImg(icon: 'code' | 'cross'): void { 34 | _container.innerHTML = icon === 'code' ? _svgCode : _svgClose; 35 | } 36 | -------------------------------------------------------------------------------- /modules/editor/src/view/index.ts: -------------------------------------------------------------------------------- 1 | import { createItem } from '@sugarlabs/mb4-view'; 2 | import { buildProgram } from '../core'; 3 | 4 | import { setStatus, setup as setupComponent } from './components'; 5 | import { setButtonImg, setup as setupButton } from './components/button'; 6 | 7 | // -- private variables ---------------------------------------------------------------------------- 8 | 9 | let _editor: HTMLDivElement; 10 | let _editorToolbarBtn: HTMLElement; 11 | 12 | // -- private functions ---------------------------------------------------------------------------- 13 | 14 | /** 15 | * Creates the DOM of the editor. 16 | */ 17 | function _createEditor(): Promise { 18 | return new Promise((resolve) => { 19 | _editor = document.createElement('div'); 20 | 21 | setupComponent(_editor).then(() => { 22 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 23 | // @ts-ignore 24 | _editor.addEventListener('buildprogram', function (e: CustomEvent) { 25 | buildProgram(e.detail).then((response) => 26 | setStatus(response ? 'Successfully Built' : 'Invalid Code'), 27 | ); 28 | }); 29 | 30 | resolve(); 31 | }); 32 | }); 33 | } 34 | 35 | /** 36 | * Creates the DOM of the editor's toolbar button. 37 | */ 38 | function _createToolbarButton(): Promise { 39 | return new Promise((resolve) => { 40 | _editorToolbarBtn = createItem({ 41 | location: 'toolbar', 42 | type: 'button', 43 | position: 'cluster-a', 44 | }); 45 | setupButton(_editorToolbarBtn); 46 | requestAnimationFrame(() => resolve()); 47 | }); 48 | } 49 | 50 | // -- public functions ----------------------------------------------------------------------------- 51 | 52 | /** 53 | * Sets up the DOM elements. 54 | */ 55 | export function setup(): Promise { 56 | return new Promise((resolve) => { 57 | (async () => { 58 | await _createEditor(); 59 | await _createToolbarButton(); 60 | resolve(); 61 | })(); 62 | }); 63 | } 64 | 65 | /** 66 | * Returns the individual DOM components. 67 | * @param element toolbar button or editor 68 | * @returns queried DOM component 69 | */ 70 | export function getElement(element: 'button' | 'editor'): HTMLElement { 71 | return element === 'button' ? _editorToolbarBtn : _editor; 72 | } 73 | 74 | /** 75 | * Sets the icon for the editor's toolbar button based on whether it is clicked or not. 76 | * @param state `clicked` or `unclicked` 77 | */ 78 | export function setButtonState(state: 'clicked' | 'unclicked'): void { 79 | setButtonImg(state === 'clicked' ? 'cross' : 'code'); 80 | } 81 | 82 | export { setCode, setHelp, setStatus, resetStates } from './components'; 83 | -------------------------------------------------------------------------------- /modules/editor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/menu/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfigExport } from 'vite'; 2 | 3 | import path from 'path'; 4 | import { mergeConfig } from 'vite'; 5 | 6 | // ------------------------------------------------------------------------------------------------- 7 | 8 | function resolve(rootPath: string) { 9 | return path.resolve(__dirname, '..', rootPath); 10 | } 11 | 12 | export default { 13 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(tsx|ts|jsx|js)'], 14 | addons: [ 15 | '@storybook/addon-links', 16 | '@storybook/addon-essentials', 17 | '@storybook/addon-interactions', 18 | ], 19 | framework: { 20 | name: '@storybook/react-vite', 21 | options: {}, 22 | }, 23 | docs: { 24 | autodocs: 'tag', 25 | }, 26 | async viteFinal(config: UserConfigExport) { 27 | return mergeConfig(config, { 28 | resolve: { 29 | alias: { 30 | '@': resolve('src'), 31 | '@res': resolve('../../res'), 32 | }, 33 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 34 | }, 35 | 36 | envDir: resolve('env'), 37 | 38 | build: { 39 | chunkSizeWarningLimit: 1024, 40 | }, 41 | }); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /modules/menu/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /modules/menu/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '@res/scss/base.scss'; 2 | 3 | export const parameters = { 4 | actions: { 5 | argTypesRegex: '^on[A-Z].*', 6 | }, 7 | controls: { 8 | matchers: { 9 | color: /(background|color)$/i, 10 | date: /Date$/, 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /modules/menu/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-module-menu", 3 | "version": "4.2.0", 4 | "description": "Menu controls", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "storybook": "storybook dev -p 6006", 9 | "check": "tsc --types 'vite/client'", 10 | "lint": "eslint src" 11 | }, 12 | "peerDependencies": { 13 | "react": "~18.x", 14 | "react-dom": "~18.x" 15 | }, 16 | "dependencies": { 17 | "@sugarlabs/mb4-components": "*", 18 | "@sugarlabs/mb4-events": "*", 19 | "@sugarlabs/mb4-view": "*", 20 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/menu/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { TInjectedMenu } from '#/@types/components/menu'; 2 | 3 | import { getCrumbs, run } from '@sugarlabs/musicblocks-v4-lib'; 4 | import { emitEvent } from '@sugarlabs/mb4-events'; 5 | 6 | import { mount as mountView, updateHandler, updateState } from './view'; 7 | 8 | // -- public functions ----------------------------------------------------------------------------- 9 | 10 | /** 11 | * Mounts the Menu component. 12 | */ 13 | export async function mount(): Promise { 14 | await mountView(); 15 | } 16 | 17 | /** 18 | * Initializes the Menu component. 19 | */ 20 | export async function setup(): Promise { 21 | await updateHandler('run', () => { 22 | const crumbs = getCrumbs(); 23 | if (crumbs.length !== 0) run(getCrumbs()[0].nodeID); 24 | updateState('running', true); 25 | setTimeout(() => updateState('running', false)); 26 | emitEvent('menu.run'); 27 | }); 28 | 29 | await updateHandler('stop', () => { 30 | updateState('running', false); 31 | emitEvent('menu.stop'); 32 | }); 33 | 34 | await updateHandler('reset', () => { 35 | updateState('running', false); 36 | emitEvent('menu.reset'); 37 | }); 38 | } 39 | 40 | // -- public variables ----------------------------------------------------------------------------- 41 | 42 | export const injected: TInjectedMenu = { 43 | // @ts-ignore 44 | flags: undefined, 45 | // @ts-ignore 46 | i18n: undefined, 47 | // @ts-ignore 48 | assets: undefined, 49 | }; 50 | -------------------------------------------------------------------------------- /modules/menu/src/view/components/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/themes/light'; 2 | 3 | #menu { 4 | padding: 0 !important; 5 | border: none !important; 6 | background-color: $mb-accent; 7 | 8 | .menu-btn { 9 | position: relative; 10 | display: grid; 11 | justify-content: center; 12 | align-items: center; 13 | width: 100%; 14 | margin: 0.5rem 0; 15 | padding: 0.25rem; 16 | border: none; 17 | border-radius: 0.25rem; 18 | background-color: $background-light; 19 | cursor: pointer; 20 | transition: all 0.25s ease; 21 | 22 | &.menu-btn-hidden { 23 | display: none; 24 | } 25 | 26 | .menu-btn-label { 27 | position: absolute; 28 | z-index: 100; 29 | left: calc(100% + 0.25rem); 30 | display: none; 31 | margin: 0; 32 | padding: 0 0.375rem 0.125rem 0.25rem; 33 | border-radius: 0 0.25rem 0.25rem 0; 34 | background-color: $mb-accent-light; 35 | 36 | &::before { 37 | position: absolute; 38 | top: 0; 39 | bottom: 0; 40 | left: 0; 41 | display: block; 42 | content: ''; 43 | width: 0.25rem; 44 | transform: translateX(-100%); 45 | clip-path: polygon(0 50%, 100% 0%, 100% 100%); 46 | background-color: $mb-accent-light; 47 | } 48 | 49 | span { 50 | font-size: 0.75rem; 51 | font-weight: bold; 52 | color: $text-light; 53 | } 54 | } 55 | 56 | .menu-btn-img { 57 | width: 100%; 58 | height: 100%; 59 | 60 | .path-fill { 61 | fill: $mb-accent; 62 | transition: all 0.25s ease; 63 | } 64 | } 65 | 66 | &:hover { 67 | .menu-btn-label { 68 | display: block; 69 | } 70 | 71 | .menu-btn-img { 72 | .path-fill { 73 | fill: $mb-accent-light; 74 | } 75 | } 76 | } 77 | 78 | .menu-btn-input { 79 | width: 0; 80 | height: 0; 81 | opacity: 0; 82 | z-index: -1; 83 | } 84 | } 85 | } 86 | 87 | #stories-menu-wrapper { 88 | height: max-content; 89 | padding: 0.5rem; 90 | background-color: $mb-accent-dark; 91 | display: flex; 92 | flex-direction: column; 93 | 94 | #menu { 95 | width: 2.5rem; 96 | margin: 0.25rem 0; 97 | padding: 0.25rem; 98 | border-radius: 0.25rem; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /modules/menu/src/view/index.ts: -------------------------------------------------------------------------------- 1 | import type { TPropsMenu } from '#/@types/components/menu'; 2 | 3 | import { createItem } from '@sugarlabs/mb4-view'; 4 | 5 | import { renderComponent } from './components'; 6 | import { injected } from '..'; 7 | 8 | // -- private variables ---------------------------------------------------------------------------- 9 | 10 | let _props: TPropsMenu; 11 | let _container: HTMLElement; 12 | 13 | // -- public functions ----------------------------------------------------------------------------- 14 | 15 | /** 16 | * Sets up the DOM. 17 | */ 18 | export async function mount(): Promise { 19 | _container = createItem({ 20 | location: 'toolbar', 21 | type: 'container', 22 | position: 'cluster-b', 23 | }); 24 | _container.id = 'menu'; 25 | 26 | _props = { 27 | injected, 28 | states: { running: false }, 29 | handlers: {}, 30 | }; 31 | 32 | await renderComponent(_container, { ..._props }); 33 | } 34 | 35 | export async function updateState(state: keyof TPropsMenu['states'], value: boolean) { 36 | _props.states[state] = value; 37 | await renderComponent(_container, { ..._props }); 38 | } 39 | 40 | export async function updateHandler( 41 | handler: keyof TPropsMenu['handlers'], 42 | callback: CallableFunction, 43 | ) { 44 | _props.handlers[handler] = callback; 45 | await renderComponent(_container, { ..._props }); 46 | } 47 | -------------------------------------------------------------------------------- /modules/menu/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/painter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-module-painter", 3 | "version": "4.2.0", 4 | "description": "Handles artwork generation", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "check": "tsc --types 'vite/client'", 9 | "lint": "eslint src" 10 | }, 11 | "peerDependencies": { 12 | "react": "~18.x", 13 | "react-dom": "~18.x" 14 | }, 15 | "dependencies": { 16 | "@sugarlabs/mb4-events": "*", 17 | "@sugarlabs/mb4-transport": "*", 18 | "@sugarlabs/mb4-view": "*", 19 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0", 20 | "p5": "^1.4.1" 21 | }, 22 | "devDependencies": { 23 | "@types/p5": "^1.4.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/painter/src/core/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts value in radians to degrees. 3 | * @param radians - value in radians 4 | * @returns value in degrees 5 | */ 6 | export function radToDeg(radians: number): number { 7 | return (radians / Math.PI) * 180; 8 | } 9 | 10 | /** 11 | * Converts value in degrees to radians. 12 | * @param degrees - value in degrees 13 | * @returns value in radians 14 | */ 15 | export function degToRad(degrees: number): number { 16 | return (degrees / 180) * Math.PI; 17 | } 18 | -------------------------------------------------------------------------------- /modules/painter/src/view/components/index.scss: -------------------------------------------------------------------------------- 1 | @import '@res/themes/light'; 2 | 3 | #painter { 4 | width: 100%; 5 | height: 100%; 6 | padding: 0.5rem; 7 | background-color: $mb-accent; 8 | 9 | #artboard-wrapper { 10 | position: relative; 11 | z-index: 1; 12 | box-sizing: border-box; 13 | width: 100%; 14 | height: 100%; 15 | border-radius: 0.25rem; 16 | overflow: hidden; 17 | 18 | #artboard-background { 19 | position: absolute; 20 | z-index: 1; 21 | top: 0; 22 | right: 0; 23 | bottom: 0; 24 | left: 0; 25 | background-color: $background-light; 26 | 27 | canvas { 28 | position: absolute; 29 | left: 50%; 30 | transform: translateX(-50%); 31 | } 32 | } 33 | 34 | #artboard-container { 35 | position: absolute; 36 | z-index: 2; 37 | top: 0; 38 | right: 0; 39 | bottom: 0; 40 | left: 0; 41 | 42 | .artboard { 43 | position: relative; 44 | z-index: 4; 45 | width: 100%; 46 | height: 100%; 47 | } 48 | 49 | .artboard-background { 50 | position: absolute; 51 | top: 0; 52 | right: 0; 53 | bottom: 0; 54 | left: 0; 55 | } 56 | } 57 | 58 | #artboard-interactor { 59 | position: absolute; 60 | z-index: 10; 61 | top: 0; 62 | right: 0; 63 | bottom: 0; 64 | left: 0; 65 | 66 | .artboard-sprite-wrapper { 67 | position: absolute; 68 | 69 | .artboard-sprite { 70 | opacity: 0.75; 71 | 72 | .path-fill.path-fill-default { 73 | fill: $mb-accent-light; 74 | } 75 | 76 | .path-stroke.path-stroke-default { 77 | stroke: $mb-accent-dark; 78 | } 79 | 80 | .path-fill-stroke.path-fill-stroke-default { 81 | fill: $mb-accent-dark; 82 | stroke: $mb-accent-dark; 83 | } 84 | } 85 | 86 | &::after { 87 | position: absolute; 88 | top: 50%; 89 | left: 50%; 90 | transform: translate(-50%, -50%); 91 | display: table; 92 | content: ' '; 93 | width: 0.25rem; 94 | height: 0.25rem; 95 | border: 1px solid $text-dark; 96 | border-radius: 50%; 97 | background-color: $background-light; 98 | opacity: 0.5; 99 | } 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /modules/painter/src/view/components/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import { setupCartesian } from './utils/background'; 5 | 6 | // -- stylesheet ----------------------------------------------------------------------------------- 7 | 8 | import './index.scss'; 9 | 10 | // -- private variables ---------------------------------------------------------------------------- 11 | 12 | let _artboard: HTMLDivElement; 13 | let _artboardBackground: HTMLDivElement; 14 | let _interactor: HTMLDivElement; 15 | 16 | let _mountedCallback: CallableFunction; 17 | 18 | // -- component definition ------------------------------------------------------------------------- 19 | 20 | /** 21 | * Painter component. 22 | * @returns root JSX element of the Painter component 23 | */ 24 | function Painter(): JSX.Element { 25 | const backgroundRef = useRef(null); 26 | const artboardRef = useRef(null); 27 | const artboardBackgroundRef = useRef(null); 28 | const interactorRef = useRef(null); 29 | 30 | useEffect(() => { 31 | _artboard = artboardRef.current!; 32 | _artboardBackground = artboardBackgroundRef.current!; 33 | _interactor = interactorRef.current!; 34 | 35 | _mountedCallback(); 36 | 37 | setupCartesian(backgroundRef.current!); 38 | }, []); 39 | 40 | return ( 41 | <> 42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | ); 52 | } 53 | 54 | /** 55 | * Mounts the React component inside the DOM container. 56 | * @param container DOM container 57 | * @returns a `Promise` of an object containing the DOM artboard and interactor elements 58 | */ 59 | export function setup(container: HTMLElement): Promise<{ 60 | artboard: HTMLDivElement; 61 | artboardBackground: HTMLDivElement; 62 | interactor: HTMLDivElement; 63 | }> { 64 | return new Promise((resolve) => { 65 | const rootContainer = createRoot(container); 66 | rootContainer.render(); 67 | 68 | _mountedCallback = () => 69 | requestAnimationFrame(() => { 70 | resolve({ 71 | artboard: _artboard, 72 | artboardBackground: _artboardBackground, 73 | interactor: _interactor, 74 | }); 75 | }); 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /modules/painter/src/view/components/utils/background.ts: -------------------------------------------------------------------------------- 1 | import p5 from 'p5'; 2 | 3 | /** 4 | * Sets up a canvas with a cartesian grid. 5 | * @param container DOM container of the grid 6 | */ 7 | export function setupCartesian(container: HTMLElement): void { 8 | const { width, height } = container.getBoundingClientRect(); 9 | 10 | new p5((p: p5) => { 11 | p.setup = () => { 12 | p.createCanvas(width, height); 13 | p.noLoop(); 14 | 15 | let delta = 10; 16 | let piecesVer = Math.floor((p.width >> 1) / delta); 17 | let piecesHor = Math.floor((p.height >> 1) / delta); 18 | 19 | p.stroke(252, 252, 252); 20 | p.strokeWeight(1); 21 | for (let i = 1; i <= piecesVer; i++) { 22 | p.line((p.width >> 1) + i * delta, 0, (p.width >> 1) + i * delta, p.height); 23 | p.line((p.width >> 1) - i * delta, 0, (p.width >> 1) - i * delta, p.height); 24 | } 25 | for (let i = 1; i <= piecesHor; i++) { 26 | p.line(0, (p.height >> 1) + i * delta, p.width, (p.height >> 1) + i * delta); 27 | p.line(0, (p.height >> 1) - i * delta, p.width, (p.height >> 1) - i * delta); 28 | } 29 | 30 | delta = 100; 31 | piecesVer = Math.floor((p.width >> 1) / delta); 32 | piecesHor = Math.floor((p.height >> 1) / delta); 33 | 34 | p.stroke(244, 244, 244); 35 | p.strokeWeight(1); 36 | for (let i = 1; i <= piecesVer; i++) { 37 | p.line((p.width >> 1) + i * delta, 0, (p.width >> 1) + i * delta, p.height); 38 | p.line((p.width >> 1) - i * delta, 0, (p.width >> 1) - i * delta, p.height); 39 | } 40 | for (let i = 1; i <= piecesHor; i++) { 41 | p.line(0, (p.height >> 1) + i * delta, p.width, (p.height >> 1) + i * delta); 42 | p.line(0, (p.height >> 1) - i * delta, p.width, (p.height >> 1) - i * delta); 43 | } 44 | 45 | p.stroke(240, 240, 240); 46 | p.strokeWeight(2); 47 | p.line(p.width >> 1, 0, p.width >> 1, p.height); 48 | p.line(0, p.height >> 1, p.width, p.height >> 1); 49 | }; 50 | }, container); 51 | } 52 | -------------------------------------------------------------------------------- /modules/painter/src/view/index.ts: -------------------------------------------------------------------------------- 1 | import { ISketch } from '../@types'; 2 | 3 | import { createItem } from '@sugarlabs/mb4-view'; 4 | import { setup as setupComponent } from './components'; 5 | import { setup as setupSprite } from './sprite'; 6 | 7 | // -- private variables ---------------------------------------------------------------------------- 8 | 9 | let _artboardSketch: HTMLElement; 10 | let _artboardBackground: HTMLElement; 11 | 12 | // -- public functions ----------------------------------------------------------------------------- 13 | 14 | /** 15 | * Initializes the Painter DOM. 16 | */ 17 | export function mount(): Promise { 18 | return new Promise((resolve) => { 19 | const container = createItem({ location: 'workspace' }); 20 | container.id = 'painter'; 21 | 22 | setupComponent(container).then(({ artboard, artboardBackground, interactor }) => { 23 | _artboardSketch = artboard; 24 | _artboardBackground = artboardBackground; 25 | setupSprite(interactor); 26 | 27 | resolve(); 28 | }); 29 | }); 30 | } 31 | 32 | /** 33 | * Mounts the sketch canvas inside the artboard. 34 | * @param sketch sketch instance 35 | */ 36 | export function mountSketch(sketch: ISketch): Promise { 37 | return new Promise((resolve) => { 38 | sketch.setup(_artboardSketch!); 39 | 40 | requestAnimationFrame(() => { 41 | const sketchCanvas = _artboardSketch.children[0] as HTMLCanvasElement; 42 | sketchCanvas.style.position = 'absolute'; 43 | sketchCanvas.style.left = '50%'; 44 | sketchCanvas.style.transform = 'translateX(-50%)'; 45 | 46 | resolve(); 47 | }); 48 | }); 49 | } 50 | 51 | /** 52 | * Removes the artboard container's background color. 53 | */ 54 | export function updateBackgroundColor(type: 'unset'): void; 55 | /** 56 | * Sets the artboard container's background color. 57 | * @param color background color 58 | */ 59 | export function updateBackgroundColor(type: 'set', color: [number, number, number]): void; 60 | /** 61 | * Updates the artboard container's background color. 62 | * @param type `set` to set background color, `unset` to remove background color 63 | * @param color background color 64 | */ 65 | export function updateBackgroundColor( 66 | type: 'set' | 'unset', 67 | color?: [number, number, number], 68 | ): void { 69 | if (type === 'unset') { 70 | _artboardBackground.style.backgroundColor = 'unset'; 71 | } else { 72 | const [r, g, b] = color!; 73 | _artboardBackground.style.backgroundColor = `rgb(${r}, ${g}, ${b})`; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /modules/painter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/singer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sugarlabs/mb4-module-singer", 3 | "version": "4.2.0", 4 | "description": "Handles music generation", 5 | "private": "true", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "playground": "vite playground --host --port 5601", 9 | "test": "vitest", 10 | "coverage": "vitest run --coverage", 11 | "check": "tsc", 12 | "lint": "eslint src" 13 | }, 14 | "dependencies": { 15 | "@sugarlabs/musicblocks-v4-lib": "^0.2.0", 16 | "tone": "^14.7.77" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/singer/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground — Singer 8 | 9 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /modules/singer/playground/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; 3 | 4 | import PageVoice from './pages/Voice'; 5 | 6 | const router = createBrowserRouter([ 7 | { 8 | path: 'voice', 9 | element: , 10 | }, 11 | { 12 | path: '/', 13 | element: , 14 | }, 15 | ]); 16 | 17 | const root = createRoot(document.getElementById('playground-root') as HTMLElement); 18 | root.render(); 19 | -------------------------------------------------------------------------------- /modules/singer/playground/pages/Voice.tsx: -------------------------------------------------------------------------------- 1 | import { Voice } from '@/core/voice'; 2 | import SynthUtils from '@/core/synthUtils'; 3 | import * as Tone from 'tone'; 4 | // import { _state, noteValueToSeconds, _defaultSynth, _polySynth } from '@/singer'; 5 | import { setupSynthUtils } from '@/core/synthUtils'; 6 | import { injected } from '@/index'; 7 | 8 | await (async () => { 9 | const { importAssets, getAsset } = await import('@sugarlabs/mb4-assets'); 10 | const assetManifest = (await import('@sugarlabs/mb4-assets')).default; 11 | await importAssets( 12 | Object.entries(assetManifest).map(([identifier, manifest]) => ({ identifier, manifest })), 13 | () => undefined, 14 | ); 15 | 16 | injected.assets = { 17 | 'audio.guitar': getAsset('audio.guitar')!, 18 | 'audio.piano': getAsset('audio.piano')!, 19 | 'audio.snare': getAsset('audio.snare')!, 20 | }; 21 | })(); 22 | 23 | function _getSynth(synthType: string) { 24 | synthType; 25 | 26 | // switch (synthType) { 27 | // case 'polysynth': 28 | // return _polySynth; 29 | // } 30 | // return _defaultSynth; 31 | } 32 | 33 | async function playSynth(synthType: string) { 34 | await Tone.start(); 35 | // const synth = _getSynth(synthType); 36 | // _state.notesPlayed = 0; 37 | console.log('playing c4 using', synthType); 38 | // const now = Tone.now(); 39 | // let offset = noteValueToSeconds(_state.notesPlayed); 40 | // synth.triggerAttackRelease('c4', '4n', now + offset); 41 | // _state.notesPlayed += 4; 42 | } 43 | 44 | async function voice() { 45 | const synth = new SynthUtils(); 46 | const myVoice = new Voice('myvoice', synth); 47 | 48 | await setupSynthUtils(); 49 | 50 | myVoice.playNote('g4', 1 / 4, 'piano'); 51 | 52 | // synth.trigger(['c4', 'g5'], 1/2, 'piano', 0); 53 | 54 | // const sampler = new Tone.Sampler({ 55 | // urls: { 56 | // A1: "A1.mp3", 57 | // A2: "A2.mp3", 58 | // }, 59 | // baseUrl: "https://tonejs.github.io/audio/casio/", 60 | // onload: () => { 61 | // sampler.triggerAttackRelease(["C4", "E4", "G4"], 4, 2); 62 | // } 63 | // }).toDestination(); 64 | 65 | // _state.notesPlayed = 0; 66 | // const now = Tone.now(); 67 | // let offset = noteValueToSeconds(_state.notesPlayed); 68 | // synth.trigger(['c4', 'd4'], 4, 'electronic synth', now + offset); 69 | // _state.notesPlayed += 4; 70 | } 71 | 72 | export default function (): JSX.Element { 73 | return ( 74 |
75 |

Voice Component

76 | 83 | 90 | 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /modules/singer/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | // 9 | react(), 10 | ], 11 | 12 | resolve: { 13 | alias: { 14 | '@': path.resolve(__dirname, '..', 'src'), 15 | }, 16 | extensions: ['.tsx', '.ts', '.js', '.scss', '.sass', '.json'], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /modules/singer/src/@types/currentPitch.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Walter Bender. All rights reserved. 3 | * Copyright (c) 2021, Kumar Saurabh Raj. All rights reserved. 4 | * Copyright (c) 2021, Anindya Kundu. All rights reserved. 5 | * 6 | * Licensed under the AGPL-3.0 License. 7 | */ 8 | 9 | /** Interface for the CurrentPitch class. */ 10 | export interface ICurrentPitch { 11 | /** Getter for the frequency of the current note in Hertz. */ 12 | readonly freq: number; 13 | /** Getter for the octave of the current note. */ 14 | readonly octave: number; 15 | /** Getter for the generic name of the current note. */ 16 | readonly genericName: string; 17 | /** Getter for the modal index of the current note. */ 18 | readonly semitoneIndex: number; 19 | /** 20 | * Getter for the index of the current note within the list of all of the notes defined by the 21 | * temperament. 22 | */ 23 | readonly number: number; 24 | 25 | /** Sets current pitch to a new pitch by frequency, index and octave or name. */ 26 | setPitch: (pitchName: number | string, octave: number) => void; 27 | /** Updates the current note by applying a semitone transposition. */ 28 | applySemitoneTransposition: (numberOfHalfSteps: number) => void; 29 | /** Updates the current note by applying a scalar transposition. */ 30 | applyScalarTransposition: (numberOfScalarSteps: number) => void; 31 | /** Calculates the frequency of the note numberOfHalfSteps away from the current note. */ 32 | getSemitoneInterval: (numberOfHalfSteps: number) => number; 33 | /** Calculates the frequency of the note numberOfScalarSteps away from the current note. */ 34 | getScalarInterval: (numberOfScalarSteps: number) => number; 35 | } 36 | -------------------------------------------------------------------------------- /modules/singer/src/@types/errors.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Anindya Kundu. All rights reserved. 3 | * 4 | * Licensed under the AGPL-3.0 License. 5 | */ 6 | 7 | /** 8 | * Interface for Error subclasses that excapsulate an error name. 9 | * 10 | * @remarks 11 | * Named error instances are to be used throughout the codebase. 12 | */ 13 | export interface INamedError extends Error { 14 | name: string; 15 | } 16 | 17 | /** 18 | * Generic type interface for the Error subclasses that bundle in a default value with the name. 19 | */ 20 | export interface IDefaultError extends INamedError { 21 | defaultValue: T; 22 | } 23 | -------------------------------------------------------------------------------- /modules/singer/src/@types/scale.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, Walter Bender. All rights reserved. 3 | * Copyright (c) 2021, Anindya Kundu. All rights reserved. 4 | * 5 | * Licensed under the AGPL-3.0 License. 6 | */ 7 | 8 | /** Interface for the Scale class. */ 9 | export interface IScale { 10 | /** Getter for the number of notes in the scale. */ 11 | readonly numberOfSemitones: number; 12 | /** Getter for the notes defined by the temperament. */ 13 | readonly noteNames: string[]; 14 | 15 | /** 16 | * Returns the notes in the scale. 17 | * @throws {InvalidArgumentError} 18 | */ 19 | getScale: (pitchFormat?: string[]) => string[]; 20 | /** 21 | * Returns the notes in the scale and the octave deltas. 22 | * @throws {InvalidArgumentError} 23 | */ 24 | getScaleAndOctaveDeltas: (pitchFormat?: string[]) => [string[], number[]]; 25 | } 26 | -------------------------------------------------------------------------------- /modules/singer/src/@types/synthUtils.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021-23 Walter Bender. All rights reserved. 3 | * Copyright (c) 2021-23 Anindya Kundu. All rights reserved. 4 | * 5 | * Licensed under the AGPL-3.0 License. 6 | */ 7 | 8 | /** Interface for the SynthUtils class. */ 9 | export interface ISynthUtils { 10 | /** 11 | * @remarks 12 | * Add a new sample synth. 13 | * 14 | * @param sample name 15 | * 16 | * @throws {InvalidArgumentError} 17 | */ 18 | addSampleSynth: (sampleName: string) => void; 19 | 20 | /** 21 | * @remarks 22 | * Returns a Tone.js synth for an instrument 23 | * 24 | * @param instrumentName is the name of a builtin synth (e.g., polySynth) 25 | * 26 | * @throws {InvalidArgumentError} 27 | */ 28 | getBuiltinSynth: (instrumentName: string) => Tone.PolySynth | undefined; 29 | 30 | /** 31 | * @remarks 32 | * Returns a Tone.js synth for an instrument 33 | * 34 | * @param instrumentName is the name of a sample. 35 | * 36 | * @throws {InvalidArgumentError} 37 | */ 38 | getSamplerSynth: (instrumentName: string) => Tone.Sampler | undefined; 39 | 40 | /** 41 | * @remarks 42 | * Returns a Tone.js synth for an instrument 43 | * 44 | * @param instrumentName is the name of a player. 45 | * 46 | * @throws {InvalidArgumentError} 47 | */ 48 | getPlayerSynth: (instrumentName: string) => Tone.Player | undefined; 49 | 50 | /** 51 | * @remarks 52 | * Set the volume for a specific synth. 53 | * 54 | * @param volume from 0 to 100 55 | **/ 56 | setVolume: (instrumentName: string, volume: number) => void; 57 | 58 | /** 59 | * @remarks 60 | * Set the master volume 61 | * 62 | * @param volume from 0 to 100 63 | **/ 64 | setMasterVolume: (volume: number) => void; 65 | 66 | /** 67 | * @remarks 68 | * trigger pitch(es) on a synth for a specified note value. 69 | * 70 | * @param pitches is an array of pitches, e.g., ["c4", "g5"] 71 | * @param noteValueInSeconds is a note duration in seconds 72 | * @param instrumentName is the name of an instrument synth (either a sample or builtin) 73 | * @param offset is the time in seconds since the synth was started. 74 | * 75 | * @throws {InvalidArgumentError} 76 | */ 77 | trigger: ( 78 | pitches: (string | number)[], 79 | noteValue: number, 80 | instrumentName: string, 81 | offset: number, 82 | ) => void; 83 | } 84 | -------------------------------------------------------------------------------- /modules/singer/src/@types/voice.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023, Walter Bender. All rights reserved. 3 | * Copyright (c) 2023, Anindya Kundu. All rights reserved. 4 | * 5 | * Licensed under the AGPL-3.0 License. 6 | */ 7 | 8 | /** A tuple containing a noteValue and an array of pitches */ 9 | export type NoteTuple = [number, (string | number)[]]; 10 | 11 | /** Interface for the Voice class. */ 12 | export interface IVoice { 13 | /** Getter for the numberOfNotesPlayedInSeconds. */ 14 | readonly numberOfNotesPlayedInSeconds: number; 15 | /** Getter for the list of notes played. */ 16 | readonly notesPlayed: NoteTuple[]; 17 | /** Notes played is the number of individual notes played */ 18 | numberOfNotesPlayed: () => number; 19 | /** Notes played is the number of notes played by note value */ 20 | getNumberOfNotesPlayedByNoteValue: (noteValue: number) => number; 21 | /** Getter and Setter for the beat. */ 22 | beat: number; 23 | /** Getter and Setter for the beats per minute. */ 24 | beatsPerMinute: number; 25 | /** Getter and Setter for the pickup */ 26 | pickup: number; 27 | /** set the meter, e.g., 4:4 */ 28 | setMeter: (beatsPerMeasure: number, noteValuePerBeat: number) => void; 29 | /** Getter and Setter for the strong beats */ 30 | strongBeats: number[]; 31 | /** set a strong beat */ 32 | setStrongBeat: (beat: number) => void; 33 | /** Getter and Setter for the weak beats */ 34 | weakBeats: number[]; 35 | /** set a weak beat */ 36 | setWeakBeat: (beat: number) => void; 37 | /** Getter and Setter for beats per measure */ 38 | beatsPerMeasure: number; 39 | /** Getter and Setter for note value per beat */ 40 | noteValuePerBeat: number; 41 | /** current beat */ 42 | getCurrentBeat: () => number; 43 | /** current measure */ 44 | getCurrentMeasure: () => number; 45 | /** Getter and Setter for the temporal offset */ 46 | temporalOffset: number; 47 | /** Enable/disable event dispatchers */ 48 | setEveryNoteEventDispatcher: (value: boolean) => void; 49 | setEveryBeatEventDispatcher: (value: boolean) => void; 50 | setEveryStrongBeatEventDispatcher: (value: boolean) => void; 51 | setEveryWeakBeatEventDispatcher: (value: boolean) => void; 52 | /** Send notes to be played to the synth. */ 53 | playNotes: ( 54 | pitches: (string | number)[], 55 | noteValue: number, 56 | instrumentName: string, 57 | future: number, 58 | tally: boolean, 59 | ) => void; 60 | /** Send one pitched note to the synth. */ 61 | playNote: (pitche: string | number, noteValue: number, instrumentName: string) => void; 62 | } 63 | -------------------------------------------------------------------------------- /modules/singer/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IElementSpecification } from '@sugarlabs/musicblocks-v4-lib'; 2 | import type { TInjectedSinger } from '#/@types/components/singer'; 3 | 4 | import { setup as setupComponent } from './singer'; 5 | import { 6 | ElementTestSynth, 7 | ElementResetNotesPlayed, 8 | ElementPlayNote, 9 | PlayGenericNoteName, 10 | PlayInterval, 11 | } from './singer'; 12 | 13 | // -- public functions ----------------------------------------------------------------------------- 14 | 15 | /** 16 | * Mounts the Singer component. 17 | */ 18 | export function mount(): Promise { 19 | return new Promise((resolve) => { 20 | resolve(); 21 | }); 22 | } 23 | 24 | /** 25 | * Initializes the Singer component. 26 | */ 27 | export function setup(): Promise { 28 | return new Promise((resolve) => { 29 | (async () => { 30 | await setupComponent(); 31 | 32 | resolve(); 33 | })(); 34 | }); 35 | } 36 | 37 | // -- public variables ----------------------------------------------------------------------------- 38 | 39 | export const injected: TInjectedSinger = { 40 | // @ts-ignore 41 | flags: undefined, 42 | // @ts-ignore 43 | i18n: undefined, 44 | // @ts-ignore 45 | assets: undefined, 46 | }; 47 | 48 | export const elements: Record = { 49 | 'test-synth': { 50 | label: 'test synth', 51 | type: 'Statement', 52 | category: 'Music', 53 | prototype: ElementTestSynth, 54 | }, 55 | 'play-note': { 56 | label: 'play note', 57 | type: 'Statement', 58 | category: 'Music', 59 | prototype: ElementPlayNote, 60 | }, 61 | 'reset-notes-played': { 62 | label: 'reset', 63 | type: 'Statement', 64 | category: 'Music', 65 | prototype: ElementResetNotesPlayed, 66 | }, 67 | 'play-generic': { 68 | label: 'play generic', 69 | type: 'Statement', 70 | category: 'Music', 71 | prototype: PlayGenericNoteName, 72 | }, 73 | 'play-interval': { 74 | label: 'play interval', 75 | type: 'Statement', 76 | category: 'Music', 77 | prototype: PlayInterval, 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /modules/singer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "./**/*.ts", 5 | "./**/*.tsx" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": [ 11 | "./src/*" 12 | ], 13 | "#/@types/*": [ 14 | "../../@types/*" 15 | ] 16 | } 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /res/scss/base.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100vw; 4 | height: 100vh; 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 7 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | -------------------------------------------------------------------------------- /res/scss/sizes.scss: -------------------------------------------------------------------------------- 1 | $s-border-radius: 4px; 2 | -------------------------------------------------------------------------------- /res/scss/wrappers.scss: -------------------------------------------------------------------------------- 1 | .l-image { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /res/themes/light.scss: -------------------------------------------------------------------------------- 1 | $mb-accent: #0652dd; 2 | $mb-accent-light: #1b9cfc; 3 | $mb-accent-dark: #1b1464; 4 | 5 | $background-light: white; 6 | 7 | $text-light: white; 8 | $text-dark: #192a56; 9 | 10 | $c-bg-white: #fff; 11 | $c-bg-grey: #c7c7c7; 12 | $c-bg-black: #111; 13 | 14 | $c-shadow: #00000040; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "downlevelIteration": true, 23 | "types": [ 24 | "vite/client", 25 | "vitest/globals" 26 | ] 27 | }, 28 | /** 29 | * `compilerOptions.module` should be UMD to execute modules with import statements 30 | */ 31 | "ts-node": { 32 | "compilerOptions": { 33 | "module": "UMD" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], 6 | 7 | watch: false, 8 | globals: true, 9 | reporters: ['verbose'], 10 | 11 | coverage: { 12 | provider: 'v8', 13 | reporter: ['html', 'text', 'text-summary', 'json', 'cobertura'], 14 | reportsDirectory: './coverage', 15 | }, 16 | }, 17 | }); 18 | --------------------------------------------------------------------------------