├── lit-plugin.log ├── packages ├── .gitkeep ├── async-queue │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── async-queue.spec.ts │ │ │ └── async-queue.ts │ ├── .babelrc │ ├── package.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── LICENSE │ └── project.json ├── dynamic-store │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── dynamic-store.spec.ts │ │ │ └── dynamic-store.ts │ ├── package.json │ ├── .babelrc │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── README.md │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json ├── event-emitter │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ └── event-emitter.spec.ts │ ├── .babelrc │ ├── package.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── LICENSE │ └── project.json ├── query │ ├── .babelrc │ ├── src │ │ ├── lib │ │ │ ├── test │ │ │ │ ├── types │ │ │ │ │ └── user.ts │ │ │ │ ├── index.ts │ │ │ │ └── resources │ │ │ │ │ ├── comment │ │ │ │ │ ├── index.ts │ │ │ │ │ └── transformations.ts │ │ │ │ │ └── user │ │ │ │ │ ├── index.ts │ │ │ │ │ └── transformations.ts │ │ │ ├── helpers │ │ │ │ └── serializer.ts │ │ │ └── query │ │ │ │ └── reducer.ts │ │ └── index.ts │ ├── package.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json ├── redux-registry │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── redux-registry.spec.ts │ │ │ └── redux-registry.ts │ ├── package.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ ├── project.json │ └── README.md ├── slice │ ├── src │ │ └── index.ts │ ├── package.json │ ├── .babelrc │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json ├── string-utils │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── string-utils.spec.ts │ │ │ └── string-utils.ts │ ├── package.json │ ├── .babelrc │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── README.md │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json ├── crux │ ├── src │ │ ├── lib │ │ │ ├── mock │ │ │ │ ├── one │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── selector.ts │ │ │ │ │ ├── subscription.ts │ │ │ │ │ ├── service.ts │ │ │ │ │ ├── view.ts │ │ │ │ │ ├── slice.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── two │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── selector.ts │ │ │ │ │ ├── subscription.ts │ │ │ │ │ ├── service.ts │ │ │ │ │ ├── view.ts │ │ │ │ │ ├── slice.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── layout │ │ │ │ │ ├── selector.ts │ │ │ │ │ ├── view.ts │ │ │ │ │ └── index.ts │ │ │ │ └── shared │ │ │ │ │ ├── index.ts │ │ │ │ │ └── service.ts │ │ │ ├── crux-with-emitter.ts │ │ │ ├── crux.test.ts │ │ │ ├── service.ts │ │ │ ├── subscription.ts │ │ │ ├── subscription.test.ts │ │ │ ├── view.ts │ │ │ └── types.ts │ │ └── index.ts │ ├── .babelrc │ ├── README.md │ ├── tsconfig.lib.json │ ├── package.json │ ├── tsconfig.spec.json │ ├── jest.config.ts │ ├── .eslintrc.json │ ├── tsconfig.json │ └── project.json ├── redux-types │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── helpers.ts │ │ │ └── types.ts │ ├── package.json │ ├── .babelrc │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── README.md │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json └── utils │ ├── package.json │ ├── src │ ├── lib │ │ ├── object.ts │ │ ├── array.ts │ │ ├── function.ts │ │ ├── classes.ts │ │ ├── promise │ │ │ ├── create-externally-resolvable-promise.spec.ts │ │ │ └── create-externally-resolvable-promise.ts │ │ ├── validate.ts │ │ ├── any.ts │ │ └── any.spec.ts │ └── index.ts │ ├── .babelrc │ ├── README.md │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── rollup.lib.js │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── tsconfig.json │ └── project.json ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── tools ├── generators │ ├── .gitkeep │ └── middleware │ │ ├── schema.ts │ │ ├── files │ │ └── index.ts__tmpl__ │ │ ├── schema.json │ │ └── index.ts ├── tsconfig.tools.json └── scripts │ └── packages │ ├── publish-single.ts │ └── version-packages.ts ├── apps └── dev │ ├── .gitignore │ ├── src │ ├── features │ │ ├── template │ │ │ ├── template.module.scss │ │ │ ├── template.service.ts │ │ │ ├── template.view.ts │ │ │ ├── template.slice.ts │ │ │ └── template.index.ts │ │ ├── dark-mode │ │ │ ├── dark-mode.config.ts │ │ │ ├── utils │ │ │ │ └── is-boolean.ts │ │ │ ├── dark-mode.scss │ │ │ ├── dark-mode.view.ts │ │ │ ├── dark-mode.index.ts │ │ │ ├── dark-mode.slice.ts │ │ │ └── dark-mode.service.ts │ │ ├── nav │ │ │ ├── nav.types.ts │ │ │ ├── nav.selectors.ts │ │ │ ├── nav.index.ts │ │ │ ├── nav.module.scss │ │ │ └── nav.view.ts │ │ ├── todos │ │ │ ├── data │ │ │ │ ├── todos.subscription.ts │ │ │ │ ├── transformations.ts │ │ │ │ └── todos.data.ts │ │ │ ├── components │ │ │ │ ├── container │ │ │ │ │ ├── container.css │ │ │ │ │ └── container.ts │ │ │ │ ├── column │ │ │ │ │ ├── column.css │ │ │ │ │ └── column.ts │ │ │ │ ├── expanding-dropzone │ │ │ │ │ ├── expanding-dropzone.css │ │ │ │ │ └── expanding-dropzone.ts │ │ │ │ ├── task │ │ │ │ │ ├── task.css │ │ │ │ │ └── task.ts │ │ │ │ └── task-overlay │ │ │ │ │ ├── task-overlay.css │ │ │ │ │ └── task-overlay.ts │ │ │ ├── domain │ │ │ │ ├── todos.types.ts │ │ │ │ ├── todos.reducers.test.ts │ │ │ │ ├── todos.reducers.ts │ │ │ │ ├── todos.http.ts │ │ │ │ ├── todos.selectors.ts │ │ │ │ └── todos.service.ts │ │ │ ├── slice │ │ │ │ └── todos.slice.ts │ │ │ └── views │ │ │ │ └── todos.module.css │ │ └── toaster │ │ │ ├── toaster.module.scss │ │ │ ├── toaster.index.ts │ │ │ ├── toaster.view.ts │ │ │ ├── toaster.service.ts │ │ │ └── toaster.slice.ts │ ├── vite-env.d.ts │ ├── design │ │ ├── template │ │ │ ├── template.css │ │ │ └── template.ts │ │ ├── divider │ │ │ ├── vertical-divider.module.scss │ │ │ └── vertical-divider.ts │ │ ├── icon │ │ │ └── icon.ts │ │ ├── alert │ │ │ ├── alert.module.scss │ │ │ └── alert.ts │ │ ├── toggle │ │ │ ├── toggle.module.scss │ │ │ └── toggle.ts │ │ ├── card │ │ │ └── card.css │ │ └── directives │ │ │ └── draggable │ │ │ └── index.ts │ ├── assets │ │ ├── logo-code-transparent.png │ │ └── logo-transparent-small.png │ ├── layout │ │ ├── types.ts │ │ ├── layout.selectors.ts │ │ ├── layout.slice.ts │ │ ├── layout.module.ts │ │ ├── layout.index.ts │ │ ├── layout.module.scss │ │ └── layout.view.ts │ ├── shared │ │ ├── env │ │ │ ├── env.index.ts │ │ │ └── env.service.ts │ │ ├── cache │ │ │ ├── cache.index.ts │ │ │ ├── types.ts │ │ │ └── cache.service.ts │ │ ├── data │ │ │ ├── data.service.ts │ │ │ ├── data.slice.ts │ │ │ ├── data.index.ts │ │ │ └── users │ │ │ │ ├── users-data.index.ts │ │ │ │ ├── transformations.ts │ │ │ │ └── users-data.service.ts │ │ ├── reporting │ │ │ ├── reporting.index.ts │ │ │ └── reporting.service.ts │ │ ├── async-cache │ │ │ ├── async-cache.index.ts │ │ │ └── async-cache.service.ts │ │ ├── http │ │ │ └── users │ │ │ │ ├── users-http.index.ts │ │ │ │ └── users-http.service.ts │ │ ├── feature-flags │ │ │ ├── feature-flags.index.ts │ │ │ └── feature-flags.service.ts │ │ ├── mock │ │ │ └── server.ts │ │ └── router │ │ │ ├── router.selectors.ts │ │ │ ├── router.index.ts │ │ │ ├── utils │ │ │ ├── get-url-from-route.ts │ │ │ └── get-route-from-url.ts │ │ │ ├── router.slice.ts │ │ │ └── router.service.ts │ ├── styles │ │ ├── main.scss │ │ └── normalise.css │ ├── types.d.ts │ ├── utils │ │ ├── relative-time.ts │ │ └── draggable │ │ │ └── index.ts │ ├── favicon.svg │ ├── main.ts │ └── logo.svg │ ├── package.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── jest.config.ts │ ├── tsconfig.app.json │ ├── index.html │ ├── project.json │ └── vite.config.ts ├── babel.config.json ├── .prettierignore ├── .prettierrc ├── jest.preset.js ├── jest.config.ts ├── .editorconfig ├── rollup.lib.prod.js ├── workspace.json ├── nx.json ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── tsconfig.base.json ├── migrations.json └── package.json /lit-plugin.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/dev/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | -------------------------------------------------------------------------------- /apps/dev/src/features/template/template.module.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/dev/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/async-queue/README.md: -------------------------------------------------------------------------------- 1 | # `@crux/async-queue` 2 | 3 | [TODO] 4 | -------------------------------------------------------------------------------- /packages/async-queue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/async-queue'; 2 | -------------------------------------------------------------------------------- /apps/dev/src/design/template/template.css: -------------------------------------------------------------------------------- 1 | .test { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /packages/dynamic-store/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/dynamic-store'; 2 | -------------------------------------------------------------------------------- /packages/event-emitter/README.md: -------------------------------------------------------------------------------- 1 | # `@crux/event-emitter` 2 | 3 | [TODO] 4 | -------------------------------------------------------------------------------- /packages/event-emitter/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/event-emitter'; 2 | -------------------------------------------------------------------------------- /packages/query/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /packages/redux-registry/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/redux-registry'; 2 | -------------------------------------------------------------------------------- /packages/slice/src/index.ts: -------------------------------------------------------------------------------- 1 | export { createSlice } from './lib/slice'; 2 | -------------------------------------------------------------------------------- /packages/string-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/string-utils'; 2 | -------------------------------------------------------------------------------- /packages/async-queue/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /packages/event-emitter/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@nrwl/web/babel"]] 3 | } 4 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/types.ts: -------------------------------------------------------------------------------- 1 | export interface State { 2 | count: number; 3 | } 4 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/types.ts: -------------------------------------------------------------------------------- 1 | export interface State { 2 | message: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/redux-types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/types'; 2 | export * from './lib/helpers'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.config.ts: -------------------------------------------------------------------------------- 1 | export const DARK_MODE_CACHE_KEY = 'crux-dev-dark-mode'; 2 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/types/user.ts: -------------------------------------------------------------------------------- 1 | export interface Err { 2 | code: number; 3 | message: string; 4 | } 5 | -------------------------------------------------------------------------------- /tools/generators/middleware/schema.ts: -------------------------------------------------------------------------------- 1 | export interface ComponentSchema { 2 | name: string; 3 | path?: string; 4 | } -------------------------------------------------------------------------------- /packages/query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/query", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs" 5 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /packages/dynamic-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/dynamic-store", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs" 5 | } -------------------------------------------------------------------------------- /apps/dev/src/assets/logo-code-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjessop/crux/HEAD/apps/dev/src/assets/logo-code-transparent.png -------------------------------------------------------------------------------- /apps/dev/src/assets/logo-transparent-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andyjessop/crux/HEAD/apps/dev/src/assets/logo-transparent-small.png -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/selector.ts: -------------------------------------------------------------------------------- 1 | import { State } from './types'; 2 | 3 | export const selectorOne = (state: State) => state.count; 4 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/selector.ts: -------------------------------------------------------------------------------- 1 | import { State } from './types'; 2 | 3 | export const selectorTwo = (state: State) => state.message; 4 | -------------------------------------------------------------------------------- /packages/slice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/slice", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/utils", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /apps/dev/src/design/divider/vertical-divider.module.scss: -------------------------------------------------------------------------------- 1 | .divider { 2 | width: 2px; 3 | height: 100%; 4 | background: var(--divider-color); 5 | } 6 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/layout/selector.ts: -------------------------------------------------------------------------------- 1 | export function selector(state: any) { 2 | return { main: 'main', sidebar: 'sidebar', top: 'top' }; 3 | } 4 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/utils/is-boolean.ts: -------------------------------------------------------------------------------- 1 | export function isBoolean(val: unknown): val is boolean { 2 | return val === true || val === false; 3 | } 4 | -------------------------------------------------------------------------------- /apps/dev/src/features/nav/nav.types.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | active: boolean; 3 | icon?: string; 4 | route: string; 5 | text: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/async-queue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/async-queue", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /packages/event-emitter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/event-emitter", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /packages/redux-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/redux-types", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /packages/string-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/string-utils", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": {} 6 | } -------------------------------------------------------------------------------- /packages/utils/src/lib/object.ts: -------------------------------------------------------------------------------- 1 | export function isPlainObject(obj: unknown) { 2 | return Object.prototype.toString.call(obj) === '[object Object]'; 3 | } 4 | -------------------------------------------------------------------------------- /apps/dev/src/layout/types.ts: -------------------------------------------------------------------------------- 1 | export interface LayoutState { 2 | roots: { 3 | sidebar?: boolean; 4 | signUpForm?: boolean; 5 | top?: boolean; 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/dev/src/shared/env/env.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const envService = service(() => import('./env.service').then((mod) => mod.env())); 4 | -------------------------------------------------------------------------------- /packages/crux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/web/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/dev/src/shared/cache/cache.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const cacheService = service(() => import('./cache.service').then((mod) => mod.cache())); 4 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/subscription.ts: -------------------------------------------------------------------------------- 1 | import { SliceOne } from './slice'; 2 | 3 | export function subscriptionOne(sliceOne: SliceOne, number: number) { 4 | return; 5 | } 6 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/subscription.ts: -------------------------------------------------------------------------------- 1 | import { SliceTwo } from './slice'; 2 | 3 | export function subscriptionTwo(sliceTwo: SliceTwo, message: string) { 4 | return; 5 | } 6 | -------------------------------------------------------------------------------- /packages/query/src/lib/helpers/serializer.ts: -------------------------------------------------------------------------------- 1 | export const serialize = (prop: unknown) => JSON.stringify(prop); 2 | 3 | export const deserialize = (str: string) => JSON.parse(str); 4 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/data.service.ts: -------------------------------------------------------------------------------- 1 | import { query } from '@crux/query'; 2 | import { service } from '@crux/crux'; 3 | 4 | export function data() { 5 | return query('data'); 6 | } 7 | -------------------------------------------------------------------------------- /packages/dynamic-store/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/web/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/query/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as mocks from './lib/test'; 2 | export * from './lib/query/query'; 3 | export type { API, State, Resource, Options, OperationType } from './lib/types'; 4 | -------------------------------------------------------------------------------- /packages/crux/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/crux'; 2 | export * from './lib/service'; 3 | export * from './lib/slice'; 4 | export * from './lib/subscription'; 5 | export * from './lib/view'; 6 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/shared/index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '../../service'; 2 | 3 | export const sharedService = service(() => import('./service').then((mod) => mod.sharedService())); 4 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/layout/view.ts: -------------------------------------------------------------------------------- 1 | import { LayoutData } from '.'; 2 | 3 | export function layout(root: HTMLElement) { 4 | return function (data: LayoutData) { 5 | /* */ 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/index.ts: -------------------------------------------------------------------------------- 1 | export { createUserConfig } from './resources/user'; 2 | export { createCommentConfig } from './resources/comment'; 3 | export { createDataAPI } from './api'; 4 | -------------------------------------------------------------------------------- /packages/redux-registry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/redux-registry", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "devDependencies": { 6 | "redux": "^4.1.2" 7 | } 8 | } -------------------------------------------------------------------------------- /packages/slice/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ], 10 | "plugins": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ], 10 | "plugins": [] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "tsc && vite build", 7 | "serve": "vite preview" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/redux-types/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ], 10 | "plugins": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/string-utils/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nrwl/react/babel", 5 | { 6 | "runtime": "automatic" 7 | } 8 | ] 9 | ], 10 | "plugins": [] 11 | } 12 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/data/todos.subscription.ts: -------------------------------------------------------------------------------- 1 | import type { TodosData } from './todos.data'; 2 | 3 | export function initiateFetch(subscription: TodosData) { 4 | return subscription.refetch(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/dev/src/features/toaster/toaster.module.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | display: flex; 3 | align-items: center; 4 | margin: 0 var(--margin) var(--margin) 0; 5 | transform: translateZ(0); 6 | will-change: all; 7 | } 8 | -------------------------------------------------------------------------------- /apps/dev/src/shared/reporting/reporting.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const reportingService = service(() => 4 | import('./reporting.service').then((mod) => mod.reporting()) 5 | ); 6 | -------------------------------------------------------------------------------- /apps/dev/src/shared/async-cache/async-cache.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const asyncCacheService = service(() => 4 | import('./async-cache.service').then((mod) => mod.asyncCache()) 5 | ); 6 | -------------------------------------------------------------------------------- /apps/dev/src/shared/http/users/users-http.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const usersHttpApi = service(() => 4 | import('./users-http.service').then((mod) => mod.createUsersHttp()) 5 | ); 6 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/service.ts: -------------------------------------------------------------------------------- 1 | export type ServiceOne = ReturnType; 2 | 3 | export function serviceOne() { 4 | return { 5 | destroy: () => { 6 | /* */ 7 | }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/service.ts: -------------------------------------------------------------------------------- 1 | export type ServiceTwo = ReturnType; 2 | 3 | export function serviceTwo() { 4 | return { 5 | destroy: () => { 6 | /* */ 7 | }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /apps/dev/src/shared/feature-flags/feature-flags.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | 3 | export const featureFlagsService = service(() => 4 | import('./feature-flags.service').then((mod) => mod.featureFlags()) 5 | ); 6 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/view.ts: -------------------------------------------------------------------------------- 1 | import { SliceTwo } from './slice'; 2 | 3 | export function viewTwo(root: HTMLElement) { 4 | return function (data: string, actions: SliceTwo) { 5 | console.log(data, actions); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/dev/src/design/divider/vertical-divider.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import styles from './vertical-divider.module.scss'; 3 | 4 | export function verticalDivider() { 5 | return html`
`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/view.ts: -------------------------------------------------------------------------------- 1 | import { ServiceOne } from './service'; 2 | 3 | export function viewOne(root: HTMLElement) { 4 | return function (data: number, actions: ServiceOne) { 5 | console.log(data, actions); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/shared/service.ts: -------------------------------------------------------------------------------- 1 | export type SharedService = ReturnType; 2 | 3 | export function sharedService() { 4 | return { 5 | destroy: () => { 6 | /* */ 7 | }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/string-utils/src/lib/string-utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { stringUtils } from './string-utils'; 2 | 3 | describe('stringUtils', () => { 4 | it('should work', () => { 5 | expect(stringUtils()).toEqual('string-utils'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/async-queue/src/lib/async-queue.spec.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncQueue } from './async-queue'; 2 | 3 | describe('asyncQueue', () => { 4 | it('should work', () => { 5 | expect(createAsyncQueue()).toEqual('async-queue'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dynamic-store/src/lib/dynamic-store.spec.ts: -------------------------------------------------------------------------------- 1 | import { dynamicStore } from './dynamic-store'; 2 | 3 | describe('dynamicStore', () => { 4 | it('should work', () => { 5 | expect(dynamicStore()).toEqual('dynamic-store'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/dev/src/features/template/template.service.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateStateAPI } from './template.slice'; 2 | 3 | export type TemplateAPI = ReturnType; 4 | 5 | export function template(api: TemplateStateAPI) { 6 | return {}; 7 | } 8 | -------------------------------------------------------------------------------- /apps/dev/src/shared/feature-flags/feature-flags.service.ts: -------------------------------------------------------------------------------- 1 | export function featureFlags() { 2 | return { 3 | has, 4 | }; 5 | 6 | function has(feature: string) { 7 | return ((window as any).features[feature] = true); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/event-emitter/src/lib/event-emitter.spec.ts: -------------------------------------------------------------------------------- 1 | import { createEventEmitter } from './event-emitter'; 2 | 3 | describe('eventEmitter', () => { 4 | it('should work', () => { 5 | expect(createEventEmitter).not.toBeUndefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/data/transformations.ts: -------------------------------------------------------------------------------- 1 | import type { Task } from '../domain/todos.types'; 2 | import type { Response } from '../domain/todos.http'; 3 | 4 | export function toData(response: Response) { 5 | return response.data; 6 | } 7 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.scss: -------------------------------------------------------------------------------- 1 | body { 2 | &.dark { 3 | --color-bg-1: rgba(0, 0, 0, 0.85); 4 | --color-bg-2: rgba(0, 0, 0, 0.83); 5 | --color-bg-3: rgba(0, 0, 0, 0.81); 6 | 7 | --color-font: rgb(255, 255, 255, 0.8); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/dev/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/redux-registry/src/lib/redux-registry.spec.ts: -------------------------------------------------------------------------------- 1 | import { reducerRegistry, middlewareRegistry } from './redux-registry'; 2 | 3 | describe('reduxRegistry', () => { 4 | it('should work', () => { 5 | expect(reducerRegistry()).toEqual('redux-registry'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/dev/src/shared/reporting/reporting.service.ts: -------------------------------------------------------------------------------- 1 | export function reporting() { 2 | return { 3 | track, 4 | }; 5 | 6 | function track(data: unknown) { 7 | console.group('Reporting Service:'); 8 | console.log(data); 9 | console.groupEnd(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/dev/src/shared/async-cache/async-cache.service.ts: -------------------------------------------------------------------------------- 1 | import { del, get, set } from 'idb-keyval'; 2 | import type { AsyncCache } from '../cache/types'; 3 | 4 | export function asyncCache(): AsyncCache { 5 | return { 6 | get, 7 | remove: del, 8 | set, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/crux/README.md: -------------------------------------------------------------------------------- 1 | # crux 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build crux` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test crux` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /packages/query/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/slice/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # utils 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build utils` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test utils` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /packages/async-queue/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/event-emitter/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/redux-registry/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/redux-types/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/any'; 2 | export * from './lib/array'; 3 | export * from './lib/classes'; 4 | export * from './lib/function'; 5 | export * from './lib/object'; 6 | export * from './lib/promise/create-externally-resolvable-promise'; 7 | export * from './lib/validate'; 8 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["jest"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/crux/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/query/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/slice/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/string-utils/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["jest"] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["**/*.spec.ts", "jest.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/async-queue/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/redux-types/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/dynamic-store/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [] 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/dynamic-store/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/event-emitter/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/redux-registry/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/string-utils/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/data.slice.ts: -------------------------------------------------------------------------------- 1 | import type { API as queryAPI } from '@crux/query'; 2 | 3 | export function dataSlice(data: queryAPI) { 4 | const { createResource, reducer, middleware } = data; 5 | 6 | return { 7 | api: { createResource }, 8 | middleware, 9 | reducer, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /apps/dev/src/shared/mock/server.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | import { createTodosMockApi } from '../../features/todos/data/todos.mock.api'; 3 | 4 | export async function createServer(apiUrl: string) { 5 | const handlers = [...createTodosMockApi(apiUrl)]; 6 | 7 | return setupWorker(...handlers); 8 | } 9 | -------------------------------------------------------------------------------- /packages/redux-types/README.md: -------------------------------------------------------------------------------- 1 | # redux-types 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build redux-types` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test redux-types` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /apps/dev/src/shared/router/router.selectors.ts: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import type { RouterState } from './router.slice'; 3 | 4 | const selectRouter = (state: { router: RouterState }) => state.router; 5 | 6 | export const selectRoute = createSelector(selectRouter, (router) => router?.route ?? null); 7 | -------------------------------------------------------------------------------- /packages/crux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@crux/crux", 3 | "version": "0.0.47-alpha", 4 | "type": "commonjs", 5 | "dependencies": { 6 | "@crux/dynamic-store": "*", 7 | "@crux/event-emitter": "*", 8 | "@crux/redux-types": "*", 9 | "@crux/slice": "*", 10 | "@crux/string-utils": "*" 11 | } 12 | } -------------------------------------------------------------------------------- /packages/crux/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "strict": false, 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "src/lib/crux.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/dynamic-store/README.md: -------------------------------------------------------------------------------- 1 | # dynamic-store 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build dynamic-store` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test dynamic-store` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /packages/string-utils/README.md: -------------------------------------------------------------------------------- 1 | # string-utils 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Building 6 | 7 | Run `nx build string-utils` to build the library. 8 | 9 | ## Running unit tests 10 | 11 | Run `nx test string-utils` to execute the unit tests via [Jest](https://jestjs.io). 12 | -------------------------------------------------------------------------------- /apps/dev/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: radial-gradient(circle at top left, var(--body-bg), var(--body-bg-highlight)); 3 | color: var(--body-color); 4 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | } 7 | -------------------------------------------------------------------------------- /apps/dev/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | interface IClassNames { 3 | [className: string]: string; 4 | } 5 | const classNames: IClassNames; 6 | export = classNames; 7 | } 8 | 9 | declare module '*' assert {type: 'css'} { 10 | const stylesheet: CSSStyleSheet; 11 | export default stylesheet; 12 | } -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "esnext", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/utils/src/lib/array.ts: -------------------------------------------------------------------------------- 1 | export function findLastIndex( 2 | array: Array, 3 | predicate: (value: T, index: number, obj: T[]) => boolean 4 | ): number { 5 | let l = array.length; 6 | 7 | while (l--) { 8 | if (predicate(array[l], l, array)) { 9 | return l; 10 | } 11 | } 12 | return -1; 13 | } 14 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.selectors.ts: -------------------------------------------------------------------------------- 1 | import type { RouterState } from '../shared/router/router.slice'; 2 | import type { LayoutState } from './layout.slice'; 3 | 4 | export function layout(layoutState: LayoutState, router: RouterState) { 5 | return { 6 | ...layoutState, 7 | todos: router?.route?.name === 'todos', 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/container/container.css: -------------------------------------------------------------------------------- 1 | .todos { 2 | display: grid; 3 | grid-template-columns: 1fr 1fr 1fr; 4 | grid-template-rows: 1fr; 5 | grid-template-areas: 'to-do in-progress completed'; 6 | max-width: 1000px; 7 | padding: 1rem 0; 8 | margin: 0 auto; 9 | height: calc(100vh - var(--top-height)); 10 | } 11 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/data.index.ts: -------------------------------------------------------------------------------- 1 | import { service, slice } from '@crux/crux'; 2 | import { data } from './data.service'; 3 | 4 | export const dataService = service(data); 5 | 6 | export const dataSlice = slice( 7 | (query) => import('./data.slice').then((mod) => mod.dataSlice(query)), 8 | { 9 | deps: [dataService], 10 | name: 'data', 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /packages/slice/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...(config.plugins || []), 8 | copy({ 9 | targets: [{ src: `packages/slice/README.md`, dest: `dist/packages/slice` }], 10 | }), 11 | ], 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/layout/index.ts: -------------------------------------------------------------------------------- 1 | import { view } from '../../view'; 2 | import { selector } from './selector'; 3 | 4 | export type LayoutData = ReturnType; 5 | 6 | export const layoutSelector = selector; 7 | 8 | export const layoutView = view(() => import('./view').then((mod) => mod.layout), { 9 | data: layoutSelector, 10 | root: 'root', 11 | }); 12 | -------------------------------------------------------------------------------- /apps/dev/src/shared/cache/types.ts: -------------------------------------------------------------------------------- 1 | export interface Cache { 2 | clear(): void; 3 | get(key: string): unknown | null; 4 | remove(key: string): void; 5 | set(key: string, value: unknown): void; 6 | } 7 | 8 | export interface AsyncCache { 9 | get(key: string): Promise; 10 | remove(key: string): Promise; 11 | set(key: string, value: unknown): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /packages/query/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/query/README.md`, dest: `dist/packages/query` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/utils/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/utils/README.md`, dest: `dist/packages/utils` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/redux-types/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Action } from './types'; 2 | 3 | export function isAction(value: unknown): value is Action { 4 | const anyValue = value as any; 5 | 6 | return ( 7 | anyValue.type && 8 | (anyValue.error === true || 9 | anyValue.error === false || 10 | anyValue.payload !== undefined || 11 | anyValue.meta !== undefined) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/async-queue/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/async-queue/README.md`, dest: `dist/packages/async-queue` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/redux-types/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/redux-types/README.md`, dest: `dist/packages/redux-types` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/string-utils/src/lib/string-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a random(ish) 10 character string 3 | */ 4 | export function generateRandomId(length = 10): string { 5 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 6 | 7 | return new Array(length) 8 | .fill(0) 9 | .reduce((acc) => acc + possible.charAt(Math.floor(Math.random() * possible.length)), ''); 10 | } 11 | -------------------------------------------------------------------------------- /packages/query/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/slice/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/string-utils/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/string-utils/README.md`, dest: `dist/packages/string-utils` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/utils/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/dev/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'dev', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/apps/dev', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/dev/src/design/template/template.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, unsafeCSS } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | import styles from './template.css?inline'; 4 | 5 | @customElement('my-element') 6 | export class MyElement extends LitElement { 7 | static styles = [unsafeCSS(styles)]; 8 | 9 | render() { 10 | return html`
My Element
`; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/column/column.css: -------------------------------------------------------------------------------- 1 | .in-progress { 2 | grid-area: in-progress; 3 | } 4 | 5 | .completed { 6 | grid-area: completed; 7 | } 8 | 9 | .to-do { 10 | grid-area: to-do; 11 | } 12 | 13 | .column { 14 | padding: 1rem; 15 | height: 100%; 16 | overflow-y: scroll; 17 | user-select: none; 18 | } 19 | 20 | .column:hover { 21 | background-color: rgba(0, 0, 0, 0.1); 22 | } 23 | -------------------------------------------------------------------------------- /packages/async-queue/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/event-emitter/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/event-emitter/README.md`, dest: `dist/packages/event-emitter` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/redux-registry/rollup.lib.js: -------------------------------------------------------------------------------- 1 | const copy = require('rollup-plugin-copy'); 2 | 3 | module.exports = function createLibConfig(config) { 4 | return { 5 | ...config, 6 | plugins: [ 7 | ...config.plugins || [], 8 | copy({ 9 | targets: [ 10 | { src: `packages/redux-registry/README.md`, dest: `dist/packages/redux-registry` } 11 | ] 12 | }) 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /packages/redux-types/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/string-utils/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/dev/src/features/template/template.view.ts: -------------------------------------------------------------------------------- 1 | import { render } from 'lit'; 2 | import type { TemplateState } from './template.slice'; 3 | import type { TemplateAPI } from './template.service'; 4 | 5 | export function templateView(root: HTMLElement) { 6 | return function toast(data: TemplateState, actions: TemplateAPI): void { 7 | render(template(), root); 8 | 9 | function template() {} 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/dynamic-store/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/event-emitter/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/redux-registry/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/crux/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'crux', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/crux', 15 | }; 16 | -------------------------------------------------------------------------------- /rollup.lib.prod.js: -------------------------------------------------------------------------------- 1 | const terser = require('rollup-plugin-terser').terser; 2 | const commonjs = require('@rollup/plugin-commonjs'); 3 | 4 | module.exports = function createLibConfig(config) { 5 | return { 6 | ...config, 7 | output: { 8 | ...config.output, 9 | globals: {} 10 | }, 11 | plugins: [ 12 | ...config.plugins || [], 13 | commonjs(), 14 | terser(), 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /packages/slice/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'slice', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/slice', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/utils/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'utils', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/utils', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/query/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'redux-query', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/redux-query', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/users/users-data.index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '@crux/crux'; 2 | import { dataService } from '../data.index'; 3 | import { usersHttpApi } from '../../http/users/users-http.index'; 4 | 5 | export const usersDataService = service( 6 | (data, users) => 7 | import('./users-data.service').then((mod) => mod.usersData(data.createResource, users)), 8 | { 9 | deps: [dataService, usersHttpApi], 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /packages/async-queue/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'async-queue', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/async-queue', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "forceConsistentCasingInFileNames": true, 5 | "noImplicitOverride": true, 6 | "noFallthroughCasesInSwitch": true 7 | }, 8 | "files": [], 9 | "include": [], 10 | "references": [ 11 | { 12 | "path": "./tsconfig.lib.json" 13 | }, 14 | { 15 | "path": "./tsconfig.spec.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/redux-types/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'redux-types', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/redux-types', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/crux/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | 5 | "plugins": ["prettier"], 6 | "overrides": [ 7 | { 8 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 9 | "rules": {} 10 | }, 11 | { 12 | "files": ["*.ts", "*.tsx"], 13 | "rules": {} 14 | }, 15 | { 16 | "files": ["*.js", "*.jsx"], 17 | "rules": {} 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/string-utils/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'string-utils', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/string-utils', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/dev/src/features/nav/nav.selectors.ts: -------------------------------------------------------------------------------- 1 | import type { RouterState } from '../../shared/router/router.slice'; 2 | import type { NavItem } from './nav.types'; 3 | 4 | export function selectNavItems(routerState: RouterState): NavItem[] { 5 | const { route } = routerState; 6 | 7 | return [ 8 | { 9 | icon: 'tasks', 10 | route: 'todos', 11 | text: 'Todos', 12 | active: route?.name === 'todos', 13 | }, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /packages/dynamic-store/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'dynamic-store', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/dynamic-store', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/event-emitter/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'event-emitter', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/event-emitter', 15 | }; 16 | -------------------------------------------------------------------------------- /packages/redux-registry/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'redux-registry', 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]s$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/packages/redux-registry', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/dev/src/shared/env/env.service.ts: -------------------------------------------------------------------------------- 1 | export enum Variable { 2 | VITE_API_URL, 3 | } 4 | 5 | export interface Env { 6 | get(key: Variable): string; 7 | } 8 | 9 | export function env(): Env { 10 | const variables: Record = { 11 | [Variable.VITE_API_URL]: import.meta.env.VITE_API_URL, 12 | }; 13 | 14 | return { 15 | get, 16 | }; 17 | 18 | function get(key: Variable) { 19 | return variables[key]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/dev/src/features/template/template.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | 3 | export interface Template {} 4 | 5 | export interface TemplateState { 6 | data: Template[]; 7 | } 8 | 9 | const initialState: TemplateState = { 10 | data: [], 11 | }; 12 | 13 | export type TemplateStateAPI = ReturnType['api']; 14 | 15 | export function createTemplateSlice(name: string) { 16 | return createSlice(name, initialState, {}); 17 | } 18 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { SharedService } from '../shared/service'; 3 | import { ServiceTwo } from './service'; 4 | 5 | export type SliceTwo = ReturnType['api']; 6 | 7 | export function sliceTwo(serviceTwo: ServiceTwo, shared: SharedService) { 8 | return createSlice( 9 | 'module1', 10 | { isTwo: true }, 11 | { 12 | set: () => ({ isTwo: true }), 13 | } 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /tools/scripts/packages/publish-single.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import { argv } from 'process'; 3 | import npmPublish from '@jsdevtools/npm-publish'; 4 | 5 | config(); 6 | 7 | const projectName = argv[2]; 8 | 9 | publish(projectName); 10 | 11 | export async function publish(name: string) { 12 | await npmPublish({ 13 | access: 'public', 14 | package: `dist/packages/${name}/package.json`, 15 | token: process.env.NPM_ACCESS_TOKEN, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | 3 | export interface LayoutState { 4 | darkModeToggle: boolean; 5 | toaster: boolean; 6 | } 7 | 8 | const initialState: LayoutState = { 9 | darkModeToggle: true, 10 | toaster: true, 11 | }; 12 | 13 | export type LayoutStateAPI = ReturnType['api']; 14 | 15 | export function createLayoutSlice(name: string) { 16 | return createSlice(name, initialState, {}); 17 | } 18 | -------------------------------------------------------------------------------- /packages/utils/src/lib/function.ts: -------------------------------------------------------------------------------- 1 | export function debounce( 2 | fn: (...props: T) => U, 3 | wait: number 4 | ): (...args: T) => void { 5 | let timeout: ReturnType; 6 | 7 | return (...args: T) => { 8 | clearTimeout(timeout); 9 | 10 | timeout = setTimeout(() => { 11 | fn(...args); 12 | }, wait); 13 | }; 14 | } 15 | 16 | export function sleep(ms: number) { 17 | return new Promise((resolve) => setTimeout(resolve, ms)); 18 | } 19 | -------------------------------------------------------------------------------- /tools/generators/middleware/files/index.ts__tmpl__: -------------------------------------------------------------------------------- 1 | import { AnyAction, Dispatch, MiddlewareAPI } from "@reduxjs/toolkit"; 2 | 3 | export function create<%= names.className %>Middlware() { 4 | return (api: MiddlewareAPI) => (next: Dispatch) => (action: AnyAction) => { 5 | if (action.type === '') { 6 | 7 | } 8 | 9 | switch (action.type) { 10 | case '': 11 | break; 12 | default: 13 | break; 14 | } 15 | 16 | next(action); 17 | }; 18 | } -------------------------------------------------------------------------------- /apps/dev/src/shared/router/router.index.ts: -------------------------------------------------------------------------------- 1 | import { service, slice } from '@crux/crux'; 2 | 3 | const config = { 4 | todos: '/todos', 5 | }; 6 | 7 | export const routerSlice = slice( 8 | () => import('./router.slice').then((m) => m.createRouterSlice(config)), 9 | { 10 | name: 'router', 11 | } 12 | ); 13 | 14 | export const routerService = service( 15 | (slice) => import('./router.service').then((m) => m.router(config, slice)), 16 | { 17 | deps: [routerSlice], 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /packages/utils/src/lib/classes.ts: -------------------------------------------------------------------------------- 1 | export { cx }; 2 | 3 | /** 4 | * Conditionally join classNames into a single string 5 | */ 6 | function cx(...args: Argument[]): string { 7 | let str = ''; 8 | let i = 0; 9 | let arg: unknown; 10 | 11 | while (i < args.length) { 12 | if ((arg = args[i++]) && typeof arg === 'string') { 13 | str && (str += ' '); 14 | str += arg; 15 | } 16 | } 17 | return str; 18 | } 19 | 20 | type Argument = string | boolean | null | undefined; 21 | -------------------------------------------------------------------------------- /apps/dev/src/features/nav/nav.index.ts: -------------------------------------------------------------------------------- 1 | import { view } from '@crux/crux'; 2 | import { createSelector } from 'reselect'; 3 | import { routerService, routerSlice } from '../../shared/router/router.index'; 4 | import { selectNavItems } from './nav.selectors'; 5 | 6 | export const navSelector = createSelector(routerSlice.selector, selectNavItems); 7 | 8 | export const navView = view(() => import('./nav.view').then((m) => m.navView), { 9 | actions: routerService, 10 | data: navSelector, 11 | root: 'nav', 12 | }); 13 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "async-queue": "packages/async-queue", 5 | "dynamic-store": "packages/dynamic-store", 6 | "dev": "apps/dev", 7 | "event-emitter": "packages/event-emitter", 8 | "query": "packages/query", 9 | "redux-registry": "packages/redux-registry", 10 | "slice": "packages/slice", 11 | "redux-types": "packages/redux-types", 12 | "string-utils": "packages/string-utils", 13 | "utils": "packages/utils", 14 | "crux": "packages/crux" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { SharedService } from '../shared/service'; 3 | import { ServiceOne } from './service'; 4 | 5 | export type SliceOne = ReturnType['api']; 6 | 7 | export function sliceOne(serviceOne: ServiceOne, shared: SharedService) { 8 | const slice = createSlice( 9 | 'sliceOne', 10 | { isOne: true }, 11 | { 12 | set: () => ({ isOne: true }), 13 | } 14 | ); 15 | 16 | return slice; // api, middleware, reducer 17 | } 18 | -------------------------------------------------------------------------------- /packages/redux-registry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "forceConsistentCasingInFileNames": true, 5 | "strict": true, 6 | "noImplicitOverride": true, 7 | "noPropertyAccessFromIndexSignature": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true 10 | }, 11 | "files": [], 12 | "include": [], 13 | "references": [ 14 | { 15 | "path": "./tsconfig.lib.json" 16 | }, 17 | { 18 | "path": "./tsconfig.spec.json" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.module.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { merge } from '@crux/utils'; 3 | import type { LayoutState } from './types'; 4 | 5 | const initialState: LayoutState = { 6 | roots: { 7 | sidebar: true, 8 | top: true, 9 | }, 10 | }; 11 | 12 | export function createLayoutModule() { 13 | return createSlice('layout', initialState, { 14 | toggleSidebar: (state: LayoutState) => 15 | merge(state, { 16 | roots: { 17 | sidebar: !state.roots.sidebar, 18 | }, 19 | }), 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.types.ts: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | createdAt: number; 3 | id: string; 4 | removing?: boolean; 5 | status: Status; 6 | text: string; 7 | updatedAt: number | null; 8 | } 9 | 10 | export interface TodosState { 11 | draggingTaskId?: string; 12 | hoveringState: { ndx: number; status: Status } | null; 13 | } 14 | 15 | export type Status = 'to-do' | 'in-progress' | 'completed'; 16 | 17 | export type PostTask = Omit; 18 | 19 | export type PutTask = Omit; 20 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.reducers.test.ts: -------------------------------------------------------------------------------- 1 | import { getMockTodosState } from '../data/todos.mock'; 2 | import { setDraggingTaskId } from './todos.reducers'; 3 | 4 | describe('Todos Reducers', () => { 5 | describe('setDraggingTaskId', () => { 6 | it('should set the id of the task that has just been picked up', () => { 7 | const state = getMockTodosState(); 8 | const draggingTaskId = '123'; 9 | const result = setDraggingTaskId(state, draggingTaskId); 10 | 11 | expect(result.draggingTaskId).toBe(draggingTaskId); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /apps/dev/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "importsNotUsedAsValues": "error", 7 | "module": "esnext", 8 | "preserveValueImports": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "strictNullChecks": false, 13 | "types": ["node", "@cloudflare/workers-types"] 14 | }, 15 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"], 16 | "include": ["**/*.ts", "**/*.svelte"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/crux/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": false, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": false, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/slice/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": false, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/async-queue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/event-emitter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/redux-types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/string-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Crux Dev 8 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/dynamic-store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/slice/todos.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { setDraggingTaskId, setHoveringState } from '../domain/todos.reducers'; 3 | import type { TodosState } from '../domain/todos.types'; 4 | 5 | const initialState = { 6 | hoveringState: undefined, 7 | draggingTaskId: undefined, 8 | } as TodosState; 9 | 10 | export type TodosStateAPI = ReturnType['api']; 11 | 12 | export function createTodosSlice(name: string) { 13 | return createSlice(name, initialState, { 14 | setDraggingTaskId, 15 | setHoveringState, 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /apps/dev/src/features/toaster/toaster.index.ts: -------------------------------------------------------------------------------- 1 | import { service, slice, view } from '@crux/crux'; 2 | 3 | export const toasterSlice = slice( 4 | () => import('./toaster.slice').then((m) => m.createToasterSlice('toaster')), 5 | { name: 'toaster' } 6 | ); 7 | 8 | export const toasterService = service( 9 | (api) => import('./toaster.service').then((m) => m.toaster(api)), 10 | { deps: [toasterSlice] } 11 | ); 12 | 13 | export const toasterView = view(() => import('./toaster.view').then((m) => m.toasterView), { 14 | actions: toasterService, 15 | data: toasterSlice.selector, 16 | root: 'toaster', 17 | }); 18 | -------------------------------------------------------------------------------- /apps/dev/src/features/template/template.index.ts: -------------------------------------------------------------------------------- 1 | import { service, slice, view } from '@crux/crux'; 2 | 3 | export const templateSlice = slice( 4 | () => import('./template.slice').then((m) => m.createTemplateSlice('template')), 5 | { name: 'template' } 6 | ); 7 | 8 | export const templateService = service( 9 | (api) => import('./template.service').then((m) => m.template(api)), 10 | { deps: [templateSlice] } 11 | ); 12 | 13 | export const templateView = view(() => import('./template.view').then((m) => m.templateView), { 14 | actions: templateService, 15 | data: templateSlice.selector, 16 | root: 'template', 17 | }); 18 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.reducers.ts: -------------------------------------------------------------------------------- 1 | import { merge } from '@crux/utils'; 2 | import type { TodosState } from './todos.types'; 3 | 4 | /** 5 | * Set the id of the task that has just been picked up. 6 | */ 7 | export function setDraggingTaskId(state: TodosState, draggingTaskId: string) { 8 | return merge(state, { draggingTaskId }); 9 | } 10 | 11 | /** 12 | * Sets the index and status where the dragging task is hovering. Note that the index is always +/- 0.5 of a task's index. 13 | */ 14 | export function setHoveringState(state: TodosState, hoveringState: TodosState['hoveringState']) { 15 | return merge(state, { hoveringState }); 16 | } 17 | -------------------------------------------------------------------------------- /apps/dev/src/design/icon/icon.ts: -------------------------------------------------------------------------------- 1 | import { cx } from '@crux/utils'; 2 | import { html } from 'lit'; 3 | 4 | const icons = { 5 | bell: 'las la-bell', 6 | 'check-circle': 'las la-check-circle', 7 | mountain: 'las la-mountain', 8 | plus: 'las la-plus', 9 | 'plus-circle': 'las la-plus-circle', 10 | 'shipping-fast': 'las la-shipping-fast', 11 | spinner: 'las la-spinner', 12 | stream: 'las la-stream', 13 | tasks: 'las la-tasks', 14 | 'times-circle': 'las la-times-circle', 15 | user: 'las la-user', 16 | } as Record; 17 | 18 | export function icon(type: string, classname?: string) { 19 | return html``; 20 | } 21 | -------------------------------------------------------------------------------- /packages/query/src/lib/query/reducer.ts: -------------------------------------------------------------------------------- 1 | import { State } from '../types'; 2 | import { Action } from '@crux/redux-types'; 3 | 4 | export function createReducer(initialState: Record>) { 5 | return function reducer(state: Record>, action: Action>) { 6 | const currentState = state || initialState; 7 | 8 | if (action.type.startsWith('__cruxQuery')) { 9 | return { 10 | ...currentState, 11 | [action.meta.id]: { 12 | ...currentState[action.meta.id], 13 | ...action.payload, 14 | }, 15 | }; 16 | } 17 | 18 | return state || initialState; 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nx/presets/core.json", 3 | "npmScope": "crux", 4 | "affected": { 5 | "defaultBase": "main" 6 | }, 7 | "tasksRunnerOptions": { 8 | "default": { 9 | "runner": "@nrwl/nx-cloud", 10 | "options": { 11 | "cacheableOperations": ["build", "lint", "test", "e2e"], 12 | "accessToken": "MmViNmI0MjAtNWQ4OS00ZGU2LWE0MzgtYWZlYWVhODJhNzUxfHJlYWQtd3JpdGU=" 13 | } 14 | } 15 | }, 16 | "defaultProject": "dev", 17 | "pluginsConfig": { 18 | "@nrwl/js": { 19 | "analyzeSourceFiles": true 20 | } 21 | }, 22 | "targetDefaults": { 23 | "build": { 24 | "dependsOn": ["^build"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.view.ts: -------------------------------------------------------------------------------- 1 | import { html, render } from 'lit'; 2 | import { toggle as toggleSwitch } from '../../design/toggle/toggle'; 3 | 4 | interface Actions { 5 | toggle(): void; 6 | } 7 | 8 | interface Data { 9 | isDark: boolean; 10 | } 11 | 12 | export function createDarkModeView(root: HTMLElement) { 13 | return function (data: Data, actions: Actions) { 14 | const { toggle } = actions; 15 | const { isDark } = data; 16 | 17 | render( 18 | html` 19 | ${toggleSwitch({ 20 | isOn: isDark, 21 | label: 'Toggle dark mode', 22 | toggle, 23 | })} 24 | `, 25 | root 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /apps/dev/src/design/alert/alert.module.scss: -------------------------------------------------------------------------------- 1 | .alert { 2 | background-color: var(--alert-background-color); 3 | border: var(--alert-border); 4 | border-radius: var(--alert-border-radius); 5 | padding: var(--alert-padding); 6 | 7 | &.danger { 8 | border-top: var(--alert-highlight-border-width) solid var(--alert-color-danger); 9 | } 10 | 11 | &.info { 12 | border-top: var(--alert-highlight-border-width) solid var(--alert-color-info); 13 | } 14 | 15 | &.success { 16 | border-top: var(--alert-highlight-border-width) solid var(--alert-color-success); 17 | } 18 | 19 | &.warning { 20 | border-top: var(--alert-highlight-border-width) solid var(--alert-color-warning); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | .env 12 | .env.local 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /packages/crux/src/lib/crux-with-emitter.ts: -------------------------------------------------------------------------------- 1 | import { createEventEmitter } from '@crux/event-emitter'; 2 | import { Subscription } from './subscription'; 3 | import { Slice, View } from './types'; 4 | import { Events, Options, crux } from './crux'; 5 | 6 | export function cruxWithEmitter( 7 | args: { 8 | layoutSelector?: (state: any) => { [key: string]: string }; 9 | root?: HTMLElement; 10 | slices: Slice[]; 11 | subscriptions: Subscription[]; 12 | views: View[]; 13 | }, 14 | options: Options = {} 15 | ) { 16 | const emitter = createEventEmitter(); 17 | 18 | const app = crux(args, { 19 | ...options, 20 | emitter, 21 | }); 22 | 23 | return { 24 | ...app, 25 | ...emitter, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/column/column.ts: -------------------------------------------------------------------------------- 1 | import { cx } from '@crux/utils'; 2 | import { LitElement, html, css, unsafeCSS } from 'lit'; 3 | import { customElement, property } from 'lit/decorators.js'; 4 | import type { Status } from '../../domain/todos.types'; 5 | import styles from './column.css?inline'; 6 | 7 | @customElement('todos-column') 8 | export class TodosColumn extends LitElement { 9 | static styles = [unsafeCSS(styles)]; 10 | 11 | @property({ type: String }) 12 | status: Status; 13 | 14 | render() { 15 | return html`
16 | 17 |
`; 18 | } 19 | } 20 | 21 | declare global { 22 | interface HTMLElementTagNameMap { 23 | 'todos-column': TodosColumn; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/users/transformations.ts: -------------------------------------------------------------------------------- 1 | import type { PutUser, Response, User } from '../../http/users/users-http.service'; 2 | 3 | export function toData(response: Response) { 4 | return response.data; 5 | } 6 | 7 | export function deleteUser(data: User[] | null, id: number) { 8 | if (data === null) { 9 | return data; 10 | } 11 | 12 | return data.filter((u) => id !== u.id); 13 | } 14 | 15 | export function mergeUser(data: User[] | null, user: PutUser) { 16 | if (data === null) { 17 | return data; 18 | } 19 | 20 | const ndx = data?.findIndex((u) => u.id === user.id); 21 | 22 | if (ndx === -1) { 23 | return data; 24 | } 25 | 26 | data[ndx] = { 27 | ...data[ndx], 28 | ...user, 29 | }; 30 | 31 | return data; 32 | } 33 | -------------------------------------------------------------------------------- /tools/generators/middleware/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "cli": "nx", 4 | "$id": "middleware", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "The name of the middleware", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What's the name of the middleware?" 15 | }, 16 | "path": { 17 | "type": "string", 18 | "description": "Specify a path for the middleware folder if not packages/dev/src/app/middleware/[name]", 19 | "x-prompt": "Specify a path for the middleware folder if not packages/dev/src/app/middleware/[name]" 20 | } 21 | }, 22 | "required": ["name", "path"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/dev/src/design/toggle/toggle.module.scss: -------------------------------------------------------------------------------- 1 | .background { 2 | cursor: pointer; 3 | position: relative; 4 | border-radius: 11px; 5 | width: 40px; 6 | height: 22px; 7 | border: 1px solid var(--toggle-border-color); 8 | background-color: var(--toggle-background-color); 9 | transition: border-color 0.25s; 10 | 11 | &:hover { 12 | border-color: var(--toggle-border-color-highlight); 13 | } 14 | } 15 | 16 | .switch { 17 | position: absolute; 18 | top: 1px; 19 | left: 1px; 20 | width: 18px; 21 | height: 18px; 22 | border-radius: 50%; 23 | background-color: var(--toggle-switch-background-color); 24 | box-shadow: var(--toggle-switch-shadow-color); 25 | transition: transform 0.25s; 26 | 27 | &.on { 28 | transform: translate(18px); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/dev/src/design/toggle/toggle.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import type { TemplateResult } from 'lit'; 3 | import toggleStyles from './toggle.module.scss'; 4 | import { cx } from '@crux/utils'; 5 | 6 | export function toggle({ 7 | content, 8 | isOn, 9 | label, 10 | toggle: toggleHandler, 11 | }: { 12 | content?: TemplateResult; 13 | isOn: boolean; 14 | label: string; 15 | toggle: () => void; 16 | }) { 17 | return html` 18 | 29 | `; 30 | } 31 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.index.ts: -------------------------------------------------------------------------------- 1 | import { service, slice, view } from '@crux/crux'; 2 | import { cacheService } from '../../shared/cache/cache.index'; 3 | 4 | export const darkModeSlice = slice( 5 | (cache) => import('./dark-mode.slice').then((m) => m.createDarkModeSlice(cache)), 6 | { 7 | deps: [cacheService], 8 | name: 'darkMode', 9 | } 10 | ); 11 | 12 | export const darkModeService = service( 13 | (slice, cache) => import('./dark-mode.service').then((m) => m.darkMode(slice, cache)), 14 | { 15 | deps: [darkModeSlice, cacheService], 16 | } 17 | ); 18 | 19 | export const darkModeView = view( 20 | () => import('./dark-mode.view').then((m) => m.createDarkModeView), 21 | { 22 | actions: darkModeService, 23 | data: darkModeSlice.selector, 24 | root: 'dark-mode-toggle', 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.index.ts: -------------------------------------------------------------------------------- 1 | import { slice, view } from '@crux/crux'; 2 | import { createSelector } from 'reselect'; 3 | import { routerSlice } from '../shared/router/router.index'; 4 | import { layout } from './layout.selectors'; 5 | 6 | /** 7 | * Slice 8 | * ===== 9 | */ 10 | export const layoutSlice = slice( 11 | () => import('./layout.slice').then((m) => m.createLayoutSlice('layout')), 12 | { 13 | name: 'layout', 14 | } 15 | ); 16 | 17 | /** 18 | * Data 19 | * ========= 20 | */ 21 | export type LayoutData = ReturnType; 22 | 23 | export const data = createSelector([layoutSlice.selector, routerSlice.selector], layout); 24 | 25 | /** 26 | * View 27 | * ==== 28 | */ 29 | export const layoutView = view(() => import('./layout.view').then((m) => m.createLayoutView), { 30 | data, 31 | root: 'root', 32 | }); 33 | -------------------------------------------------------------------------------- /packages/utils/src/lib/promise/create-externally-resolvable-promise.spec.ts: -------------------------------------------------------------------------------- 1 | import { createExternallyResolvablePromise } from './create-externally-resolvable-promise'; 2 | 3 | describe('Utils/Promise: createExternallyResolvablePromise', () => { 4 | it('should resolve with external method', () => { 5 | const result = 'foobar'; 6 | const { promise, resolve } = createExternallyResolvablePromise(); 7 | 8 | resolve(result); 9 | 10 | return promise.then((res) => { 11 | expect(res).toEqual(result); 12 | }); 13 | }); 14 | 15 | it('should reject with external method', () => { 16 | const result = 'bazbar'; 17 | const { promise, reject } = createExternallyResolvablePromise(); 18 | 19 | reject(result); 20 | 21 | return promise.catch((res) => { 22 | expect(res).toEqual(result); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/views/todos.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin-bottom: 1rem; 3 | display: flex; 4 | align-items: center; 5 | } 6 | 7 | .title i:not(button *) { 8 | margin-right: 0.5rem; 9 | font-size: 1.5rem; 10 | } 11 | 12 | .title button { 13 | margin-left: auto; 14 | background-color: transparent; 15 | color: var(--color-warning); 16 | border: none; 17 | border-radius: var(--border-radius); 18 | border: 1px solid var(--color-warning); 19 | aspect-ratio: 1; 20 | cursor: pointer; 21 | } 22 | 23 | .title button:hover { 24 | background-color: var(--color-warning); 25 | color: white; 26 | } 27 | 28 | .title.to-do i:not(button *) { 29 | color: var(--color-info-light); 30 | } 31 | 32 | .title.in-progress i { 33 | color: var(--color-primary-light); 34 | } 35 | 36 | .title.completed i { 37 | color: var(--color-success-light); 38 | } 39 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.module.scss: -------------------------------------------------------------------------------- 1 | // Top Bar 2 | // ============================== 3 | 4 | .top { 5 | grid-area: top; 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | 10 | height: var(--top-height); 11 | 12 | background-color: var(--top-bg); 13 | } 14 | 15 | .logo { 16 | display: flex; 17 | align-items: center; 18 | padding: 0 var(--padding); 19 | 20 | img { 21 | width: 70px; 22 | } 23 | } 24 | 25 | .nav { 26 | width: 100%; 27 | } 28 | 29 | .right-nav { 30 | padding-right: var(--padding); 31 | } 32 | 33 | // Main Content 34 | // ============================== 35 | 36 | .main { 37 | grid-area: main; 38 | height: 100%; 39 | overflow-y: scroll; 40 | } 41 | 42 | // Toaster 43 | // ============================== 44 | 45 | .toaster { 46 | position: fixed; 47 | bottom: 0; 48 | right: 0; 49 | } 50 | -------------------------------------------------------------------------------- /packages/utils/src/lib/validate.ts: -------------------------------------------------------------------------------- 1 | export async function validate( 2 | value: T, 3 | validators: ((val: T) => Promise | ValidationResult)[] 4 | ) { 5 | const results = [] as Promise[]; 6 | 7 | for (const validator of validators) { 8 | const res = validator(value); 9 | 10 | if ((res as Promise).then) { 11 | results.push(res as Promise); 12 | } else { 13 | results.push(Promise.resolve(res)); 14 | } 15 | } 16 | 17 | const res = await Promise.all(results); 18 | 19 | return { 20 | errors: res.filter((r) => r && !r.success).map((validation) => validation.message), 21 | messages: res.filter((r) => r && r.success).map((validation) => validation.message), 22 | }; 23 | } 24 | 25 | export type ValidationResult = { 26 | message: string; 27 | success: boolean; 28 | }; 29 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/expanding-dropzone/expanding-dropzone.css: -------------------------------------------------------------------------------- 1 | .dropzone { 2 | width: calc(100% + 0.125rem); 3 | height: 100%; 4 | position: relative; 5 | top: -0.625rem; 6 | left: -0.125rem; 7 | } 8 | 9 | .dropzone:hover { 10 | border-top: 2px solid var(--todos-drag-separator-color); 11 | } 12 | 13 | .handle { 14 | opacity: 0; 15 | border-radius: 4px; 16 | width: 0.5rem; 17 | height: 0.5rem; 18 | background-color: var(--todos-drag-separator-color); 19 | } 20 | 21 | .handle i { 22 | color: white; 23 | } 24 | 25 | .dropzone:hover .handle { 26 | opacity: 1; 27 | position: absolute; 28 | } 29 | 30 | .dropzone:hover .left { 31 | left: 0; 32 | transform: translateX(-0.4375rem); 33 | } 34 | 35 | .dropzone:hover .right { 36 | right: 0; 37 | transform: translateX(0.5rem); 38 | } 39 | 40 | .dropzone:hover .handle { 41 | top: -0.3125rem; 42 | } 43 | -------------------------------------------------------------------------------- /packages/utils/src/lib/any.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from './object'; 2 | 3 | export type RecursivePartial = { 4 | [P in keyof T]?: T[P] extends (infer U)[] 5 | ? RecursivePartial[] 6 | : T[P] extends object 7 | ? RecursivePartial 8 | : T[P]; 9 | }; 10 | 11 | export type Merge> = (dest: T, source: RecursivePartial) => T; 12 | 13 | export function merge>(dest: T, source: RecursivePartial): T { 14 | const obj = { ...dest } as Record; 15 | 16 | for (const [key, val] of Object.entries(source)) { 17 | if (Array.isArray(val)) { 18 | obj[key] = [...val]; 19 | } else if (isPlainObject(val)) { 20 | obj[key] = { ...merge(obj[key], source[key] as RecursivePartial) }; 21 | } else { 22 | obj[key] = val; 23 | } 24 | } 25 | 26 | return obj as T; 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nrwl/nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nrwl/nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /apps/dev/src/shared/router/utils/get-url-from-route.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from '../router.slice'; 2 | 3 | export function getUrlFromRoute>( 4 | config: T, 5 | name: keyof T, 6 | params: Route['params'], 7 | search: Route['search'], 8 | hash: Route['hash'] 9 | ): string { 10 | let pathname = config[name]; 11 | 12 | while (params && pathname.match(/(:)\w+/)) { 13 | const param = pathname.match(/(:)\w+/)[0]; 14 | const value = params[param.replace(':', '')]; 15 | 16 | pathname = pathname.replace(param, value) as T[keyof T]; 17 | } 18 | 19 | const searchStr = search 20 | ? Object.entries(search) 21 | .map(([key, value]) => `${key}=${value}`) 22 | .join('&') 23 | : ''; 24 | 25 | return `${window.location.origin}${pathname}${searchStr.length ? `?${searchStr}` : ''}${ 26 | hash ? `#${hash}` : '' 27 | }`; 28 | } 29 | -------------------------------------------------------------------------------- /apps/dev/src/features/toaster/toaster.view.ts: -------------------------------------------------------------------------------- 1 | import { render } from 'lit'; 2 | import { repeat } from 'lit/directives/repeat.js'; 3 | import type { Alert, ToasterState } from './toaster.slice'; 4 | import type { ToasterService } from './toaster.service'; 5 | import toasterStyles from './toaster.module.scss'; 6 | import { alert } from '../../design/alert/alert'; 7 | import { cx } from '@crux/utils'; 8 | 9 | export function toasterView(root: HTMLElement) { 10 | return function toast(data: ToasterState, actions: ToasterService): void { 11 | const { alerts } = data; 12 | const { close } = actions; 13 | 14 | render(template(alerts), root); 15 | 16 | function template(toRender: Alert[]) { 17 | return repeat( 18 | toRender, 19 | (a: Alert) => a.id, 20 | (a: Alert) => alert(a, close, cx(toasterStyles['alert'], ' animate__fadeInRight')) 21 | ); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /apps/dev/src/shared/cache/cache.service.ts: -------------------------------------------------------------------------------- 1 | import type { Cache } from './types'; 2 | 3 | export function cache(storage: Storage = localStorage): Cache { 4 | return { 5 | clear, 6 | get, 7 | remove, 8 | set, 9 | }; 10 | 11 | function clear() { 12 | return storage.clear(); 13 | } 14 | 15 | function get(key: string): unknown | null { 16 | try { 17 | return JSON.parse(storage.getItem(key) as unknown as string) as unknown; 18 | } catch (e: unknown) { 19 | throw new Error('Could not JSON.parse cached value'); 20 | } 21 | } 22 | 23 | function remove(key: string): void { 24 | return storage.removeItem(key); 25 | } 26 | 27 | function set(key: string, value: unknown): void { 28 | try { 29 | return storage.setItem(key, JSON.stringify(value)); 30 | } catch (e: unknown) { 31 | throw new Error('Could not JSON.stringify value'); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tools/generators/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Tree, formatFiles, generateFiles, joinPathFragments, names } from '@nrwl/devkit'; 2 | import { ComponentSchema } from './schema'; 3 | 4 | export default async function (tree: Tree, schema: ComponentSchema) { 5 | const { 6 | name, path, 7 | } = schema; 8 | 9 | const middlewareNames = names(name); 10 | 11 | generateFiles( 12 | // virtual file system 13 | tree, 14 | 15 | // the location where the template files are 16 | joinPathFragments(__dirname, './files'), 17 | 18 | // where the files should be generated 19 | path || `./packages/dev/src/app/middleware/${name}`, 20 | 21 | // the variables to be substituted in the template 22 | { 23 | // remove __tmpl__ from file endings 24 | tmpl: '', 25 | filename: middlewareNames.className, 26 | names: middlewareNames, 27 | } 28 | ); 29 | 30 | await formatFiles(tree); 31 | } -------------------------------------------------------------------------------- /apps/dev/src/features/todos/data/todos.data.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '@crux/query'; 2 | import type { PostTask, PutTask } from '../domain/todos.types'; 3 | import type { TodosHttpApi } from '../domain/todos.http'; 4 | import { toData } from './transformations'; 5 | 6 | export type TodosData = ReturnType; 7 | 8 | export function todosData(createResource: API['createResource'], todos: TodosHttpApi) { 9 | const resource = createResource('todos', { 10 | query: async () => todos.getAll().then(toData), 11 | mutations: { 12 | createTask: { 13 | query: async (task: PostTask) => todos.createTask(task), 14 | }, 15 | updateTask: { 16 | query: async (task: PutTask, statusNdx?: number) => todos.updateTask(task, statusNdx), 17 | }, 18 | }, 19 | options: { 20 | lazy: true, 21 | keepUnusedDataFor: 60, 22 | pollingInterval: 60, 23 | }, 24 | }); 25 | 26 | return resource.subscribe(); 27 | } 28 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { merge } from '@crux/utils'; 3 | import type { Cache } from '../../shared/cache/types'; 4 | import { DARK_MODE_CACHE_KEY } from './dark-mode.config'; 5 | import { isBoolean } from './utils/is-boolean'; 6 | 7 | export type DarkModeState = { 8 | isDark: boolean; 9 | }; 10 | 11 | export type DarkModeSlice = ReturnType['api']; 12 | 13 | export function createDarkModeSlice(cache: Cache) { 14 | const initialState = { 15 | isDark: valueFromCache(), 16 | }; 17 | 18 | return createSlice('darkMode', initialState, { 19 | setDarkMode: (state: DarkModeState, payload: boolean) => 20 | merge(state, { 21 | isDark: payload, 22 | }), 23 | }); 24 | 25 | function valueFromCache() { 26 | const cachedDarkMode = cache.get(DARK_MODE_CACHE_KEY); 27 | 28 | return isBoolean(cachedDarkMode) ? cachedDarkMode : false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/crux/src/lib/crux.test.ts: -------------------------------------------------------------------------------- 1 | import { sliceOne, subscriptionOne, viewOne } from './mock/one'; 2 | import { sliceTwo, subscriptionTwo, viewTwo } from './mock/two'; 3 | import { cruxWithEmitter as crux } from './crux-with-emitter'; 4 | 5 | import { layoutView } from './mock/layout'; 6 | 7 | describe('crux', () => { 8 | it('should create an crux', (done) => { 9 | const slices = [sliceOne, sliceTwo]; 10 | 11 | const subscriptions = [subscriptionOne, subscriptionTwo]; 12 | 13 | const views = [layoutView, viewOne, viewTwo]; 14 | 15 | const app = crux({ 16 | slices, 17 | subscriptions, 18 | views, 19 | }); 20 | 21 | app.once('afterSubscriptions', (data) => { 22 | const state = app.store.getState(); 23 | 24 | expect(state).toEqual({ 25 | sliceOne: { 26 | isOne: true, 27 | }, 28 | sliceTwo: { 29 | isTwo: true, 30 | }, 31 | }); 32 | 33 | done(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/task/task.css: -------------------------------------------------------------------------------- 1 | .task { 2 | box-sizing: border-box; 3 | position: relative; 4 | border: 1px solid white; 5 | width: 100%; 6 | cursor: grab; 7 | padding: 1rem; 8 | margin-bottom: 1rem; 9 | background-color: var(--color-grey-850); 10 | border: none; 11 | border-radius: 3px; 12 | } 13 | 14 | .task.to-do { 15 | background-color: var(--color-info-dark); 16 | } 17 | 18 | .task.in-progress { 19 | background-color: var(--color-primary-dark); 20 | } 21 | 22 | .task.completed { 23 | background-color: var(--color-success-dark); 24 | } 25 | 26 | .task.completed .title { 27 | text-decoration: line-through; 28 | } 29 | 30 | .dragging { 31 | background-color: var(--card-background-faded-color); 32 | border: 1px dashed var(--card-border-faded-color); 33 | user-select: none; 34 | position: absolute; 35 | z-index: 1000; 36 | transition: none; 37 | pointer-events: none; 38 | opacity: 0.75; 39 | border: 1px dashed whitesmoke; 40 | cursor: grabbing; 41 | } 42 | -------------------------------------------------------------------------------- /apps/dev/src/features/dark-mode/dark-mode.service.ts: -------------------------------------------------------------------------------- 1 | import type { Cache } from '../../shared/cache/types'; 2 | import { DARK_MODE_CACHE_KEY } from './dark-mode.config'; 3 | import type { DarkModeSlice } from './dark-mode.slice'; 4 | 5 | export function darkMode(darkModeSlice: DarkModeSlice, cache: Cache) { 6 | return { 7 | set, 8 | toggle, 9 | }; 10 | 11 | function set(isDark: boolean) { 12 | cache.set(DARK_MODE_CACHE_KEY, isDark); 13 | 14 | if (isDark) { 15 | document.body.classList.add('dark'); 16 | } else { 17 | document.body.classList.remove('dark'); 18 | } 19 | 20 | darkModeSlice.setDarkMode(isDark); 21 | } 22 | 23 | function toggle() { 24 | const isDark = !darkModeSlice.getState().isDark; 25 | 26 | cache.set(DARK_MODE_CACHE_KEY, isDark); 27 | 28 | if (isDark) { 29 | document.body.classList.add('dark'); 30 | } else { 31 | document.body.classList.remove('dark'); 32 | } 33 | 34 | darkModeSlice.setDarkMode(isDark); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/one/index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '../../service'; 2 | import { slice } from '@crux/crux'; 3 | import { subscription } from '../../subscription'; 4 | import { view } from '../../view'; 5 | import { sharedService } from '../shared'; 6 | import { selectorOne } from './selector'; 7 | 8 | export const serviceOne = service(() => import('./service').then((mod) => mod.serviceOne())); 9 | 10 | export const sliceOne = slice( 11 | (one, shared) => import('./slice').then((mod) => mod.sliceOne(one, shared)), 12 | { 13 | deps: [serviceOne, sharedService], 14 | name: 'sliceOne', 15 | } 16 | ); 17 | 18 | export const subscriptionOne = subscription( 19 | () => import('./subscription').then((mod) => mod.subscriptionOne), 20 | { deps: [sliceOne, selectorOne] } 21 | ); 22 | 23 | export const viewOne = view(() => import('./view').then((mod) => mod.viewOne), { 24 | actions: serviceOne, 25 | data: selectorOne, 26 | root: 'viewOne', 27 | }); 28 | 29 | export const selectOne = sliceOne.selector; 30 | -------------------------------------------------------------------------------- /apps/dev/src/shared/router/router.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { merge } from '@crux/utils'; 3 | import { getRouteFromUrl } from './utils/get-route-from-url'; 4 | import { getUrlFromRoute } from './utils/get-url-from-route'; 5 | 6 | export type Route = any> = { 7 | name: keyof T & string; 8 | hash?: string; 9 | params?: Record; 10 | search?: Record; 11 | }; 12 | 13 | export interface RouterState { 14 | route: Route | null; 15 | } 16 | 17 | export type RouterStateAPI = ReturnType['api']; 18 | 19 | export function createRouterSlice>(config: T) { 20 | const initialState = { 21 | route: getRouteFromUrl(config, window.location.href), 22 | }; 23 | 24 | const slice = createSlice('router', initialState, { 25 | navigateSuccess: (state: RouterState, route: Route) => 26 | merge(state, { 27 | route, 28 | }), 29 | }); 30 | 31 | return slice; 32 | } 33 | -------------------------------------------------------------------------------- /apps/dev/src/design/alert/alert.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit'; 2 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 3 | import { styleMap } from 'lit-html/directives/style-map.js'; 4 | import type { Alert } from '../../features/toaster/toaster.slice'; 5 | import alertStyles from './alert.module.scss'; 6 | import { cx } from '@crux/utils'; 7 | 8 | export function alert( 9 | { animationDuration, html: alertHTML, id, removing, text, variant }: Alert, 10 | close: (id: string) => void, 11 | className: string 12 | ) { 13 | const fullClassName = cx( 14 | alertStyles['alert'], 15 | alertStyles[variant], 16 | className, 17 | 'animate__animated', 18 | removing ? 'animate__fadeOutRight' : '' 19 | ); 20 | 21 | const style = styleMap({ 22 | '--animate-duration': `${(animationDuration ?? 0) / 1000}s`, 23 | }); 24 | 25 | return html``; 29 | } 30 | -------------------------------------------------------------------------------- /apps/dev/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "projectType": "application", 4 | "sourceRoot": "apps/app-client/src", 5 | "tags": [], 6 | "targets": { 7 | "serve": { 8 | "executor": "@nrwl/workspace:run-commands", 9 | "options": { 10 | "command": "vite serve apps/dev --config apps/dev/vite.config.ts --port 3000" 11 | } 12 | }, 13 | "preview": { 14 | "executor": "@nrwl/workspace:run-commands", 15 | "options": { 16 | "command": "vite preview apps/dev --config apps/dev/vite.config.ts" 17 | } 18 | }, 19 | "build": { 20 | "executor": "@nrwl/workspace:run-commands", 21 | "options": { 22 | "command": "vite build apps/dev --config apps/dev/vite.config.ts" 23 | } 24 | }, 25 | "test": { 26 | "executor": "@nrwl/jest:jest", 27 | "outputs": ["coverage/apps/dev"], 28 | "options": { 29 | "jestConfig": "apps/dev/jest.config.ts", 30 | "passWithNoTests": true 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/crux/src/lib/mock/two/index.ts: -------------------------------------------------------------------------------- 1 | import { service } from '../../service'; 2 | import { slice } from '@crux/crux'; 3 | import { subscription } from '../../subscription'; 4 | import { view } from '../../view'; 5 | import { sharedService } from '../shared'; 6 | import { selectorTwo } from './selector'; 7 | export { selectorTwo } from './selector'; 8 | 9 | export const serviceTwo = service(() => import('./service').then((mod) => mod.serviceTwo())); 10 | 11 | export const sliceTwo = slice( 12 | (two, shared) => import('./slice').then((mod) => mod.sliceTwo(two, shared)), 13 | { 14 | deps: [serviceTwo, sharedService], 15 | name: 'sliceTwo', 16 | } 17 | ); 18 | 19 | export const subscriptionTwo = subscription( 20 | () => import('./subscription').then((mod) => mod.subscriptionTwo), 21 | { deps: [sliceTwo, selectorTwo] } 22 | ); 23 | 24 | export const viewTwo = view(() => import('./view').then((mod) => mod.viewTwo), { 25 | actions: sliceTwo, 26 | data: selectorTwo, 27 | root: 'viewTwo', 28 | }); 29 | 30 | export const selectTwo = sliceTwo.selector; 31 | -------------------------------------------------------------------------------- /apps/dev/src/utils/relative-time.ts: -------------------------------------------------------------------------------- 1 | export function relativeTime(time: number) { 2 | const delta = Math.round((+new Date() - time) / 1000); 3 | 4 | const minute = 60; 5 | const hour = minute * 60; 6 | const day = hour * 24; 7 | const week = day * 7; 8 | 9 | let ret: string; 10 | 11 | if (delta < 30) { 12 | ret = 'just now'; 13 | } else if (delta < minute) { 14 | ret = `${delta} seconds ago`; 15 | } else if (delta < 2 * minute) { 16 | ret = 'a minute ago'; 17 | } else if (delta < hour) { 18 | ret = `${Math.floor(delta / minute)} minutes ago`; 19 | } else if (Math.floor(delta / hour) === 1) { 20 | ret = '1 hour ago'; 21 | } else if (delta < day) { 22 | ret = `${Math.floor(delta / hour)} hours ago`; 23 | } else if (delta < day * 2) { 24 | ret = 'yesterday'; 25 | } else if (delta < week) { 26 | ret = `${Math.floor(delta / day)} days ago`; 27 | } else if (Math.floor(delta / week) === 1) { 28 | ret = '1 week ago'; 29 | } else { 30 | ret = `${Math.floor(delta / week)} weeks ago`; 31 | } 32 | 33 | return ret; 34 | } 35 | -------------------------------------------------------------------------------- /apps/dev/src/design/card/card.css: -------------------------------------------------------------------------------- 1 | .card { 2 | background-color: var(--card-background-color); 3 | border: var(--card-border-width) solid var(--card-border-color); 4 | transition: var(--card-transition); 5 | } 6 | .card.interactive:hover { 7 | border-color: var(--card-border-highlight-color); 8 | } 9 | 10 | .card.dragging { 11 | background-color: var(--card-background-faded-color); 12 | border-color: var(--card-border-faded-color) !important; 13 | user-select: none; 14 | } 15 | 16 | .card.raised { 17 | position: absolute; 18 | z-index: 1000; 19 | transition: none; 20 | pointer-events: none; 21 | opacity: 0.75; 22 | border: 1px dashed whitesmoke; 23 | cursor: grabbing; 24 | } 25 | 26 | .title { 27 | color: var(--card-title-color); 28 | font-size: var(--card-title-font-size); 29 | font-weight: var(--card-title-font-weight); 30 | margin-bottom: var(--card-title-margin-bottom); 31 | } 32 | 33 | .subtitle { 34 | color: var(--card-subtitle-color); 35 | font-size: var(--card-subtitle-font-size); 36 | font-weight: var(--card-subtitle-font-weight); 37 | } 38 | -------------------------------------------------------------------------------- /packages/utils/src/lib/promise/create-externally-resolvable-promise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the promise object, externalizing the reject() and resolve() 3 | * functions so that they can be called from without the Promise. 4 | * 5 | * @example 6 | * ```ts 7 | * const { promise, resolve } = createExternallyResolvablePromise(); 8 | * 9 | * promise.then(result => { ... }); 10 | * 11 | * resolve([data]); 12 | * ``` 13 | */ 14 | export function createExternallyResolvablePromise(): ExternallyResolvablePromise { 15 | let res: PromiseResolveReject | undefined; 16 | let rej: PromiseResolveReject | undefined; 17 | 18 | return { 19 | promise: new Promise((resolve, reject) => { 20 | res = resolve; 21 | rej = reject; 22 | }), 23 | reject: rej as PromiseResolveReject, 24 | resolve: res as PromiseResolveReject, 25 | }; 26 | } 27 | 28 | export type PromiseResolveReject = (data: any) => void; 29 | 30 | export interface ExternallyResolvablePromise { 31 | promise: Promise; 32 | reject: PromiseResolveReject; 33 | resolve: PromiseResolveReject; 34 | } 35 | -------------------------------------------------------------------------------- /apps/dev/src/features/nav/nav.module.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | list-style: none; 3 | margin: 0; 4 | padding: 0; 5 | 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | .nav-item { 11 | margin-left: var(--nav-item-spacing); 12 | } 13 | 14 | .nav-item { 15 | a { 16 | display: flex; 17 | align-items: center; 18 | color: var(--nav-item-color); 19 | font-size: var(--nav-item-font-size); 20 | font-weight: var(--nav-item-font-weight); 21 | transition: var(--nav-item-color-transition); 22 | 23 | &:hover { 24 | color: var(--nav-item-color-highlight); 25 | 26 | i { 27 | color: var(--nav-item-icon-color-highlight); 28 | } 29 | } 30 | 31 | i { 32 | margin-right: var(--nav-item-icon-spacing); 33 | font-size: var(--nav-item-icon-font-size); 34 | color: var(--nav-item-icon-color); 35 | transition: var(--nav-item-color-transition); 36 | } 37 | } 38 | 39 | &.active a { 40 | color: var(--nav-item-color-highlight); 41 | 42 | i { 43 | color: var(--nav-item-icon-color-highlight); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/dev/src/features/nav/nav.view.ts: -------------------------------------------------------------------------------- 1 | import { html, render } from 'lit'; 2 | import { repeat } from 'lit/directives/repeat.js'; 3 | import navStyles from './nav.module.scss'; 4 | import type { RouterAPI } from '../../shared/router/router.service'; 5 | import { cx } from '@crux/utils'; 6 | import type { NavItem } from './nav.types'; 7 | import { icon } from '../../design/icon/icon'; 8 | 9 | export function navView(root: HTMLElement) { 10 | return function nav(items: NavItem[], actions: RouterAPI): void { 11 | const { link } = actions; 12 | 13 | render(template(items), root); 14 | 15 | function template(items: NavItem[]) { 16 | const listItems = repeat( 17 | items, 18 | (item) => item.route, 19 | (item) => 20 | html`
  • 21 | ${icon(item.icon)} ${item.text} 22 |
  • ` 23 | ); 24 | 25 | return html`
      26 | ${listItems} 27 |
    `; 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /apps/dev/src/shared/router/utils/get-route-from-url.ts: -------------------------------------------------------------------------------- 1 | import type { Route } from '../router.slice'; 2 | 3 | export function getRouteFromUrl(config: Record, fullUrl: string): Route | null { 4 | const { hash, pathname, searchParams } = new URL(fullUrl); 5 | 6 | const pathnameTokens = pathname.split('/'); 7 | 8 | const params = {} as Record; 9 | 10 | const name = Object.entries(config).find(([name, path]) => { 11 | const pathTokens = path.split('/'); 12 | 13 | if (pathTokens.length !== pathnameTokens.length) { 14 | return false; 15 | } 16 | 17 | return pathTokens.every((token, index) => { 18 | if (token.startsWith(':')) { 19 | params[token.replace(':', '')] = pathnameTokens[index]; 20 | 21 | return true; 22 | } 23 | 24 | return token === pathnameTokens[index]; 25 | }); 26 | })?.[0]; 27 | 28 | return name 29 | ? { 30 | hash, 31 | name, 32 | params: Object.keys(params).length ? params : undefined, 33 | search: Object.fromEntries(searchParams.entries()), 34 | } 35 | : null; 36 | } 37 | -------------------------------------------------------------------------------- /packages/crux/src/lib/service.ts: -------------------------------------------------------------------------------- 1 | import type { NodeTypes, Service } from './types'; 2 | 3 | export function service[], Args extends NodeTypes, I = any>( 4 | factory: (...args: Args) => Promise | I, 5 | options: { 6 | deps: T; 7 | } = { 8 | deps: [] as T, 9 | } 10 | ): Service { 11 | const { deps } = options; 12 | 13 | let instance: I | undefined; 14 | let promise: Promise | undefined; 15 | 16 | return { 17 | getAPI: getInstance, 18 | getInstance, 19 | instance, 20 | promise, 21 | }; 22 | 23 | async function getInstance(): Promise { 24 | if (instance) { 25 | return instance; 26 | } 27 | 28 | if (promise) { 29 | return promise; 30 | } 31 | 32 | const depInstances = (await Promise.all(deps.map((dep) => dep.getAPI()))) as Args; 33 | 34 | const ret = factory(...depInstances); 35 | 36 | if (ret instanceof Promise) { 37 | ret.then((i) => { 38 | instance = i; 39 | }); 40 | 41 | return ret; 42 | } 43 | 44 | instance = ret; 45 | 46 | return instance; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Andrew Jessop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/dev/src/styles/normalise.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 15 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | code { 21 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 22 | } 23 | 24 | pre, 25 | code { 26 | white-space: pre-wrap; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | margin: 0; 36 | padding: 0; 37 | } 38 | 39 | a, 40 | a:visited { 41 | color: var(--sl-color-sky-500); 42 | text-decoration: none; 43 | } 44 | 45 | a:hover { 46 | color: var(--sl-color-sky-700); 47 | } 48 | 49 | .sl-toast-stack { 50 | right: 0; 51 | top: 70px; 52 | } 53 | 54 | sl-tab-group { 55 | --track-color: var(--sl-color-neutral-200); 56 | --track-width: 1px; 57 | } 58 | 59 | sl-tab::part(base) { 60 | padding-bottom: var(--sl-spacing-small); 61 | } 62 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/resources/comment/index.ts: -------------------------------------------------------------------------------- 1 | import { createDataAPI, PostComment, PutComment, Comment } from '../../api'; 2 | import { deleteComment, mergeComment, toData } from './transformations'; 3 | 4 | export function createCommentConfig(api: ReturnType) { 5 | return { 6 | query: () => api.comment.getAll().then(toData), 7 | mutations: { 8 | create: { 9 | query: (comment: PostComment) => api.comment.post(comment), 10 | }, 11 | delete: { 12 | query: (id: number) => api.comment.delete(id), 13 | options: { 14 | updateStateOptimistically: (user: Comment) => (data: Comment[] | null) => 15 | deleteComment(data, user), 16 | }, 17 | }, 18 | update: { 19 | query: (comment: PutComment) => api.comment.put(comment), 20 | options: { 21 | updateStateOptimistically: (user: PutComment) => (data: Comment[] | null) => 22 | mergeComment(data, user), 23 | }, 24 | }, 25 | }, 26 | options: { 27 | lazy: true, 28 | keepUnusedDataFor: 60, 29 | pollingInterval: 360, 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/async-queue/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Andrew Jessop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/event-emitter/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Andrew Jessop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/dev/src/shared/router/router.service.ts: -------------------------------------------------------------------------------- 1 | import type { Route, RouterStateAPI } from './router.slice'; 2 | import { getRouteFromUrl } from './utils/get-route-from-url'; 3 | import { getUrlFromRoute } from './utils/get-url-from-route'; 4 | 5 | export type RouterAPI = ReturnType; 6 | 7 | export function router>(config: T, api: RouterStateAPI) { 8 | addEventListener('popstate', () => { 9 | onPopstate(); 10 | }); 11 | 12 | return { 13 | link, 14 | navigate, 15 | }; 16 | 17 | function link(route: Route) { 18 | return function withEvent(event: Event) { 19 | event.preventDefault(); 20 | 21 | navigate(route); 22 | }; 23 | } 24 | 25 | function navigate({ hash, name, search, params }: Route) { 26 | const url = getUrlFromRoute(config, name, params, search, hash); 27 | 28 | window.history.pushState({}, '', url); 29 | 30 | api.navigateSuccess({ 31 | hash, 32 | name, 33 | params, 34 | search, 35 | }); 36 | } 37 | 38 | function onPopstate() { 39 | const route = getRouteFromUrl(config, window.location.href); 40 | 41 | api.navigateSuccess(route); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/redux-types/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | error?: boolean; 3 | meta?: Meta; 4 | payload?: Payload; 5 | type: string; 6 | } 7 | 8 | export type ActionCreator = (...args: any) => Action; 9 | 10 | export type Dispatch = { 11 | (action: T): T; 12 | }; 13 | 14 | export type GetState = () => S; 15 | 16 | export type Thunk = (dispatch: Dispatch, getState: GetState) => void; 17 | 18 | export type DispatchActionOrThunk = (action: T | Thunk) => T; 19 | 20 | export type Reducer = (state: S | undefined, action: A) => S; 21 | 22 | export interface MiddlewareAPI { 23 | dispatch: D; 24 | getState(): S; 25 | } 26 | 27 | export type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => void; 28 | 29 | export interface Store { 30 | dispatch: Dispatch; 31 | getState: GetState; 32 | } 33 | 34 | export interface StoreWithThunkableDispatch { 35 | dispatch: DispatchActionOrThunk; 36 | getState: GetState; 37 | } 38 | -------------------------------------------------------------------------------- /apps/dev/src/features/toaster/toaster.service.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomId } from '@crux/string-utils'; 2 | import { sleep } from '@crux/utils'; 3 | import type { Alert, ToasterStateAPI } from './toaster.slice'; 4 | 5 | export type BaseAlert = Omit; 6 | 7 | export type ToasterService = ReturnType; 8 | 9 | const DEFAULT_ANIMATION_DURATION = 200; 10 | 11 | export function toaster(api: ToasterStateAPI) { 12 | return { 13 | close, 14 | toast, 15 | }; 16 | 17 | async function close(id: string) { 18 | const { alerts } = api.getState(); 19 | 20 | const alert = alerts.find((a) => a.id === id); 21 | 22 | api.setRemoving(id); 23 | 24 | await sleep(alert.animationDuration); 25 | 26 | api.remove(id); 27 | } 28 | 29 | async function toast(alert: BaseAlert) { 30 | const id = generateRandomId(); 31 | const alertWithId = { ...alert, id }; 32 | 33 | api.add(alertWithId); 34 | 35 | if (alert.duration !== undefined) { 36 | await sleep(alert.duration); 37 | 38 | api.setRemoving(id); 39 | 40 | await sleep(alert.animationDuration ?? DEFAULT_ANIMATION_DURATION); 41 | 42 | api.remove(id); 43 | } 44 | 45 | return id; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/resources/user/index.ts: -------------------------------------------------------------------------------- 1 | import { createDataAPI, PostUser, PutUser, User } from '../../api'; 2 | import { deleteUser, mergeUser, toData } from './transformations'; 3 | 4 | export function createUserConfig(api: ReturnType) { 5 | return { 6 | query: () => 7 | api.user 8 | .getAll() 9 | .then(toData) 10 | .catch((e) => { 11 | throw e as { message: string }; 12 | }), 13 | mutations: { 14 | create: { 15 | query: (user: PostUser) => api.user.post(user), 16 | }, 17 | delete: { 18 | query: (id: number) => api.user.delete(id), 19 | optimistic: (id: number) => (data: User[] | null) => deleteUser(data, id), 20 | }, 21 | update: { 22 | query: (user: PutUser) => (data: User[] | null) => 23 | api.user.put(user).then((res) => mergeUser(data, res.data)), 24 | optimistic: (user: PutUser) => (data: User[] | null) => mergeUser(data, user), 25 | options: { 26 | refetchOnSuccess: false, 27 | }, 28 | }, 29 | }, 30 | options: { 31 | lazy: true, 32 | keepUnusedDataFor: 60, 33 | pollingInterval: 360, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/resources/user/transformations.ts: -------------------------------------------------------------------------------- 1 | import { serialize } from '../../../helpers/serializer'; 2 | import { ErrorResponse, PutUser, Response, User } from '../../api'; 3 | 4 | export function toError(response: ErrorResponse): string { 5 | throw new Error(serialize(response.error)); 6 | } 7 | 8 | export function toData(response: Response) { 9 | return response.data; 10 | } 11 | 12 | export function toNull() { 13 | return null; 14 | } 15 | 16 | export function toUndefined() { 17 | return undefined; 18 | } 19 | 20 | export function postResponseToData(data: User[] | null, response: Response) { 21 | return [...(data || []), response.data]; 22 | } 23 | 24 | export function deleteUser(data: User[] | null, id: number) { 25 | if (data === null) { 26 | return data; 27 | } 28 | 29 | return data.filter((u) => id !== u.id); 30 | } 31 | 32 | export function mergeUser(data: User[] | null, user: PutUser) { 33 | if (data === null) { 34 | return data; 35 | } 36 | 37 | const ndx = data?.findIndex((u) => u.id === user.id); 38 | 39 | if (ndx === -1) { 40 | return data; 41 | } 42 | 43 | data[ndx] = { 44 | ...data[ndx], 45 | ...user, 46 | }; 47 | 48 | return data; 49 | } 50 | -------------------------------------------------------------------------------- /packages/dynamic-store/src/lib/dynamic-store.ts: -------------------------------------------------------------------------------- 1 | import { legacy_createStore, applyMiddleware, compose } from 'redux'; 2 | import { middlewareRegistry, reducerRegistry } from '@crux/redux-registry'; 3 | import type { Reducer, Store } from '@crux/redux-types'; 4 | 5 | export type DynamicStore = ReturnType; 6 | 7 | export function dynamicStore({ isDev }: { isDev?: boolean } = { isDev: true }) { 8 | const mRegistry = middlewareRegistry(); 9 | const rRegistry = reducerRegistry(); 10 | const middlewareEnhancer = applyMiddleware(...[mRegistry.middleware]); 11 | 12 | const composeEnhancers = 13 | (isDev && 14 | typeof window !== 'undefined' && 15 | (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || 16 | compose; 17 | 18 | const store = legacy_createStore( 19 | rRegistry.reducer, 20 | {}, 21 | composeEnhancers(middlewareEnhancer) 22 | ) as Store; 23 | 24 | const addMiddleware = mRegistry.add; 25 | const addReducer = (id: string, reducer: Reducer) => { 26 | const remove = rRegistry.add(id, reducer); 27 | 28 | store.dispatch({ type: '__cruxRegistry/reducer/add', payload: { id } }); 29 | 30 | return remove; 31 | }; 32 | 33 | return { addMiddleware, addReducer, ...store }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/query/src/lib/test/resources/comment/transformations.ts: -------------------------------------------------------------------------------- 1 | import { serialize } from '../../../helpers/serializer'; 2 | import { ErrorResponse, Comment, PutComment, Response } from '../../api'; 3 | 4 | export function toError(response: ErrorResponse): string { 5 | throw new Error(serialize(response.error)); 6 | } 7 | 8 | export function toData(response: Response) { 9 | return response.data; 10 | } 11 | 12 | export function toNull() { 13 | return null; 14 | } 15 | 16 | export function toUndefined() { 17 | return undefined; 18 | } 19 | 20 | export function postResponseToData(data: Comment[] | null, response: Response) { 21 | return [...(data || []), response.data]; 22 | } 23 | 24 | export function deleteComment(data: Comment[] | null, comment: Comment) { 25 | if (data === null) { 26 | return data; 27 | } 28 | 29 | return data.filter((c) => comment.id !== c.id); 30 | } 31 | 32 | export function mergeComment(data: Comment[] | null, comment: PutComment) { 33 | if (data === null) { 34 | return data; 35 | } 36 | 37 | const ndx = data?.findIndex((c) => c.id === comment.id); 38 | 39 | if (ndx === -1) { 40 | return data; 41 | } 42 | 43 | data[ndx] = { 44 | ...data[ndx], 45 | ...comment, 46 | }; 47 | 48 | return data; 49 | } 50 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/task-overlay/task-overlay.css: -------------------------------------------------------------------------------- 1 | .before { 2 | height: calc(50% + 0.625rem); 3 | top: -0.625rem; 4 | } 5 | 6 | .after { 7 | height: calc(50% + 0.375rem); 8 | top: 50%; 9 | } 10 | 11 | .after, 12 | .before { 13 | width: calc(100% + 0.125rem); 14 | position: absolute; 15 | left: -0.125rem; 16 | } 17 | .after:hover, 18 | .before:hover { 19 | cursor: copy; 20 | } 21 | 22 | .after:hover { 23 | border-bottom: 2px solid var(--todos-drag-separator-color); 24 | } 25 | .before:hover { 26 | border-top: 2px solid var(--todos-drag-separator-color); 27 | } 28 | 29 | .handle { 30 | opacity: 0; 31 | border-radius: 4px; 32 | width: 0.5rem; 33 | height: 0.5rem; 34 | background-color: var(--todos-drag-separator-color); 35 | } 36 | 37 | .handle i { 38 | color: white; 39 | } 40 | 41 | .before:hover .handle, 42 | .after:hover .handle { 43 | opacity: 1; 44 | position: absolute; 45 | } 46 | 47 | .before:hover .left, 48 | .after:hover .left { 49 | left: 0; 50 | transform: translateX(-0.4375rem); 51 | } 52 | 53 | .before:hover .right, 54 | .after:hover .right { 55 | right: 0; 56 | transform: translateX(0.5rem); 57 | } 58 | 59 | .before:hover .handle { 60 | top: -0.3125rem; 61 | } 62 | 63 | .after:hover .handle { 64 | bottom: -0.3125rem; 65 | } 66 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/task/task.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, unsafeCSS } from 'lit'; 2 | import { property } from 'lit/decorators.js'; 3 | import styles from './task.css?inline'; 4 | import cardStyles from '../../../../design/card/card.css?inline'; 5 | import { cx } from '@crux/utils'; 6 | 7 | export class TodoTask extends LitElement { 8 | static styles = [unsafeCSS(styles), unsafeCSS(cardStyles)]; 9 | 10 | @property({ type: String }) 11 | 'created-at': string; 12 | 13 | @property({ type: String }) 14 | 'dragging-task-id': string; 15 | 16 | @property({ type: String }) 17 | status: string; 18 | 19 | @property({ type: String }) 20 | 'task-id': string; 21 | 22 | @property({ type: String }) 23 | text: string; 24 | 25 | render() { 26 | return html`
    34 |
    ${this.text}
    35 |
    ${this['created-at']}
    36 | 37 |
    `; 38 | } 39 | } 40 | 41 | customElements.define('todo-task', TodoTask); 42 | 43 | declare global { 44 | interface HTMLElementTagNameMap { 45 | 'todo-task': TodoTask; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/dev/src/layout/layout.view.ts: -------------------------------------------------------------------------------- 1 | import { html, render } from 'lit'; 2 | import type { TemplateResult } from 'lit'; 3 | import styles from './layout.module.scss'; 4 | import logo from '../assets/logo-transparent-small.png'; 5 | import { verticalDivider } from '../design/divider/vertical-divider'; 6 | import type { LayoutData } from './layout.index'; 7 | 8 | export function createLayoutView(root: HTMLElement) { 9 | return function renderLayout(data: LayoutData) { 10 | const { todos } = data; 11 | 12 | render(template(), root); 13 | 14 | function template() { 15 | let outlet: TemplateResult; 16 | 17 | if (todos) { 18 | outlet = html`
    `; 19 | } 20 | 21 | return html` 22 |
    23 |
    24 | Crux Code 25 |
    26 | ${verticalDivider()} 27 |
    28 |
    29 |
    30 |
    31 |
    32 | ${outlet} 33 |
    34 | `; 35 | } 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/utils/src/lib/any.spec.ts: -------------------------------------------------------------------------------- 1 | import { merge } from './any'; 2 | 3 | const dest = { 4 | a: true, 5 | b: { 6 | c: [1, 2], 7 | d: { e: true }, 8 | f: true, 9 | }, 10 | g: [1, 2], 11 | h: { 12 | i: true, 13 | }, 14 | }; 15 | 16 | test('updates shallow primitive', () => { 17 | expect( 18 | merge(dest, { 19 | a: false, 20 | }) 21 | ).toEqual({ ...dest, a: false }); 22 | }); 23 | 24 | test('updates nested primitive', () => { 25 | expect( 26 | merge(dest, { 27 | b: { f: false }, 28 | }) 29 | ).toEqual({ ...dest, b: { ...dest.b, f: false } }); 30 | }); 31 | 32 | test('updates shallow array immutably', () => { 33 | const updated = merge(dest, { 34 | g: [1, 2], 35 | }); 36 | 37 | expect(updated).toEqual(dest); 38 | expect(updated.g === dest.g).toEqual(false); 39 | }); 40 | 41 | test('updates shallow object immutably', () => { 42 | const updated = merge(dest, { 43 | h: { 44 | i: true, 45 | }, 46 | }); 47 | 48 | expect(updated).toEqual(dest); 49 | expect(updated.h === dest.h).toEqual(false); 50 | }); 51 | 52 | test('updates nested array immutably', () => { 53 | const updated = merge(dest, { 54 | b: { c: [1, 2] }, 55 | }); 56 | 57 | expect(updated).toEqual(dest); 58 | expect(updated.b === dest.b).toEqual(false); 59 | expect(updated.b.c === dest.b.c).toEqual(false); 60 | }); 61 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/expanding-dropzone/expanding-dropzone.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css, unsafeCSS } from 'lit'; 2 | import { property } from 'lit/decorators.js'; 3 | import styles from './expanding-dropzone.css?inline'; 4 | import overlayStyles from '../task-overlay/task-overlay.css?inline'; 5 | import type { Status } from '../../domain/todos.types'; 6 | 7 | export class ExpandingDropzone extends LitElement { 8 | static styles = [unsafeCSS(styles)]; 9 | 10 | @property({ type: String }) 11 | status: Status; 12 | 13 | private _onEnter(status: Status) { 14 | this.dispatchEvent( 15 | new CustomEvent('status-overlay-enter', { 16 | detail: { status }, 17 | }) 18 | ); 19 | } 20 | 21 | private _onExit(status: Status) { 22 | this.dispatchEvent( 23 | new CustomEvent('status-overlay-exit', { 24 | detail: { status }, 25 | }) 26 | ); 27 | } 28 | 29 | render() { 30 | return html`
    this._onEnter(this.status)} 32 | @mouseleave=${() => this._onExit(this.status)} 33 | class="dropzone" 34 | > 35 |
    36 |
    37 |
    `; 38 | } 39 | } 40 | 41 | customElements.define('expanding-dropzone', ExpandingDropzone); 42 | 43 | declare global { 44 | interface HTMLElementTagNameMap { 45 | 'expanding-dropzone': ExpandingDropzone; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "name": "vscode-jest-tests", 10 | "request": "launch", 11 | "args": [ 12 | "--runInBand", 13 | "--watchAll=false" 14 | ], 15 | "cwd": "${workspaceFolder}", 16 | "console": "integratedTerminal", 17 | "internalConsoleOptions": "neverOpen", 18 | "disableOptimisticBPs": true, 19 | "program": "${workspaceFolder}/node_modules/.bin/jest", 20 | "windows": { 21 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 22 | }, 23 | "skipFiles": [ 24 | "${workspaceFolder}/node_modules/**/*.js", 25 | "/**" 26 | ] 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Jest debug current file", 32 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 33 | "args": ["--verbose", "-i", "--no-cache", "--testPathPattern", "${fileBasename}"], 34 | "console": "integratedTerminal", 35 | "internalConsoleOptions": "neverOpen", 36 | "skipFiles": [ 37 | "${workspaceFolder}/node_modules/**/*.js", 38 | "/**" 39 | ] 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.http.ts: -------------------------------------------------------------------------------- 1 | import type { Env } from '../../../shared/env/env.service'; 2 | import { Variable } from '../../../shared/env/env.service'; 3 | import type { PostTask, PutTask, Task } from './todos.types'; 4 | 5 | export interface Response { 6 | data: Data; 7 | } 8 | 9 | export type TodosHttpApi = ReturnType; 10 | 11 | export function createTodosHttpApi(env: Env) { 12 | return { 13 | /** 14 | * Create a task. 15 | */ 16 | createTask: (task: PostTask): Promise> => 17 | fetch(`${env.get(Variable.VITE_API_URL)}/tasks`, { 18 | body: JSON.stringify({ task }), 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | method: 'POST', 23 | }).then((res) => res.json()), 24 | 25 | /** 26 | * Fetch all tasks as an array. 27 | */ 28 | getAll: (): Promise> => 29 | fetch(`${env.get(Variable.VITE_API_URL)}/tasks`).then((res) => res.json()), 30 | 31 | /** 32 | * Update a task. 33 | */ 34 | updateTask: (task: PutTask, statusNdx?: number): Promise> => 35 | fetch(`${env.get(Variable.VITE_API_URL)}/tasks/${task.id}`, { 36 | body: JSON.stringify({ task, statusNdx }), 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | method: 'PUT', 41 | }).then((res) => res.json()), 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /apps/dev/src/shared/data/users/users-data.service.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '@crux/query'; 2 | import type { PostUser, PutUser, User, UsersAPI } from '../../http/users/users-http.service'; 3 | import { deleteUser, mergeUser, toData } from './transformations'; 4 | 5 | export type UsersData = ReturnType; 6 | 7 | export function usersData(createResource: API['createResource'], users: UsersAPI) { 8 | const resource = createResource('users', { 9 | query: async () => 10 | users 11 | .getAll() 12 | .then(toData) 13 | .catch((e) => { 14 | throw e as { message: string }; 15 | }), 16 | mutations: { 17 | create: { 18 | query: (user: PostUser) => users.post(user), 19 | }, 20 | delete: { 21 | query: (id: number) => users.delete(id), 22 | optimistic: (id: number) => (data: User[] | null) => deleteUser(data, id), 23 | }, 24 | update: { 25 | query: (user: PutUser) => (data: User[] | null) => 26 | users.put(user).then((res) => (res.data ? mergeUser(data, res.data) : data)), 27 | optimistic: (user: PutUser) => (data: User[] | null) => mergeUser(data, user), 28 | options: { 29 | refetchOnSuccess: false, 30 | }, 31 | }, 32 | }, 33 | options: { 34 | lazy: true, 35 | keepUnusedDataFor: 60, 36 | pollingInterval: 360, 37 | }, 38 | }); 39 | 40 | return resource.subscribe; 41 | } 42 | -------------------------------------------------------------------------------- /apps/dev/src/features/toaster/toaster.slice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@crux/slice'; 2 | import { merge } from '@crux/utils'; 3 | 4 | export interface Alert { 5 | animationDuration?: number; 6 | duration: number; 7 | html?: string; 8 | id: string; 9 | removing?: boolean; 10 | text?: string; 11 | variant: 'success' | 'danger' | 'warning' | 'info'; 12 | } 13 | 14 | export interface ToasterState { 15 | alerts: Alert[]; 16 | } 17 | 18 | const initialState: ToasterState = { 19 | alerts: [], 20 | }; 21 | 22 | export type ToasterStateAPI = ReturnType['api']; 23 | 24 | export function createToasterSlice(name: string) { 25 | return createSlice(name, initialState, { 26 | add: (state: ToasterState, alert: Alert) => { 27 | const baseAlert = { 28 | animationDuration: 200, 29 | }; 30 | 31 | return merge(state, { 32 | alerts: [...state.alerts, merge(baseAlert, alert)], 33 | }); 34 | }, 35 | 36 | remove: (state: ToasterState, id: string) => 37 | merge(state, { 38 | alerts: state.alerts.filter((alert) => alert.id !== id), 39 | }), 40 | 41 | setRemoving: (state: ToasterState, id: string) => 42 | merge(state, { 43 | alerts: state.alerts.map((alert) => 44 | alert.id !== id 45 | ? alert 46 | : { 47 | ...alert, 48 | removing: true, 49 | } 50 | ), 51 | }), 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "downlevelIteration": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "useDefineForClassFields": false, 12 | "noPropertyAccessFromIndexSignature": false, 13 | "importHelpers": true, 14 | "target": "es2018", 15 | "module": "esnext", 16 | "lib": ["es2019", "dom"], 17 | "skipLibCheck": true, 18 | "skipDefaultLibCheck": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "@crux/crux": ["packages/crux/src/index.ts"], 22 | "@crux/async-queue": ["packages/async-queue/src/index.ts"], 23 | "@crux/core": ["packages/core/src/index.ts"], 24 | "@crux/dynamic-store": ["packages/dynamic-store/src/index.ts"], 25 | "@crux/event-emitter": ["packages/event-emitter/src/index.ts"], 26 | "@crux/query": ["packages/query/src/index.ts"], 27 | "@crux/redux-registry": ["packages/redux-registry/src/index.ts"], 28 | "@crux/slice": ["packages/slice/src/index.ts"], 29 | "@crux/redux-types": ["packages/redux-types/src/index.ts"], 30 | "@crux/string-utils": ["packages/string-utils/src/index.ts"], 31 | "@crux/sync-queue": ["packages/sync-queue/src/index.ts"], 32 | "@crux/utils": ["packages/utils/src/index.ts"] 33 | }, 34 | "types": ["node"] 35 | }, 36 | "exclude": ["node_modules", "tmp", "**/jest.config.ts"] 37 | } 38 | -------------------------------------------------------------------------------- /apps/dev/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/crux/src/lib/subscription.ts: -------------------------------------------------------------------------------- 1 | import type { Selector, SelectorOrServiceTypes, Service } from './types'; 2 | 3 | export type Subscription = ReturnType; 4 | 5 | export function subscription< 6 | T extends [] | (Selector | Service)[], 7 | Args extends SelectorOrServiceTypes 8 | >( 9 | factory: (...args: Args) => void, 10 | options?: { 11 | deps: T; 12 | shouldBeEnabled?: (state: any) => boolean; 13 | } 14 | ) { 15 | const cachedDeps = [] as any; 16 | const { deps = [], shouldBeEnabled = () => true } = options ?? {}; 17 | 18 | return { 19 | runSubscription, 20 | updateDeps, 21 | }; 22 | 23 | async function runSubscription(state: any) { 24 | if (!shouldBeEnabled(state)) { 25 | return; 26 | } 27 | 28 | if (!(await updateDeps(state))) { 29 | return; 30 | } 31 | 32 | return factory(...cachedDeps); 33 | } 34 | 35 | async function updateDeps(state: any) { 36 | let isUpdated = false; 37 | 38 | for (const [i, dep] of deps.entries()) { 39 | if ((dep as Service).getAPI) { 40 | const api = await (dep as Service).getAPI(); 41 | 42 | if (cachedDeps[i] === api) { 43 | continue; 44 | } 45 | 46 | cachedDeps[i] = api; 47 | 48 | isUpdated = true; 49 | } else { 50 | const value = await (dep as Selector)(state); 51 | 52 | if (cachedDeps[i] === value) { 53 | continue; 54 | } 55 | 56 | cachedDeps[i] = value; 57 | 58 | isUpdated = true; 59 | } 60 | } 61 | 62 | return isUpdated; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/task-overlay/task-overlay.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css, unsafeCSS } from 'lit'; 2 | import { property } from 'lit/decorators.js'; 3 | import styles from './task-overlay.css?inline'; 4 | import type { Status } from '../../domain/todos.types'; 5 | import { icon } from '../../../../design/icon/icon'; 6 | 7 | export class TaskOverlay extends LitElement { 8 | static styles = [unsafeCSS(styles)]; 9 | 10 | @property({ type: Number }) 11 | index: number; 12 | 13 | @property({ type: String }) 14 | status: Status; 15 | 16 | @property({ type: String }) 17 | placement: 'before' | 'after'; 18 | 19 | private _onEnter(index: number, status: Status) { 20 | this.dispatchEvent( 21 | new CustomEvent('overlay-enter', { 22 | detail: { index, status }, 23 | }) 24 | ); 25 | } 26 | 27 | private _onExit(index: number, status: Status) { 28 | this.dispatchEvent( 29 | new CustomEvent('overlay-exit', { 30 | detail: { index, status }, 31 | }) 32 | ); 33 | } 34 | 35 | render() { 36 | const ndx = this.placement === 'before' ? this.index - 0.5 : this.index + 0.5; 37 | 38 | return html`
    this._onEnter(ndx, this.status)} 40 | @mouseleave=${() => this._onExit(ndx, this.status)} 41 | class=${this.placement} 42 | > 43 |
    44 |
    45 |
    `; 46 | } 47 | } 48 | 49 | customElements.define('task-overlay', TaskOverlay); 50 | 51 | declare global { 52 | interface HTMLElementTagNameMap { 53 | 'task-overlay': TaskOverlay; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/query/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/query/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/query", 10 | "main": "packages/query/src/index.ts", 11 | "tsConfig": "packages/query/tsconfig.lib.json", 12 | "assets": ["packages/query/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/query", 20 | "tsConfig": "packages/query/tsconfig.lib.json", 21 | "project": "packages/query/package.json", 22 | "entryFile": "packages/query/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": ["rollup.lib.prod.js", "packages/query/rollup.lib.js"] 25 | } 26 | }, 27 | "publish": { 28 | "executor": "@nrwl/workspace:run-commands", 29 | "options": { 30 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts query" 31 | } 32 | }, 33 | "lint": { 34 | "executor": "@nrwl/linter:eslint", 35 | "outputs": ["{options.outputFile}"], 36 | "options": { 37 | "lintFilePatterns": ["packages/query/**/*.ts"] 38 | } 39 | }, 40 | "test": { 41 | "executor": "@nrwl/jest:jest", 42 | "outputs": ["coverage/packages/query"], 43 | "options": { 44 | "jestConfig": "packages/query/jest.config.ts", 45 | "passWithNoTests": true 46 | } 47 | } 48 | }, 49 | "tags": [] 50 | } 51 | -------------------------------------------------------------------------------- /packages/slice/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/slice/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/slice", 10 | "main": "packages/slice/src/index.ts", 11 | "tsConfig": "packages/slice/tsconfig.lib.json", 12 | "assets": ["packages/slice/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/slice", 20 | "tsConfig": "packages/slice/tsconfig.lib.json", 21 | "project": "packages/slice/package.json", 22 | "entryFile": "packages/slice/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": ["rollup.lib.prod.js", "packages/slice/rollup.lib.js"] 25 | } 26 | }, 27 | "publish": { 28 | "executor": "@nrwl/workspace:run-commands", 29 | "options": { 30 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts slice" 31 | } 32 | }, 33 | "lint": { 34 | "executor": "@nrwl/linter:eslint", 35 | "outputs": ["{options.outputFile}"], 36 | "options": { 37 | "lintFilePatterns": ["packages/slice/**/*.ts"] 38 | } 39 | }, 40 | "test": { 41 | "executor": "@nrwl/jest:jest", 42 | "outputs": ["coverage/packages/slice"], 43 | "options": { 44 | "jestConfig": "packages/slice/jest.config.ts", 45 | "passWithNoTests": true 46 | } 47 | } 48 | }, 49 | "tags": [] 50 | } 51 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.selectors.ts: -------------------------------------------------------------------------------- 1 | import type { TodosService } from './todos.service'; 2 | import type { Task, TodosState } from './todos.types'; 3 | 4 | /** 5 | * Select the to-do tasks. 6 | */ 7 | export function tasksTodo(tasks: Task[]): Task[] { 8 | return tasks.filter((task) => task.status === 'to-do'); 9 | } 10 | 11 | /** 12 | * Select the in-progress tasks. 13 | */ 14 | export function tasksInProgress(tasks: Task[]): Task[] { 15 | return tasks.filter((task) => task.status === 'in-progress'); 16 | } 17 | 18 | /** 19 | * Select the completed tasks. 20 | */ 21 | export function tasksCompleted(tasks: Task[]): Task[] { 22 | return tasks.filter((task) => task.status === 'completed'); 23 | } 24 | 25 | /** 26 | * Select the tasks as an object with status keys. 27 | */ 28 | export function taskColumns(toDo: Task[], inProgress: Task[], completed: Task[]) { 29 | return [ 30 | { icon: 'bell', status: 'to-do', tasks: toDo, title: 'To Do' }, 31 | { 32 | icon: 'shipping-fast', 33 | status: 'in-progress', 34 | tasks: inProgress, 35 | title: 'In Progress', 36 | }, 37 | { icon: 'check-circle', status: 'completed', tasks: completed, title: 'Completed' }, 38 | ]; 39 | } 40 | 41 | /** 42 | * Select all data required for the todo view. 43 | */ 44 | export function todosData(todos: TodosState, tasks: ReturnType) { 45 | return { 46 | draggingTaskId: todos.draggingTaskId, 47 | tasks, 48 | }; 49 | } 50 | 51 | export type TodosData = ReturnType; 52 | 53 | export function todosActions(api: TodosService) { 54 | return api; 55 | } 56 | 57 | export type TodosActions = ReturnType; 58 | -------------------------------------------------------------------------------- /packages/crux/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "packages/crux/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/packages/crux", 11 | "main": "packages/crux/src/index.ts", 12 | "tsConfig": "packages/crux/tsconfig.lib.json", 13 | "assets": ["packages/crux/*.md"] 14 | } 15 | }, 16 | "prod": { 17 | "executor": "@nrwl/web:rollup", 18 | "outputs": ["{options.outputPath}"], 19 | "options": { 20 | "outputPath": "dist/packages/crux", 21 | "tsConfig": "packages/crux/tsconfig.lib.json", 22 | "project": "packages/crux/package.json", 23 | "entryFile": "packages/crux/src/index.ts", 24 | "format": ["esm", "cjs"], 25 | "rollupConfig": ["rollup.lib.prod.js", "packages/crux/rollup.lib.js"] 26 | } 27 | }, 28 | "publish": { 29 | "executor": "@nrwl/workspace:run-commands", 30 | "options": { 31 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts crux" 32 | } 33 | }, 34 | "lint": { 35 | "executor": "@nrwl/linter:eslint", 36 | "outputs": ["{options.outputFile}"], 37 | "options": { 38 | "lintFilePatterns": ["packages/crux/**/*.ts"] 39 | } 40 | }, 41 | "test": { 42 | "executor": "@nrwl/jest:jest", 43 | "outputs": ["coverage/packages/crux"], 44 | "options": { 45 | "jestConfig": "packages/crux/jest.config.ts", 46 | "passWithNoTests": true 47 | } 48 | } 49 | }, 50 | "tags": [] 51 | } 52 | -------------------------------------------------------------------------------- /apps/dev/src/main.ts: -------------------------------------------------------------------------------- 1 | import './styles/variables.scss'; 2 | import './styles/normalise.css'; 3 | import './styles/main.scss'; 4 | import 'animate.css'; 5 | import { crux } from '@crux/crux'; 6 | import { toasterSlice, toasterView } from './features/toaster/toaster.index'; 7 | import { layoutSlice, layoutView } from './layout/layout.index'; 8 | import { routerSlice } from './shared/router/router.index'; 9 | import { darkModeSlice, darkModeView } from './features/dark-mode/dark-mode.index'; 10 | import { navView } from './features/nav/nav.index'; 11 | import { todosFetchInitiator, todosSlice, todosView } from './features/todos/todos.index'; 12 | import { dataSlice } from './shared/data/data.index'; 13 | 14 | main(); 15 | 16 | async function main() { 17 | const root = document.getElementById('root'); 18 | 19 | if (!root) { 20 | throw new Error('#root element does not exist'); 21 | } 22 | 23 | // If we're in development, start the mock server. This starts a ServiceWorker 24 | // which intercepts fetch requests and returns mocks according to the contents 25 | // of ./shared/mocks/handlers. See https://mswjs.io/ for more details. 26 | if (import.meta.env.DEV) { 27 | const { createServer } = await import('./shared/mock/server'); 28 | 29 | (await createServer(import.meta.env.VITE_API_URL)).start({ 30 | onUnhandledRequest: 'bypass', 31 | waitUntilReady: true, 32 | }); 33 | } 34 | 35 | const slices = [dataSlice, layoutSlice, routerSlice, toasterSlice, darkModeSlice, todosSlice]; 36 | 37 | const subscriptions = [todosFetchInitiator]; 38 | 39 | const views = [layoutView, navView, toasterView, darkModeView, todosView]; 40 | 41 | const app = crux({ 42 | root, 43 | slices, 44 | subscriptions, 45 | views, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /packages/async-queue/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/async-queue/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/async-queue", 10 | "main": "packages/async-queue/src/index.ts", 11 | "tsConfig": "packages/async-queue/tsconfig.lib.json", 12 | "assets": ["packages/async-queue/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/async-queue", 20 | "tsConfig": "packages/async-queue/tsconfig.lib.json", 21 | "project": "packages/async-queue/package.json", 22 | "entryFile": "packages/async-queue/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": [ 25 | "rollup.lib.prod.js", 26 | "packages/async-queue/rollup.lib.js" 27 | ] 28 | } 29 | }, 30 | "publish": { 31 | "executor": "@nrwl/workspace:run-commands", 32 | "options": { 33 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts async-queue" 34 | } 35 | }, 36 | "lint": { 37 | "executor": "@nrwl/linter:eslint", 38 | "outputs": ["{options.outputFile}"], 39 | "options": { 40 | "lintFilePatterns": ["packages/async-queue/**/*.ts"] 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/packages/async-queue"], 46 | "options": { 47 | "jestConfig": "packages/async-queue/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/redux-types/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/redux-types/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/redux-types", 10 | "main": "packages/redux-types/src/index.ts", 11 | "tsConfig": "packages/redux-types/tsconfig.lib.json", 12 | "assets": ["packages/redux-types/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/redux-types", 20 | "tsConfig": "packages/redux-types/tsconfig.lib.json", 21 | "project": "packages/redux-types/package.json", 22 | "entryFile": "packages/redux-types/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": [ 25 | "rollup.lib.prod.js", 26 | "packages/redux-types/rollup.lib.js" 27 | ] 28 | } 29 | }, 30 | "publish": { 31 | "executor": "@nrwl/workspace:run-commands", 32 | "options": { 33 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts redux-types" 34 | } 35 | }, 36 | "lint": { 37 | "executor": "@nrwl/linter:eslint", 38 | "outputs": ["{options.outputFile}"], 39 | "options": { 40 | "lintFilePatterns": ["packages/redux-types/**/*.ts"] 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/packages/redux-types"], 46 | "options": { 47 | "jestConfig": "packages/redux-types/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/string-utils/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/string-utils/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/string-utils", 10 | "main": "packages/string-utils/src/index.ts", 11 | "tsConfig": "packages/string-utils/tsconfig.lib.json", 12 | "assets": ["packages/string-utils/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/string-utils", 20 | "tsConfig": "packages/string-utils/tsconfig.lib.json", 21 | "project": "packages/string-utils/package.json", 22 | "entryFile": "packages/string-utils/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": [ 25 | "rollup.lib.prod.js", 26 | "packages/string-utils/rollup.lib.js" 27 | ] 28 | } 29 | }, 30 | "publish": { 31 | "executor": "@nrwl/workspace:run-commands", 32 | "options": { 33 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts string-utils" 34 | } 35 | }, 36 | "lint": { 37 | "executor": "@nrwl/linter:eslint", 38 | "outputs": ["{options.outputFile}"], 39 | "options": { 40 | "lintFilePatterns": ["packages/string-utils/**/*.ts"] 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/packages/string-utils"], 46 | "options": { 47 | "jestConfig": "packages/string-utils/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/utils/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/utils/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/utils", 10 | "main": "packages/utils/src/index.ts", 11 | "tsConfig": "packages/utils/tsconfig.lib.json", 12 | "assets": ["packages/utils/*.md"] 13 | }, 14 | "configurations": { 15 | "production": { 16 | "tsConfig": "libs/mylib/tsconfig-prod.lib.json" 17 | } 18 | } 19 | }, 20 | "prod": { 21 | "executor": "@nrwl/web:rollup", 22 | "outputs": ["{options.outputPath}"], 23 | "options": { 24 | "outputPath": "dist/packages/utils", 25 | "tsConfig": "packages/utils/tsconfig.lib.json", 26 | "project": "packages/utils/package.json", 27 | "entryFile": "packages/utils/src/index.ts", 28 | "format": ["esm", "cjs"], 29 | "rollupConfig": ["rollup.lib.prod.js", "packages/utils/rollup.lib.js"] 30 | } 31 | }, 32 | "publish": { 33 | "executor": "@nrwl/workspace:run-commands", 34 | "options": { 35 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts utils" 36 | } 37 | }, 38 | "lint": { 39 | "executor": "@nrwl/linter:eslint", 40 | "outputs": ["{options.outputFile}"], 41 | "options": { 42 | "lintFilePatterns": ["packages/utils/**/*.ts"] 43 | } 44 | }, 45 | "test": { 46 | "executor": "@nrwl/jest:jest", 47 | "outputs": ["coverage/packages/utils"], 48 | "options": { 49 | "jestConfig": "packages/utils/jest.config.ts", 50 | "passWithNoTests": true 51 | } 52 | } 53 | }, 54 | "tags": [] 55 | } 56 | -------------------------------------------------------------------------------- /apps/dev/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | import postcssLit from 'rollup-plugin-postcss-lit'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [postcssLit()], 8 | resolve: { 9 | alias: [ 10 | { 11 | find: '@crux/async-queue', 12 | replacement: path.resolve(__dirname, '../../packages/async-queue/src/index.ts'), 13 | }, 14 | { 15 | find: '@crux/dynamic-store', 16 | replacement: path.resolve(__dirname, '../../packages/dynamic-store/src/index.ts'), 17 | }, 18 | { 19 | find: '@crux/event-emitter', 20 | replacement: path.resolve(__dirname, '../../packages/event-emitter/src/index.ts'), 21 | }, 22 | { 23 | find: '@crux/query', 24 | replacement: path.resolve(__dirname, '../../packages/query/src/index.ts'), 25 | }, 26 | { 27 | find: '@crux/redux-registry', 28 | replacement: path.resolve(__dirname, '../../packages/redux-registry/src/index.ts'), 29 | }, 30 | { 31 | find: '@crux/slice', 32 | replacement: path.resolve(__dirname, '../../packages/slice/src/index.ts'), 33 | }, 34 | { 35 | find: '@crux/redux-types', 36 | replacement: path.resolve(__dirname, '../../packages/redux-types/src/index.ts'), 37 | }, 38 | { 39 | find: '@crux/string-utils', 40 | replacement: path.resolve(__dirname, '../../packages/string-utils/src/index.ts'), 41 | }, 42 | { 43 | find: '@crux/utils', 44 | replacement: path.resolve(__dirname, '../../packages/utils/src/index.ts'), 45 | }, 46 | { 47 | find: '@crux/crux', 48 | replacement: path.resolve(__dirname, '../../packages/crux/src/index.ts'), 49 | }, 50 | ], 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /packages/event-emitter/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/event-emitter/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/event-emitter", 10 | "main": "packages/event-emitter/src/index.ts", 11 | "tsConfig": "packages/event-emitter/tsconfig.lib.json", 12 | "assets": ["packages/event-emitter/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/event-emitter", 20 | "tsConfig": "packages/event-emitter/tsconfig.lib.json", 21 | "project": "packages/event-emitter/package.json", 22 | "entryFile": "packages/event-emitter/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": [ 25 | "rollup.lib.prod.js", 26 | "packages/event-emitter/rollup.lib.js" 27 | ] 28 | } 29 | }, 30 | "publish": { 31 | "executor": "@nrwl/workspace:run-commands", 32 | "options": { 33 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts event-emitter" 34 | } 35 | }, 36 | "lint": { 37 | "executor": "@nrwl/linter:eslint", 38 | "outputs": ["{options.outputFile}"], 39 | "options": { 40 | "lintFilePatterns": ["packages/event-emitter/**/*.ts"] 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/packages/event-emitter"], 46 | "options": { 47 | "jestConfig": "packages/event-emitter/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/async-queue/src/lib/async-queue.ts: -------------------------------------------------------------------------------- 1 | type AnyFunction = (...args: any[]) => void; 2 | 3 | export interface AsyncQueue { 4 | add(fn: AnyFunction, ...params: unknown[]): Promise; 5 | clear(): void; 6 | entries: AsyncQueueEntry[]; 7 | flush(): Promise; 8 | } 9 | 10 | export interface AsyncQueueEntry { 11 | fn: AnyFunction; 12 | params: unknown[]; 13 | reject: AnyFunction; 14 | resolve: AnyFunction; 15 | } 16 | 17 | export function createAsyncQueue(): AsyncQueue { 18 | const entries: AsyncQueueEntry[] = []; 19 | let flushing = false; 20 | 21 | return { 22 | add, 23 | clear, 24 | entries, 25 | flush, 26 | }; 27 | 28 | function add(fn: AnyFunction, ...params: unknown[]): Promise { 29 | let rej: AnyFunction = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function 30 | let res: AnyFunction = () => {}; // eslint-disable-line @typescript-eslint/no-empty-function 31 | 32 | const promise = new Promise((resolve, reject) => { 33 | res = resolve; 34 | rej = reject; 35 | }); 36 | 37 | entries.push({ 38 | fn, 39 | params, 40 | reject: rej, 41 | resolve: res, 42 | }); 43 | 44 | return promise; 45 | } 46 | 47 | function clear() { 48 | flushing = false; 49 | 50 | entries.length = 0; 51 | } 52 | 53 | async function flush(): Promise { 54 | if (flushing) { 55 | return; 56 | } 57 | 58 | const entry = entries[0]; 59 | 60 | if (!entry) { 61 | return; 62 | } 63 | 64 | flushing = true; 65 | 66 | try { 67 | const result = await entry.fn(...entry.params); 68 | 69 | entry.resolve(result); 70 | 71 | entries.shift(); 72 | 73 | flushing = false; 74 | 75 | return flush(); 76 | } catch (e) { 77 | entry.reject(e); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/redux-registry/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "packages/redux-registry/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@nrwl/js:tsc", 7 | "outputs": ["{options.outputPath}"], 8 | "options": { 9 | "outputPath": "dist/packages/redux-registry", 10 | "main": "packages/redux-registry/src/index.ts", 11 | "tsConfig": "packages/redux-registry/tsconfig.lib.json", 12 | "assets": ["packages/redux-registry/*.md"] 13 | } 14 | }, 15 | "prod": { 16 | "executor": "@nrwl/web:rollup", 17 | "outputs": ["{options.outputPath}"], 18 | "options": { 19 | "outputPath": "dist/packages/redux-registry", 20 | "tsConfig": "packages/redux-registry/tsconfig.lib.json", 21 | "project": "packages/redux-registry/package.json", 22 | "entryFile": "packages/redux-registry/src/index.ts", 23 | "format": ["esm", "cjs"], 24 | "rollupConfig": [ 25 | "rollup.lib.prod.js", 26 | "packages/redux-registry/rollup.lib.js" 27 | ] 28 | } 29 | }, 30 | "publish": { 31 | "executor": "@nrwl/workspace:run-commands", 32 | "options": { 33 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts redux-registry" 34 | } 35 | }, 36 | "lint": { 37 | "executor": "@nrwl/linter:eslint", 38 | "outputs": ["{options.outputFile}"], 39 | "options": { 40 | "lintFilePatterns": ["packages/redux-registry/**/*.ts"] 41 | } 42 | }, 43 | "test": { 44 | "executor": "@nrwl/jest:jest", 45 | "outputs": ["coverage/packages/redux-registry"], 46 | "options": { 47 | "jestConfig": "packages/redux-registry/jest.config.ts", 48 | "passWithNoTests": true 49 | } 50 | } 51 | }, 52 | "tags": [] 53 | } 54 | -------------------------------------------------------------------------------- /packages/dynamic-store/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 3 | "sourceRoot": "packages/dynamic-store/src", 4 | "projectType": "library", 5 | "targets": { 6 | "build": { 7 | "executor": "@nrwl/js:tsc", 8 | "outputs": ["{options.outputPath}"], 9 | "options": { 10 | "outputPath": "dist/packages/dynamic-store", 11 | "main": "packages/dynamic-store/src/index.ts", 12 | "tsConfig": "packages/dynamic-store/tsconfig.lib.json", 13 | "assets": ["packages/dynamic-store/*.md"] 14 | } 15 | }, 16 | "prod": { 17 | "executor": "@nrwl/web:rollup", 18 | "outputs": ["{options.outputPath}"], 19 | "options": { 20 | "outputPath": "dist/packages/dynamic-store", 21 | "tsConfig": "packages/dynamic-store/tsconfig.lib.json", 22 | "project": "packages/dynamic-store/package.json", 23 | "entryFile": "packages/dynamic-store/src/index.ts", 24 | "format": ["esm", "cjs"], 25 | "rollupConfig": ["rollup.lib.prod.js", "packages/dynamic-store/rollup.lib.js"] 26 | } 27 | }, 28 | "publish": { 29 | "executor": "@nrwl/workspace:run-commands", 30 | "options": { 31 | "command": "./node_modules/.bin/ts-node tools/scripts/packages/publish-single.ts dynamic-store" 32 | } 33 | }, 34 | "lint": { 35 | "executor": "@nrwl/linter:eslint", 36 | "outputs": ["{options.outputFile}"], 37 | "options": { 38 | "lintFilePatterns": ["packages/dynamic-store/**/*.ts"] 39 | } 40 | }, 41 | "test": { 42 | "executor": "@nrwl/jest:jest", 43 | "outputs": ["coverage/packages/dynamic-store"], 44 | "options": { 45 | "jestConfig": "packages/dynamic-store/jest.config.ts", 46 | "passWithNoTests": true 47 | } 48 | } 49 | }, 50 | "tags": [] 51 | } 52 | -------------------------------------------------------------------------------- /packages/redux-registry/README.md: -------------------------------------------------------------------------------- 1 | ## `@crux/redux-registry` 2 | 3 | `@crux/redux-registry` is a tool to dynamically add and remove reducers and middleware. It was conceived to aid code-splitting. 4 | 5 | It's very small, weighing-in <600B minified and gzipped. 6 | 7 | ### Usage 8 | 9 | First create the registries and add them to your store: 10 | 11 | 12 | ```ts 13 | // ./store.ts 14 | import { configureStore } from '@reduxjs/toolkit'; 15 | import { middlewareRegistry, reducerRegistry } from '@crux/redux-registry'; 16 | 17 | const mRegistry = middlewareRegistry(); 18 | const rRegistry = reducerRegistry(); 19 | 20 | const store = configureStore({ 21 | reducer: rRegistry.reducer, 22 | middleware: (getDefaultMiddleware) => 23 | getDefaultMiddleware().concat(mRegistry.middleware), 24 | }); 25 | 26 | export const addMiddleware = mRegistry.add; 27 | export const addReducer = rRegistry.add; 28 | ``` 29 | 30 | Then in your application code: 31 | 32 | ```ts 33 | import { addMiddleware, addReducer } from './store'; 34 | 35 | /** 36 | * Example reducer that adds a user to a `data` array in the store. 37 | */ 38 | const usersReducer = (state, action) => { 39 | switch (action.type) { 40 | case 'addUser': 41 | return { 42 | ...state, 43 | data: [...state.data, action.payload] 44 | }; 45 | } 46 | } 47 | 48 | const removeUsersReducer = addReducer('users', usersReducer); 49 | 50 | /** 51 | * Example middleware to log when a user is added. 52 | */ 53 | const loggerMiddleware = () => next => action => { 54 | if (action.type === 'addUser') { 55 | console.log(action); 56 | } 57 | } 58 | 59 | const removeLoggerMiddleware = addMiddleware(loggerMiddleware); 60 | ``` 61 | 62 | Both `addReducer` and `addMiddleware` return a function that allows you to remove the reducer or middleware you added. So to clean up: 63 | 64 | ```ts 65 | removeUsersReducer(); 66 | removeLoggerMiddleware(); 67 | ``` -------------------------------------------------------------------------------- /packages/redux-registry/src/lib/redux-registry.ts: -------------------------------------------------------------------------------- 1 | import { Action, Dispatch, Middleware, MiddlewareAPI, Reducer } from '@crux/redux-types'; 2 | import { combineReducers, compose } from 'redux'; 3 | 4 | export const middlewareRegistry = () => { 5 | const mdw: Middleware[] = []; 6 | 7 | return { 8 | middleware: 9 | ({ getState, dispatch }: MiddlewareAPI) => 10 | (next: Dispatch) => 11 | (action: Action) => { 12 | const middlewareAPI = { 13 | getState, 14 | dispatch: (act: Action) => dispatch(act), 15 | } as MiddlewareAPI; 16 | 17 | const chain = mdw.map((m) => m(middlewareAPI)); 18 | 19 | return (compose(...chain)(next) as Dispatch)(action); 20 | }, 21 | 22 | add: (m: Middleware, order?: number) => { 23 | if (order === undefined) { 24 | mdw.push(m); 25 | } else { 26 | mdw.splice(order, 0, m); 27 | } 28 | 29 | return () => { 30 | const index = mdw.findIndex((d) => d === m); 31 | 32 | if (index < 0) { 33 | return; 34 | } 35 | 36 | mdw.splice(index, 1); 37 | }; 38 | }, 39 | 40 | getIndex: (m: Middleware) => { 41 | return mdw.findIndex((d) => d === m); 42 | }, 43 | }; 44 | }; 45 | 46 | export const reducerRegistry = () => { 47 | const reducers = new Map(); 48 | let currentReducer: Reducer = (state?: S) => state || {}; 49 | 50 | return { 51 | add, 52 | reducer: (state: any, action: Action) => currentReducer(state, action), 53 | }; 54 | 55 | function add(id: string, newReducer: Reducer) { 56 | reducers.set(id, newReducer); 57 | 58 | currentReducer = combineReducers(Object.fromEntries(reducers)) as Reducer; 59 | 60 | return function unregisterReducer() { 61 | reducers.delete(id); 62 | 63 | currentReducer = combineReducers(Object.fromEntries(reducers)) as Reducer; 64 | }; 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/components/container/container.ts: -------------------------------------------------------------------------------- 1 | import { createDraggable } from '../../../../utils/draggable'; 2 | import { LitElement, html, unsafeCSS } from 'lit'; 3 | import { customElement, property } from 'lit/decorators.js'; 4 | import styles from './container.css?inline'; 5 | 6 | @customElement('todos-container') 7 | export class TodosContainer extends LitElement { 8 | static styles = [unsafeCSS(styles)]; 9 | 10 | private destroyDraggable: () => void; 11 | 12 | @property({ type: String }) 13 | 'id-attribute' = ''; 14 | 15 | @property({ type: String }) 16 | 'dragging-class' = 'dragging'; 17 | 18 | @property({ type: String }) 19 | 'drag-selector' = '.task'; 20 | 21 | _onDrag(id: string) { 22 | if (id) { 23 | const options = { 24 | detail: { id }, 25 | bubbles: true, 26 | composed: true, 27 | }; 28 | 29 | this.dispatchEvent(new CustomEvent('dragTask', options)); 30 | } 31 | } 32 | 33 | _onDrop(id: string) { 34 | if (id) { 35 | const options = { 36 | detail: { id }, 37 | bubbles: true, 38 | composed: true, 39 | }; 40 | 41 | this.dispatchEvent(new CustomEvent('dropTask', options)); 42 | } 43 | } 44 | 45 | connectedCallback() { 46 | super.connectedCallback(); 47 | 48 | this.destroyDraggable = createDraggable({ 49 | idAttribute: this['id-attribute'], 50 | draggingClass: this['dragging-class'], 51 | dragSelector: this['drag-selector'], 52 | onDrag: this._onDrag.bind(this), 53 | onDrop: this._onDrop.bind(this), 54 | }); 55 | } 56 | 57 | disconnectedCallback(): void { 58 | this.destroyDraggable?.(); 59 | } 60 | 61 | render() { 62 | return html`
    63 | 64 |
    `; 65 | } 66 | } 67 | 68 | declare global { 69 | interface HTMLElementTagNameMap { 70 | 'todos-container': TodosContainer; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "13.9.0-beta.0", 5 | "description": "Replace @nrwl/tao with nx", 6 | "cli": "nx", 7 | "implementation": "./src/migrations/update-13-9-0/replace-tao-with-nx", 8 | "package": "@nrwl/workspace", 9 | "name": "13-9-0-replace-tao-with-nx" 10 | }, 11 | { 12 | "version": "13.10.0-beta.0", 13 | "description": "Update the decorate-angular-cli script to require nx instead of @nrwl/cli", 14 | "cli": "nx", 15 | "implementation": "./src/migrations/update-13-10-0/update-decorate-cli", 16 | "package": "@nrwl/workspace", 17 | "name": "13-10-0-update-decorate-cli" 18 | }, 19 | { 20 | "version": "13.10.0-beta.0", 21 | "description": "Update the tasks runner property to import it from the nx package instead of @nrwl/worksapce", 22 | "cli": "nx", 23 | "implementation": "./src/migrations/update-13-10-0/update-tasks-runner", 24 | "package": "@nrwl/workspace", 25 | "name": "13-10-0-update-tasks-runner" 26 | }, 27 | { 28 | "version": "14.0.0-beta.0", 29 | "description": "Changes the presets in nx.json to come from the nx package", 30 | "cli": "nx", 31 | "implementation": "./src/migrations/update-14-0-0/change-nx-json-presets", 32 | "package": "@nrwl/workspace", 33 | "name": "14-0-0-change-nx-json-presets" 34 | }, 35 | { 36 | "version": "14.0.0-beta.0", 37 | "description": "Migrates from @nrwl/workspace:run-script to nx:run-script", 38 | "cli": "nx", 39 | "implementation": "./src/migrations/update-14-0-0/change-npm-script-executor", 40 | "package": "@nrwl/workspace", 41 | "name": "14-0-0-change-npm-script-executor" 42 | }, 43 | { 44 | "version": "14.2.0", 45 | "description": "Explicitly enable sourceAnalysis for all workspaces extending from npm.json or core.json (this was default behavior prior to 14.2)", 46 | "cli": "nx", 47 | "implementation": "./src/migrations/update-14-2-0/enable-source-analysis", 48 | "package": "@nrwl/workspace", 49 | "name": "14-2-0-enable-source-analysis" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/crux/src/lib/subscription.test.ts: -------------------------------------------------------------------------------- 1 | import { service } from './service'; 2 | import { subscription } from './subscription'; 3 | 4 | const mockA = jest.fn(); 5 | 6 | describe('service', () => { 7 | beforeEach(() => { 8 | mockA.mockClear(); 9 | }); 10 | 11 | test('calls handler with dependencies', async () => { 12 | const sA = service(() => Promise.resolve(serviceA())); 13 | const a = subscription((servA, selA) => Promise.resolve(subscriptionA(servA, selA)), { 14 | deps: [sA, selectorA], 15 | }); 16 | 17 | await a.runSubscription({ a: 'a' }); 18 | 19 | expect(mockA.mock.calls[0][0]).toEqual({ 20 | isA: true, 21 | a: 'a', 22 | }); 23 | }); 24 | 25 | test('does not call handler if inputs have not changed', async () => { 26 | const sA = service(() => Promise.resolve(serviceA())); 27 | const a = subscription((servA, selA) => subscriptionA(servA, selA), { 28 | deps: [sA, selectorA], 29 | }); 30 | 31 | const state = { a: 'a' }; 32 | 33 | await a.runSubscription(state); 34 | await a.runSubscription(state); // same call 35 | 36 | expect(mockA.mock.calls.length).toEqual(1); 37 | 38 | mockA.mockClear(); 39 | 40 | const nestedState = { a: { nested: 'a' } }; 41 | 42 | await a.runSubscription(nestedState); 43 | await a.runSubscription(nestedState); 44 | 45 | expect(mockA.mock.calls.length).toEqual(1); 46 | }); 47 | 48 | test('calls handler again if state is new reference', async () => { 49 | const sA = service(() => Promise.resolve(serviceA())); 50 | 51 | const a = subscription((servA, selA) => Promise.resolve(subscriptionA(servA, selA)), { 52 | deps: [sA, selectorA], 53 | }); 54 | 55 | await a.runSubscription({ a: { nested: 'a' } }); 56 | await a.runSubscription({ a: { nested: 'a' } }); // same call but different reference 57 | 58 | expect(mockA.mock.calls.length).toEqual(2); 59 | }); 60 | }); 61 | 62 | function serviceA() { 63 | return { 64 | isA: true, 65 | }; 66 | } 67 | 68 | function selectorA(state: any) { 69 | return state.a as boolean; 70 | } 71 | 72 | function subscriptionA(service: ReturnType, value: ReturnType) { 73 | mockA({ 74 | isA: service.isA, 75 | a: value, 76 | }); 77 | } 78 | -------------------------------------------------------------------------------- /packages/crux/src/lib/view.ts: -------------------------------------------------------------------------------- 1 | import type { Selector, Service, Slice, View, ViewInstance } from './types'; 2 | 3 | export function view( 4 | factory: () => Promise> | ViewInstance, 5 | options: { 6 | actions?: Service
    | Slice; 7 | data: Selector; 8 | root: string; 9 | } 10 | ): View { 11 | const { actions, data, root } = options; 12 | 13 | let currentData: D | undefined; 14 | let instance: ViewInstance | undefined; 15 | let promise: Promise> | undefined; 16 | let renderFn: ((data: D, actions: A) => void) | undefined; 17 | let renderedTo: HTMLElement | undefined; 18 | 19 | return { 20 | get, 21 | getCurrentData: () => currentData, 22 | instance, 23 | promise, 24 | render, 25 | root, 26 | updateData, 27 | }; 28 | 29 | async function get(): Promise> { 30 | if (instance) { 31 | return instance; 32 | } 33 | 34 | if (promise) { 35 | return promise; 36 | } 37 | 38 | const ret = factory(); 39 | 40 | if (ret instanceof Promise) { 41 | ret.then((i) => { 42 | instance = i; 43 | }); 44 | 45 | return ret; 46 | } 47 | 48 | instance = ret; 49 | 50 | return instance; 51 | } 52 | 53 | function updateData(state: any) { 54 | const newData = data(state); 55 | 56 | if (newData !== currentData) { 57 | currentData = newData; 58 | 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | 65 | async function render(root: HTMLElement, state: any) { 66 | if (!instance) { 67 | await get(); 68 | } 69 | 70 | if (!instance) { 71 | throw new Error('Could not get view instance'); 72 | } 73 | 74 | // Return if the data hasn't changed and we've already rendered to this root 75 | if (!updateData(state) && root?.childElementCount) { 76 | return; 77 | } 78 | 79 | const actionsAPI = actions ? await actions.getAPI() : undefined; 80 | 81 | if (renderedTo === root && renderFn) { 82 | return renderFn(currentData as D, actionsAPI as A); 83 | } 84 | 85 | renderFn = instance(root); 86 | renderedTo = root; 87 | 88 | return renderFn(currentData as D, actionsAPI as A); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/crux/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { DynamicStore } from '@crux/dynamic-store'; 2 | import { Middleware, Reducer } from '@crux/redux-types'; 3 | 4 | export type NodeTypes[]> = { 5 | [P in keyof T]: T[P] extends APIType ? R : never; 6 | } & unknown[]; 7 | 8 | export type SelectorTypes = { 9 | [P in keyof T]: T[P] extends Selector ? U : never; 10 | }; 11 | 12 | export type SelectorOrServiceTypes = { 13 | [P in keyof T]: T[P] extends Selector 14 | ? U 15 | : T[P] extends APIType 16 | ? R 17 | : never; 18 | } & unknown[]; 19 | 20 | export type SelectorOrSliceTypes = { 21 | [P in keyof T]: T[P] extends Selector ? U : T[P] extends Slice ? S : never; 22 | }; 23 | 24 | export type Selector = (state: T) => U; 25 | 26 | export type LayoutSelector = (state: any) => { [key: string]: string }; 27 | 28 | export type APIType = { getAPI(): Promise }; 29 | 30 | export type Service = APIType & { 31 | getInstance(): Promise; 32 | instance?: I; 33 | promise?: Promise; 34 | }; 35 | 36 | export type ExtractInstance = T extends Service ? I : never; 37 | 38 | export type ViewInstance = (root: HTMLElement) => (data: D, actions: A) => void; 39 | 40 | export type View = { 41 | get: () => Promise>; 42 | getCurrentData: () => D | undefined; 43 | instance?: ViewInstance; 44 | promise?: Promise>; 45 | render: (root: HTMLElement, state: any) => void | Promise; 46 | root: string; 47 | updateData(state: any): boolean; 48 | }; 49 | 50 | export type Slice = APIType & { 51 | bindStore(store: DynamicStore): void; 52 | getInstance(): Promise>; 53 | getStore(): DynamicStore | undefined; 54 | getPromise(): Promise> | undefined; 55 | getUnregister(): (() => void) | undefined; 56 | name: string; 57 | register(middleware?: Middleware, reducer?: Reducer): void; 58 | selector(state: { [key: string]: State }): State; 59 | shouldBeEnabled?(state: any): boolean; 60 | store?: DynamicStore; 61 | }; 62 | 63 | export type SliceInstance = { 64 | api: API; 65 | middleware?: Middleware; 66 | reducer: Reducer; 67 | }; 68 | -------------------------------------------------------------------------------- /apps/dev/src/features/todos/domain/todos.service.ts: -------------------------------------------------------------------------------- 1 | import type { ToasterService } from '../../toaster/toaster.service'; 2 | import type { TodosRepository } from '../todos.index'; 3 | import type { Status } from './todos.types'; 4 | 5 | export type TodosService = ReturnType; 6 | 7 | export function todosService(todos: TodosRepository, toaster: ToasterService) { 8 | return { 9 | createTask, 10 | onDrag, 11 | onDrop, 12 | onEnter, 13 | onExit, 14 | }; 15 | 16 | function createTask() { 17 | todos.createTask({ 18 | status: 'to-do', 19 | text: 'Do something...', 20 | }); 21 | } 22 | 23 | function onDrag(taskId: string) { 24 | todos.setDraggingTaskId(taskId); 25 | } 26 | 27 | async function onDrop(taskId: string) { 28 | todos.setDraggingTaskId(null); 29 | 30 | const tasks = todos.getTasks(); 31 | const hoveringState = todos.getHoveringState(); 32 | 33 | if (!hoveringState) { 34 | return; 35 | } 36 | 37 | const { ndx: hoveringIndex, status: hoveringStatus } = hoveringState; 38 | 39 | todos.setHoveringState(null); 40 | 41 | if (hoveringIndex !== undefined && hoveringStatus !== undefined) { 42 | const newStatusTasks = tasks.filter((task) => task.status === hoveringStatus); 43 | const ndx = newStatusTasks.findIndex((task) => task.id === taskId); 44 | const task = tasks.find((task) => task.id === taskId); 45 | 46 | // If it's the same status and the new index is -0.5 <= ndx <= 0.5, then do nothing. 47 | if (hoveringStatus === task.status && Math.abs(ndx - hoveringIndex) < 1) { 48 | return; 49 | } 50 | 51 | const newStatusIndex = 52 | hoveringIndex < 1 ? Math.floor(hoveringIndex) : Math.ceil(hoveringIndex); 53 | 54 | try { 55 | await todos.updateTask({ ...task, status: hoveringStatus }, newStatusIndex); 56 | 57 | toaster.toast({ 58 | duration: 4000, 59 | text: `Task moved to ${hoveringStatus}.`, 60 | variant: 'success', 61 | }); 62 | } catch (e) { 63 | toaster.toast({ 64 | duration: 4000, 65 | text: `Failed to move task to ${hoveringStatus}.`, 66 | variant: 'danger', 67 | }); 68 | } 69 | } 70 | } 71 | 72 | function onEnter(taskNdx: number, status: Status) { 73 | todos.setHoveringState({ ndx: taskNdx, status }); 74 | } 75 | 76 | function onExit() { 77 | todos.setHoveringState(null); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /apps/dev/src/utils/draggable/index.ts: -------------------------------------------------------------------------------- 1 | export function createDraggable({ 2 | idAttribute, 3 | draggingClass, 4 | dragSelector, 5 | onDrag, 6 | onDragMove, 7 | onDrop, 8 | }: { 9 | idAttribute: string; 10 | draggingClass?: string; 11 | dragSelector: string; 12 | onDrag: (id: string, event: Event) => void; 13 | onDragMove?: (event: Event) => void; 14 | onDrop: (id: string, event: Event) => void; 15 | }) { 16 | document.addEventListener('pointerdown', onPointerdown); 17 | 18 | return () => { 19 | document.removeEventListener('pointerdown', onPointerdown); 20 | }; 21 | 22 | function onPointerdown(event: Event) { 23 | const target = (event.target as HTMLElement)?.closest(dragSelector) as HTMLElement; 24 | 25 | if (!target) { 26 | return; 27 | } 28 | 29 | const x = (event as PointerEvent).clientX; 30 | const y = (event as PointerEvent).clientY; 31 | 32 | const rect = target.getBoundingClientRect(); 33 | let shiftX = x - rect.left; 34 | let shiftY = y - rect.top; 35 | 36 | const clone = target.cloneNode(true) as HTMLElement; 37 | clone.style.width = `${rect.width}px`; 38 | clone.style.height = `${rect.height}px`; 39 | clone.style.position = 'absolute'; 40 | clone.style.zIndex = '1000'; 41 | clone.style.userSelect = 'none'; 42 | clone.style.pointerEvents = 'none'; 43 | 44 | if (draggingClass) { 45 | clone.className = draggingClass; 46 | } 47 | 48 | clone.ondragstart = function () { 49 | return false; 50 | }; 51 | 52 | document.body.append(clone); 53 | 54 | document.addEventListener('pointermove', onPointermove); 55 | document.addEventListener('pointerup', onPointerup); 56 | 57 | snapTo(x, y); 58 | onDrag(target.getAttribute(idAttribute), event); 59 | 60 | function snapTo(clientX: number, clientY: number) { 61 | clone.style.left = clientX - shiftX + 'px'; 62 | clone.style.top = clientY - shiftY + 'px'; 63 | } 64 | 65 | function onPointermove(event: Event) { 66 | const currentX = (event as PointerEvent).clientX; 67 | const currentY = (event as PointerEvent).clientY; 68 | 69 | snapTo(currentX, currentY); 70 | 71 | onDragMove?.(event); 72 | } 73 | 74 | function onPointerup() { 75 | document.removeEventListener('pointermove', onPointermove); 76 | document.removeEventListener('pointerup', onPointerup); 77 | clone.remove(); 78 | onDrop(target.getAttribute(idAttribute), event); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tools/scripts/packages/version-packages.ts: -------------------------------------------------------------------------------- 1 | import { ProjectGraph } from '@nrwl/devkit'; 2 | import { readFileSync, writeFileSync } from 'fs'; 3 | import { exec } from 'child_process'; 4 | import { config } from 'dotenv'; 5 | 6 | config(); 7 | 8 | execute('npx nx dep-graph --file=tools/scripts/publish/tmp/project.json').then(() => { 9 | const projectJson = JSON.parse(readFileSync(`tools/scripts/publish/tmp/project.json`, 'utf8')); 10 | versionPackages(projectJson.graph); 11 | }); 12 | 13 | export async function versionPackages(graph: ProjectGraph) { 14 | const version = getCurrentVersion(); 15 | const libNodes = getLibs(graph); 16 | 17 | for (const name of libNodes) { 18 | const packageJson = getProjectPackageJson(name, 'dev'); 19 | packageJson.version = version; 20 | 21 | writePackageJson(packageJson, name); 22 | } 23 | } 24 | 25 | function getLibs(graph: ProjectGraph) { 26 | return Object.keys(graph.nodes).filter((nodeName) => graph.nodes[nodeName].type === 'lib'); 27 | } 28 | 29 | function getPackagePath(name: string) { 30 | return `packages/${name}`; 31 | } 32 | 33 | function getPackageDistPath(name: string) { 34 | return `dist/${getPackagePath(name)}`; 35 | } 36 | 37 | function writePackageJson(obj: any, name: string) { 38 | const json = JSON.stringify(obj, null, 2); 39 | const path = `${getPackagePath(name)}/package.json`; 40 | 41 | console.log(`Writing JSON to ${path}`); 42 | console.log(json); 43 | writeFileSync(path, json); 44 | } 45 | 46 | function getCurrentVersion() { 47 | const { version } = JSON.parse(readFileSync(`package.json`, 'utf8')); 48 | 49 | console.log(`Current version: ${version}`); 50 | 51 | return version; 52 | } 53 | 54 | function getProjectPackageJson(name: string, env: 'dev' | 'prod') { 55 | return JSON.parse( 56 | readFileSync( 57 | `${env === 'prod' ? getPackageDistPath(name) : getPackagePath(name)}/package.json`, 58 | 'utf8' 59 | ) 60 | ); 61 | } 62 | 63 | function getProjectDeps(graph: ProjectGraph, name: string): string[] { 64 | const deps = [] as any[]; 65 | 66 | for (const file of graph.nodes[name].data.files) { 67 | for (const dep of file.deps || []) { 68 | if (dep && !dep.startsWith('npm:')) { 69 | deps.push(dep); 70 | } 71 | } 72 | } 73 | 74 | return [...new Set(deps)]; 75 | } 76 | 77 | function execute(command: string) { 78 | return new Promise((resolve) => { 79 | exec(command, function (error, stdout, stderr) { 80 | resolve(stdout); 81 | }); 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /apps/dev/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crux", 3 | "version": "0.0.47-alpha", 4 | "license": "MIT", 5 | "scripts": { 6 | "build-packages": "npx nx run-many --target=prod --all --with-deps", 7 | "publish-packages": "npx nx run-many --target=publish --all --with-deps", 8 | "version-packages": "ts-node --project=tools/tsconfig.tools.json tools/scripts/packages/version-packages.ts" 9 | }, 10 | "private": false, 11 | "dependencies": { 12 | "@nrwl/js": "^13.7.3", 13 | "@reduxjs/toolkit": "1.8.3", 14 | "@shoelace-style/shoelace": "^2.0.0-beta.82", 15 | "animate.css": "^4.1.1", 16 | "classnames": "^2.3.1", 17 | "core-js": "^3.6.5", 18 | "idb-keyval": "^6.2.0", 19 | "lit-html": "^2.3.1", 20 | "path": "^0.12.7", 21 | "react": "18.2.0", 22 | "react-dom": "18.2.0", 23 | "react-is": "18.0.0", 24 | "react-redux": "8.0.2", 25 | "redux": "^4.2.0", 26 | "regenerator-runtime": "0.13.7", 27 | "reselect": "^4.1.6", 28 | "rollup-plugin-copy": "^3.4.0", 29 | "tslib": "^2.0.0", 30 | "vite": "3.2.0" 31 | }, 32 | "devDependencies": { 33 | "@jsdevtools/npm-publish": "^1.4.3", 34 | "@nrwl/cli": "14.5.4", 35 | "@nrwl/cypress": "14.5.4", 36 | "@nrwl/devkit": "14.5.4", 37 | "@nrwl/eslint-plugin-nx": "14.5.4", 38 | "@nrwl/jest": "14.5.4", 39 | "@nrwl/js": "14.5.4", 40 | "@nrwl/linter": "14.5.4", 41 | "@nrwl/nx-cloud": "14.3.0", 42 | "@nrwl/react": "14.5.4", 43 | "@nrwl/web": "14.5.4", 44 | "@nrwl/workspace": "14.5.4", 45 | "@rollup/plugin-commonjs": "^21.0.1", 46 | "@testing-library/react": "13.3.0", 47 | "@types/jest": "27.4.1", 48 | "@types/jsdom": "^20.0.0", 49 | "@types/node": "18.0.0", 50 | "@types/react": "18.0.15", 51 | "@types/react-dom": "18.0.6", 52 | "@typescript-eslint/eslint-plugin": "5.33.0", 53 | "@typescript-eslint/parser": "5.33.0", 54 | "babel-jest": "27.5.1", 55 | "cypress": "^9.1.0", 56 | "dotenv": "^16.0.1", 57 | "eslint": "8.15.0", 58 | "eslint-config-prettier": "8.1.0", 59 | "eslint-plugin-cypress": "^2.10.3", 60 | "eslint-plugin-import": "2.26.0", 61 | "eslint-plugin-jsx-a11y": "6.6.1", 62 | "eslint-plugin-react": "7.30.1", 63 | "eslint-plugin-react-hooks": "4.6.0", 64 | "jest": "27.5.1", 65 | "msw": "^0.45.0", 66 | "nx": "14.5.4", 67 | "prettier": "2.7.1", 68 | "react-test-renderer": "18.2.0", 69 | "redux-thunk": "^2.4.1", 70 | "rollup-plugin-postcss": "^4.0.2", 71 | "rollup-plugin-postcss-lit": "^2.0.0", 72 | "rollup-plugin-terser": "^7.0.2", 73 | "ts-jest": "27.1.4", 74 | "ts-node": "~10.8.0", 75 | "typescript": "4.7.4" 76 | }, 77 | "workspaces": [ 78 | "apps/**", 79 | "packages/**" 80 | ], 81 | "msw": { 82 | "workerDirectory": "public" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /apps/dev/src/shared/http/users/users-http.service.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: number; 3 | email: string; 4 | roles: string[]; 5 | } 6 | 7 | export interface Response { 8 | data: Data; 9 | } 10 | 11 | export type PostUser = Omit; 12 | export type PutUser = Partial; 13 | 14 | export type Users = typeof createUsersHttp; 15 | export type UsersAPI = ReturnType; 16 | 17 | export function createUsersHttp() { 18 | const users: User[] = [ 19 | { id: 1, email: 'name1', roles: ['admin'] }, 20 | { id: 2, email: 'name2', roles: ['user'] }, 21 | ]; 22 | 23 | return { 24 | getAll: (): Promise> => 25 | Promise.resolve({ 26 | data: users, 27 | }), 28 | getOne: (id: number): Promise> => 29 | new Promise((resolve, reject) => { 30 | const user = users.find((u) => u.id === id); 31 | 32 | if (!user) { 33 | reject({ 34 | error: { 35 | code: '404', 36 | message: 'User not found', 37 | }, 38 | }); 39 | } 40 | 41 | resolve({ 42 | data: user, 43 | }); 44 | }), 45 | post: (user: PostUser): Promise> => 46 | new Promise((resolve, reject) => { 47 | if (!user.email) { 48 | reject({ 49 | error: { 50 | code: '400', 51 | message: 'Bad request', 52 | }, 53 | }); 54 | } 55 | 56 | const lastId = Math.max(...users.map((user) => user.id)); 57 | 58 | const newUser = { 59 | ...user, 60 | id: lastId + 1, 61 | }; 62 | 63 | users.push(newUser); 64 | 65 | resolve({ 66 | data: newUser, 67 | }); 68 | }), 69 | put: (user: PutUser): Promise> => 70 | new Promise((resolve, reject) => { 71 | const existingUser = users.find((u) => u.id === user.id); 72 | 73 | if (!existingUser) { 74 | reject({ 75 | error: { 76 | code: '400', 77 | message: 'Bad request', 78 | }, 79 | }); 80 | } 81 | 82 | Object.assign(existingUser as User, user); 83 | 84 | resolve({ 85 | data: existingUser, 86 | }); 87 | }), 88 | delete: (id: number): Promise> => 89 | new Promise((resolve, reject) => { 90 | const existingUserNdx = users.findIndex((u) => u.id === id); 91 | 92 | if (existingUserNdx === -1) { 93 | reject({ 94 | error: { 95 | code: '400', 96 | message: 'Bad request', 97 | }, 98 | }); 99 | } 100 | 101 | users.splice(existingUserNdx, 1); 102 | 103 | resolve({ 104 | data: null, 105 | }); 106 | }), 107 | }; 108 | } 109 | -------------------------------------------------------------------------------- /apps/dev/src/design/directives/draggable/index.ts: -------------------------------------------------------------------------------- 1 | import { Directive, directive } from 'lit/directive.js'; 2 | import type { ElementPart } from 'lit/directive.js'; 3 | import { noChange } from 'lit'; 4 | export class Draggable extends Directive { 5 | registered = false; 6 | 7 | render({ 8 | dataProp, 9 | draggingClass, 10 | dragSelector, 11 | onDrag, 12 | onDragMove, 13 | onDrop, 14 | }: { 15 | dataProp: string; 16 | draggingClass?: string; 17 | dragSelector: string; 18 | onDrag: (id: string, event: Event) => void; 19 | onDragMove?: (event: Event) => void; 20 | onDrop: (id: string, event: Event) => void; 21 | }) { 22 | return noChange; 23 | } 24 | 25 | update( 26 | part: ElementPart, 27 | [{ dataProp, draggingClass, dragSelector, onDrag, onDragMove, onDrop }]: Parameters< 28 | Draggable['render'] 29 | > 30 | ) { 31 | if (this.registered) { 32 | return; 33 | } 34 | 35 | this.registered = true; 36 | 37 | const { element } = part; 38 | 39 | element.addEventListener('pointerdown', (event) => { 40 | const target = (event.target as HTMLElement)?.closest(dragSelector) as HTMLElement; 41 | 42 | if (!target) { 43 | return; 44 | } 45 | 46 | const x = (event as PointerEvent).clientX; 47 | const y = (event as PointerEvent).clientY; 48 | 49 | const rect = target.getBoundingClientRect(); 50 | let shiftX = x - rect.left; 51 | let shiftY = y - rect.top; 52 | 53 | const clone = target.cloneNode(true) as HTMLElement; 54 | clone.style.width = `${rect.width}px`; 55 | clone.style.height = `${rect.height}px`; 56 | // clone.style.position = 'absolute'; 57 | // clone.style.zIndex = '1000'; 58 | // clone.style.transition = 'none'; 59 | // clone.style.pointerEvents = 'none'; 60 | 61 | if (draggingClass) { 62 | clone.className = draggingClass; 63 | } 64 | 65 | clone.ondragstart = function () { 66 | return false; 67 | }; 68 | 69 | document.body.append(clone); 70 | 71 | snapTo(x, y); 72 | 73 | document.addEventListener('pointermove', onMouseMove); 74 | 75 | document.addEventListener('pointerup', () => { 76 | document.removeEventListener('pointermove', onMouseMove); 77 | clone.remove(); 78 | onDrop(target.dataset[dataProp], event); 79 | }); 80 | 81 | onDrag(target.dataset[dataProp], event); 82 | 83 | function snapTo(clientX: number, clientY: number) { 84 | clone.style.left = clientX - shiftX + 'px'; 85 | clone.style.top = clientY - shiftY + 'px'; 86 | } 87 | 88 | function onMouseMove(event: Event) { 89 | const currentX = (event as PointerEvent).clientX; 90 | const currentY = (event as PointerEvent).clientY; 91 | 92 | snapTo(currentX, currentY); 93 | 94 | onDragMove?.(event); 95 | } 96 | }); 97 | 98 | return noChange; 99 | } 100 | } 101 | 102 | export const draggable = directive(Draggable); 103 | --------------------------------------------------------------------------------