mirror of
https://github.com/penpot/penpot.git
synced 2026-03-17 07:56:14 +00:00
✨ Add improvements to AGENTS.md (#8586)
This commit is contained in:
21
.github/workflows/tests.yml
vendored
21
.github/workflows/tests.yml
vendored
@@ -34,6 +34,8 @@ jobs:
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:js
|
||||
pnpm run lint:clj
|
||||
|
||||
- name: Lint Frontend
|
||||
@@ -42,6 +44,9 @@ jobs:
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:js
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:scss
|
||||
pnpm run lint:clj
|
||||
pnpm run lint:js
|
||||
pnpm run lint:scss
|
||||
@@ -52,7 +57,8 @@ jobs:
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run lint:clj
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
- name: Lint Exporter
|
||||
working-directory: ./exporter
|
||||
@@ -60,7 +66,8 @@ jobs:
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run lint:clj
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
- name: Lint Library
|
||||
working-directory: ./library
|
||||
@@ -68,7 +75,8 @@ jobs:
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run lint:clj
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
test-common:
|
||||
name: "Common Tests"
|
||||
@@ -79,12 +87,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run tests on JVM
|
||||
working-directory: ./common
|
||||
run: |
|
||||
clojure -M:dev:test
|
||||
|
||||
- name: Run tests on NODE
|
||||
- name: Run tests
|
||||
working-directory: ./common
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -1,11 +1,4 @@
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnpm-store
|
||||
*-init.clj
|
||||
*.css.json
|
||||
*.jar
|
||||
@@ -20,8 +13,6 @@
|
||||
.nyc_output
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
.shadow-cljs
|
||||
.pnpm-store/
|
||||
/*.jpg
|
||||
/*.md
|
||||
/*.png
|
||||
@@ -36,6 +27,7 @@
|
||||
/playground/
|
||||
/backend/*.md
|
||||
!/backend/AGENTS.md
|
||||
/backend/.shadow-cljs
|
||||
/backend/*.sql
|
||||
/backend/*.txt
|
||||
/backend/assets/
|
||||
@@ -48,13 +40,13 @@
|
||||
/backend/experiments
|
||||
/backend/scripts/_env.local
|
||||
/bundle*
|
||||
/cd.md
|
||||
/clj-profiler/
|
||||
/common/coverage
|
||||
/common/target
|
||||
/deploy
|
||||
/common/.shadow-cljs
|
||||
/docker/images/bundle*
|
||||
/exporter/target
|
||||
/exporter/.shadow-cljs
|
||||
/frontend/.storybook/preview-body.html
|
||||
/frontend/.storybook/preview-head.html
|
||||
/frontend/playwright-report/
|
||||
@@ -68,9 +60,9 @@
|
||||
/frontend/storybook-static/
|
||||
/frontend/target/
|
||||
/frontend/test-results/
|
||||
/frontend/.shadow-cljs
|
||||
/other/
|
||||
/scripts/
|
||||
/telemetry/
|
||||
/nexus/
|
||||
/tmp/
|
||||
/vendor/**/target
|
||||
/vendor/svgclean/bundle*.js
|
||||
@@ -79,13 +71,11 @@
|
||||
/library/*.zip
|
||||
/external
|
||||
/penpot-nitrate
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/**/node_modules
|
||||
/**/.yarn/*
|
||||
/.pnpm-store
|
||||
|
||||
352
AGENTS.md
352
AGENTS.md
@@ -1,4 +1,4 @@
|
||||
# Penpot – Copilot Instructions
|
||||
# Penpot – Instructions
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
@@ -18,7 +18,13 @@ The monorepo is managed with `pnpm` workspaces. The `manage.sh`
|
||||
orchestrates cross-component builds. `run-ci.sh` defines the CI
|
||||
pipeline.
|
||||
|
||||
---
|
||||
## Search Standards
|
||||
|
||||
When searching code, always use `ripgrep` (rg) instead of grep if
|
||||
available, as it respects `.gitignore` by default.
|
||||
|
||||
If using grep, try to exclude node_modules and .shadow-cljs directories
|
||||
|
||||
|
||||
## Build, Test & Lint Commands
|
||||
|
||||
@@ -28,27 +34,26 @@ Run `./scripts/setup` for setup all dependencies.
|
||||
|
||||
|
||||
```bash
|
||||
# Dev
|
||||
pnpm run watch:app # Full dev build (WASM + CLJS + assets)
|
||||
|
||||
# Production Build
|
||||
# Build (Producution)
|
||||
./scripts/build
|
||||
|
||||
# Tests
|
||||
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
|
||||
pnpm run watch:test # Watch + auto-rerun on change
|
||||
pnpm run test:e2e # Playwright e2e tests
|
||||
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
|
||||
pnpm run test # Build ClojureScript tests + run node target/tests/test.js
|
||||
|
||||
# Lint
|
||||
pnpm run lint:js # format and linter check for JS
|
||||
pnpm run lint:clj # format and linter check for CLJ
|
||||
pnpm run lint:scss # prettier check for SCSS
|
||||
pnpm run lint:js # Linter for JS/TS
|
||||
pnpm run lint:clj # Linter for CLJ/CLJS/CLJC
|
||||
pnpm run lint:scss # Linter for SCSS
|
||||
|
||||
# Code formatting
|
||||
pnpm run fmt:clj # Format CLJ
|
||||
pnpm run fmt:js # prettier for JS
|
||||
pnpm run fmt:scss # prettier for SCSS
|
||||
# Check Code Formart
|
||||
pnpm run check-fmt:clj # Format CLJ/CLJS/CLJC
|
||||
pnpm run check-fmt:js # Format JS/TS
|
||||
pnpm run check-fmt:scss # Format SCSS
|
||||
|
||||
# Code Format (Automatic Formating)
|
||||
pnpm run fmt:clj # Format CLJ/CLJS/CLJC
|
||||
pnpm run fmt:js # Format JS/TS
|
||||
pnpm run fmt:scss # Format SCSS
|
||||
```
|
||||
|
||||
To run a focused ClojureScript unit test: edit
|
||||
@@ -58,28 +63,63 @@ run build:test && node target/tests/test.js`.
|
||||
|
||||
### Backend (`cd backend`)
|
||||
|
||||
```bash
|
||||
# Tests (Kaocha)
|
||||
clojure -M:dev:test # Full suite
|
||||
clojure -M:dev:test --focus backend-tests.my-ns-test # Single namespace
|
||||
Run `pnpm install` for install all dependencies.
|
||||
|
||||
# Lint / Format
|
||||
pnpm run lint:clj
|
||||
pnpm run fmt:clj
|
||||
```bash
|
||||
# Run full test suite
|
||||
pnpm run test
|
||||
|
||||
# Run single namespace
|
||||
pnpm run test --focus backend-tests.rpc-doc-test
|
||||
|
||||
# Check Code Format
|
||||
pnpm run check-fmt
|
||||
|
||||
# Code Format (Automatic Formatting)
|
||||
pnpm run fmt
|
||||
|
||||
# Code Linter
|
||||
pnpm run lint
|
||||
```
|
||||
|
||||
Test config is in `backend/tests.edn`; test namespaces match `.*-test$` under `test/`.
|
||||
Test config is in `backend/tests.edn`; test namespaces match
|
||||
`.*-test$` under `test/` directory. You should not touch this file,
|
||||
just use it for reference.
|
||||
|
||||
|
||||
### Common (`cd common`)
|
||||
|
||||
This contains code that should compile and run under different runtimes: JVM & JS so the commands are
|
||||
separarated for each runtime.
|
||||
|
||||
```bash
|
||||
pnpm run test # Build + run node target/tests/test.js
|
||||
pnpm run watch:test # Watch mode
|
||||
pnpm run lint:clj
|
||||
pnpm run fmt:clj
|
||||
clojure -M:dev:test # Run full test suite under JVM
|
||||
clojure -M:dev:test --focus backend-tests.my-ns-test # Run single namespace under JVM
|
||||
|
||||
# Run full test suite under JS or JVM runtimes
|
||||
pnpm run test:js
|
||||
pnpm run test:jvm
|
||||
|
||||
# Run single namespace (only on JVM)
|
||||
pnpm run test:jvm --focus common-tests.my-ns-test
|
||||
|
||||
# Lint
|
||||
pnpm run lint:clj # Lint CLJ/CLJS/CLJC code
|
||||
|
||||
# Check Format
|
||||
pnpm run check-fmt:clj # Check CLJ/CLJS/CLJS code
|
||||
pnpm run check-fmt:js # Check JS/TS code
|
||||
|
||||
# Code Format (Automatic Formatting)
|
||||
pnpm run fmt:clj # Check CLJ/CLJS/CLJS code
|
||||
pnpm run fmt:js # Check JS/TS code
|
||||
```
|
||||
|
||||
To run a focused ClojureScript unit test: edit
|
||||
`test/common_tests/runner.cljs` to narrow the test suite, then `pnpm
|
||||
run build:test && node target/tests/test.js`.
|
||||
|
||||
|
||||
### Render-WASM (`cd render-wasm`)
|
||||
|
||||
```bash
|
||||
@@ -93,6 +133,10 @@ cargo fmt --check
|
||||
|
||||
### Namespace Structure
|
||||
|
||||
The backend, frontend and exporter are developed using clojure and
|
||||
clojurescript and code is organized in namespaces. This is a general
|
||||
overview of the available namespaces.
|
||||
|
||||
**Backend:**
|
||||
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
||||
- `app.http.*` – HTTP routes and middleware
|
||||
@@ -109,14 +153,26 @@ cargo fmt --check
|
||||
- `app.util.*` – Utilities (DOM, HTTP, i18n, keyboard shortcuts)
|
||||
|
||||
**Common:**
|
||||
- `app.common.types.*` – Shared data types for shapes, files, pages
|
||||
- `app.common.schema` – Malli validation schemas
|
||||
- `app.common.geom.*` – Geometry utilities
|
||||
- `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas
|
||||
- `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli
|
||||
- `app.common.geom.*` – Geometry and shape transformation helpers
|
||||
- `app.common.data` – Generic helpers used around all application
|
||||
- `app.common.math` – Generic math helpers used around all aplication
|
||||
- `app.common.json` – Generic JSON encoding/decoding helpers
|
||||
- `app.common.data.macros` – Performance macros used everywhere
|
||||
|
||||
|
||||
### Backend RPC Commands
|
||||
|
||||
All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name>`.
|
||||
The PRC methods are implement in a some kind of multimethod structure using
|
||||
`app.util.serivices` namespace. All RPC methods are collected under `app.rpc`
|
||||
namespace and exposed under `/api/rpc/command/<cmd-name>`. The RPC method
|
||||
accepts POST and GET requests indistinctly and uses `Accept` header for
|
||||
negotiate the response encoding (which can be transit, the defaut or plain
|
||||
json). It also accepts transit (defaut) or json as input, which should be
|
||||
indicated using `Content-Type` header.
|
||||
|
||||
This is an example:
|
||||
|
||||
```clojure
|
||||
(sv/defmethod ::my-command
|
||||
@@ -129,12 +185,18 @@ All API calls go through a single RPC endpoint: `POST /api/rpc/command/<cmd-name
|
||||
{:id (uuid/next)})
|
||||
```
|
||||
|
||||
Look under `src/app/rpc/commands/*.clj` to see more examples.
|
||||
|
||||
|
||||
### Frontend State Management (Potok)
|
||||
|
||||
State is a single atom managed by a Potok store. Events implement protocols:
|
||||
State is a single atom managed by a Potok store. Events implement protocols
|
||||
(funcool/potok library):
|
||||
|
||||
```clojure
|
||||
(defn my-event [data]
|
||||
(defn my-event
|
||||
"doc string"
|
||||
[data]
|
||||
(ptk/reify ::my-event
|
||||
ptk/UpdateEvent
|
||||
(update [_ state] ;; synchronous state transition
|
||||
@@ -148,19 +210,40 @@ State is a single atom managed by a Potok store. Events implement protocols:
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _] ;; pure side effects (DOM, logging)
|
||||
(.focus (dom/get-element "id")))))
|
||||
(dom/focus (dom/get-element "id")))))
|
||||
```
|
||||
|
||||
Dispatch with `(st/emit! (my-event data))`. Read state via reactive
|
||||
refs: `(deref refs/selected-shapes)`. Prefer helpers from
|
||||
`app.util.dom` instead of using direct dom calls, if no helper is
|
||||
The state is located under `app.main.store` namespace where we have
|
||||
the `emit!` function responsible of emiting events.
|
||||
|
||||
Example:
|
||||
|
||||
```cljs
|
||||
(ns some.ns
|
||||
(:require
|
||||
[app.main.data.my-events :refer [my-event]]
|
||||
[app.main.store :as st]))
|
||||
|
||||
(defn on-click
|
||||
[event]
|
||||
(st/emit! (my-event)))
|
||||
```
|
||||
|
||||
On `app.main.refs` we have reactive references which lookup into the main state
|
||||
for just inner data or precalculated data. That references are very usefull but
|
||||
should be used with care because, per example if we have complex operation, this
|
||||
operation will be executed on each state change, and sometimes is better to have
|
||||
simple references and use react `use-memo` for more granular memoization.
|
||||
|
||||
Prefer helpers from `app.util.dom` instead of using direct dom calls, if no helper is
|
||||
available, prefer adding a new helper for handling it and the use the
|
||||
new helper.
|
||||
|
||||
|
||||
### CSS Modules Pattern
|
||||
### CSS (Modules Pattern)
|
||||
|
||||
Styles are co-located with components. Each `.cljs` file has a corresponding `.scss` file:
|
||||
Styles are co-located with components. Each `.cljs` file has a corresponding
|
||||
`.scss` file:
|
||||
|
||||
```clojure
|
||||
;; In the component namespace:
|
||||
@@ -174,8 +257,24 @@ Styles are co-located with components. Each `.cljs` file has a corresponding `.s
|
||||
|
||||
;; When you need concat an existing class:
|
||||
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
|
||||
```
|
||||
|
||||
### Integration tests (Playwright)
|
||||
|
||||
Integration tests are developed under `frontend/playwright` directory, we use
|
||||
mocks for remove communication with backend.
|
||||
|
||||
The tests should be executed under `./frontend` directory:
|
||||
|
||||
```
|
||||
cd frontend/
|
||||
|
||||
pnpm run test:e2e # Playwright e2e tests
|
||||
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
|
||||
```
|
||||
|
||||
Ensure everything installed with `./scripts/setup` script.
|
||||
|
||||
|
||||
### Performance Macros (`app.common.data.macros`)
|
||||
|
||||
@@ -187,7 +286,7 @@ Always prefer these macros over their `clojure.core` equivalents — they compil
|
||||
(dm/str "a" "b" "c") ;; string concatenation
|
||||
```
|
||||
|
||||
### Shared Code (cljc)
|
||||
### Shared Code under Common (CLJC)
|
||||
|
||||
Files in `common/src/app/common/` use reader conditionals to target both runtimes:
|
||||
|
||||
@@ -196,37 +295,129 @@ Files in `common/src/app/common/` use reader conditionals to target both runtime
|
||||
:cljs (:require [cljs.core :as core]))
|
||||
```
|
||||
|
||||
Both frontend and backend depend on `common` as a local library (`penpot/common {:local/root "../common"}`).
|
||||
Both frontend and backend depend on `common` as a local library (`penpot/common
|
||||
{:local/root "../common"}`).
|
||||
|
||||
|
||||
### Component Definition (Rumext / React)
|
||||
|
||||
The codebase has several kind of components, some of them use legacy
|
||||
syntax. The current and the most recent syntax uses `*` suffix on the
|
||||
name. This indicates to the `mf/defc` macro apply concrete rules on
|
||||
how props should be treated.
|
||||
### Component Standards & Syntax (React & Rumext: mf/defc)
|
||||
|
||||
```clojure
|
||||
The codebase contains various component patterns. When creating or refactoring
|
||||
components, follow the Modern Syntax rules outlined below.
|
||||
|
||||
1. The * Suffix Convention
|
||||
|
||||
The most recent syntax uses a * suffix in the component name (e.g.,
|
||||
my-component*). This suffix signals the mf/defc macro to apply specific rules
|
||||
for props handling and destructuring and optimization.
|
||||
|
||||
2. Component Definition
|
||||
|
||||
Modern components should use the following structure:
|
||||
|
||||
```clj
|
||||
(mf/defc my-component*
|
||||
{::mf/wrap [mf/memo]} ;; React.memo
|
||||
[{:keys [name on-click]}]
|
||||
{::mf/wrap [mf/memo]} ;; Equivalent to React.memo
|
||||
[{:keys [name on-click]}] ;; Destructured props
|
||||
[:div {:class (stl/css :root)
|
||||
:on-click on-click}
|
||||
name])
|
||||
```
|
||||
|
||||
Hooks: `(mf/use-state)`, `(mf/use-effect)`, `(mf/use-memo)` – analgous to react hooks.
|
||||
3. Hooks
|
||||
|
||||
Use the mf namespace for hooks to maintain consistency with the macro's
|
||||
lifecycle management. These are analogous to standard React hooks:
|
||||
|
||||
```clj
|
||||
(mf/use-state) ;; analogous to React.useState adapted to cljs semantics
|
||||
(mf/use-effect) ;; analogous to React.useEffect
|
||||
(mf/use-memo) ;; analogous to React.useMemo
|
||||
(mf/use-fn) ;; analogous to React.useCallback
|
||||
```
|
||||
|
||||
The `mf/use-state` in difference with React.useState, returns an atom-like
|
||||
object, where you can use `swap!` or `reset!` for to perform an update and
|
||||
`deref` for get the current value.
|
||||
|
||||
You also has `mf/deref` hook (which does not follow the `use-` naming pattern)
|
||||
and it's purpose is watch (subscribe to changes) on atom or derived atom (from
|
||||
okulary) and get the current value. Is mainly used for subscribe to lenses
|
||||
defined in `app.main.refs` or (private lenses defined in namespaces).
|
||||
|
||||
Rumext also comes with improved syntax macros as alternative to `mf/use-effect`
|
||||
and `mf/use-memo` functions. Examples:
|
||||
|
||||
|
||||
The component usage should always follow the `[:> my-component*
|
||||
props]`, where props should be a map literal or symbol pointing to
|
||||
javascript props objects. The javascript props object can be created
|
||||
manually `#js {:data-foo "bar"}` or using `mf/spread-object` helper
|
||||
macro.
|
||||
Example for `mf/with-memo` macro:
|
||||
|
||||
---
|
||||
```clj
|
||||
;; Using functions
|
||||
(mf/use-effect
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit! (dd/initialize team-id))
|
||||
(fn []
|
||||
(st/emit! (dd/finalize team-id)))))
|
||||
|
||||
## Commit Guidelines
|
||||
;; The same effect but using mf/with-effect
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/initialize team-id))
|
||||
(fn []
|
||||
(st/emit! (dd/finalize team-id))))
|
||||
```
|
||||
|
||||
Example for `mf/with-memo` macro:
|
||||
|
||||
```
|
||||
;; Using functions
|
||||
(mf/use-memo
|
||||
(mf/deps projects team-id)
|
||||
(fn []
|
||||
(->> (vals projects)
|
||||
(filterv #(= team-id (:team-id %))))))
|
||||
|
||||
;; Using the macro
|
||||
(mf/with-memo [projects team-id]
|
||||
(->> (vals projects)
|
||||
(filterv #(= team-id (:team-id %)))))
|
||||
```
|
||||
|
||||
Prefer using the macros for it syntax simplicity.
|
||||
|
||||
|
||||
4. Component Usage (Hiccup Syntax)
|
||||
|
||||
When invoking a component within Hiccup, always use the [:> component* props]
|
||||
pattern.
|
||||
|
||||
Requirements for props:
|
||||
|
||||
- Must be a map literal or a symbol pointing to a JavaScript props object.
|
||||
- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro.
|
||||
|
||||
Examples:
|
||||
|
||||
```clj
|
||||
;; Using object literal (no need of #js because macro already interprets it)
|
||||
[:> my-component* {:data-foo "bar"}]
|
||||
|
||||
;; Using object literal (no need of #js because macro already interprets it)
|
||||
(let [props #js {:data-foo "bar"
|
||||
:className "myclass"}]
|
||||
[:> my-component* props])
|
||||
|
||||
;; Using the spread helper
|
||||
(let [props (mf/spread-object base-props {:extra "data"})]
|
||||
[:> my-component* props])
|
||||
```
|
||||
|
||||
4. Checklist
|
||||
|
||||
- [ ] Does the component name end with *?
|
||||
|
||||
|
||||
## Commit Format Guidelines
|
||||
|
||||
Format: `<emoji-code> <subject>`
|
||||
|
||||
@@ -263,3 +454,46 @@ applicable.
|
||||
| ⬇️ | `:arrow_down:` | Dependency downgrade |
|
||||
| 🔥 | `:fire:` | Remove files or code |
|
||||
| 🌐 | `:globe_with_meridians:` | Translations |
|
||||
|
||||
|
||||
## SCSS Rules & Migration
|
||||
|
||||
### General rules
|
||||
|
||||
- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss
|
||||
variables and get the already defined properties from `_sizes.scss`. The SCSS
|
||||
variables are allowed and still used, just prefer properties if they are
|
||||
already defined.
|
||||
- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss"
|
||||
as *; padding: px2rem(23);`.
|
||||
- Do **not** create new SCSS variables for one-off values.
|
||||
- Use physical directions with logical ones to support RTL/LTR naturally.
|
||||
- ❌ `margin-left`, `padding-right`, `left`, `right`.
|
||||
- ✅ `margin-inline-start`, `padding-inline-end`, `inset-inline-start`.
|
||||
- Always use the `use-typography` mixin from `ds/typography.scss`.
|
||||
- ✅ `@include t.use-typography("title-small");`
|
||||
- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`.
|
||||
- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or
|
||||
legacy color variables.
|
||||
- Use mixins only those defined in`ds/mixins.scss`. Avoid legacy mixins like
|
||||
`@include flexCenter;`. Write standard CSS (flex/grid) instead.
|
||||
|
||||
### Syntax & Structure
|
||||
|
||||
- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file,
|
||||
try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as
|
||||
*;` (Use `as *` to expose variables directly).
|
||||
- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors:
|
||||
- ❌ `.card { .title { ... } }`
|
||||
- ✅ `.card-title { ... }`
|
||||
- Leverage component-level CSS variables for state changes (hover/focus) instead
|
||||
of rewriting properties.
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] No references to `common/refactor/`
|
||||
- [ ] All `@import` converted to `@use` (only if refactoring)
|
||||
- [ ] Physical properties (left/right) using logical properties (inline-start/end).
|
||||
- [ ] Typography implemented via `use-typography()` mixin.
|
||||
- [ ] Hardcoded pixel values wrapped in `px2rem()`.
|
||||
- [ ] Selectors are flat (no deep nesting).
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/"
|
||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
||||
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
||||
"fmt": "cljfmt fix --parallel=true src/ test/",
|
||||
"test": "clojure -M:dev:test"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2",
|
||||
"nodemon": "^3.1.10",
|
||||
"prettier": "3.5.3",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
@@ -20,11 +21,15 @@
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel=true --lint src/",
|
||||
"lint:clj": "clj-kondo --parallel=true --lint src/",
|
||||
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||
"check-fmt:js": "prettier -c src/**/*.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:js": "prettier -c src/**/*.js -w",
|
||||
"lint": "pnpm run lint:clj",
|
||||
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -C -d 2 -w target/tests/ --exec 'node target/tests/test.js'\"",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "pnpm run build:test && node target/tests/test.js"
|
||||
"test:js": "pnpm run build:test && node target/tests/test.js",
|
||||
"test:jvm": "clojure -M:dev:test"
|
||||
}
|
||||
}
|
||||
|
||||
10
common/pnpm-lock.yaml
generated
10
common/pnpm-lock.yaml
generated
@@ -18,6 +18,9 @@ importers:
|
||||
nodemon:
|
||||
specifier: ^3.1.10
|
||||
version: 3.1.11
|
||||
prettier:
|
||||
specifier: 3.5.3
|
||||
version: 3.5.3
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
@@ -169,6 +172,11 @@ packages:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
prettier@3.5.3:
|
||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||
|
||||
@@ -405,6 +413,8 @@ snapshots:
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
|
||||
@@ -4,4 +4,5 @@ set -ex
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run test;
|
||||
pnpm run test:js;
|
||||
pnpm run test:jvm;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
goog.require("cljs.core");
|
||||
goog.provide("app.common.encoding_impl");
|
||||
|
||||
goog.scope(function() {
|
||||
goog.scope(function () {
|
||||
const core = cljs.core;
|
||||
const global = goog.global;
|
||||
const self = app.common.encoding_impl;
|
||||
@@ -28,8 +28,10 @@ goog.scope(function() {
|
||||
// Accept UUID hex format
|
||||
input = input.replace(/-/g, "");
|
||||
|
||||
if ((input.length % 2) !== 0) {
|
||||
throw new RangeError("Expected string to be an even number of characters")
|
||||
if (input.length % 2 !== 0) {
|
||||
throw new RangeError(
|
||||
"Expected string to be an even number of characters",
|
||||
);
|
||||
}
|
||||
|
||||
const view = new Uint8Array(input.length / 2);
|
||||
@@ -44,7 +46,11 @@ goog.scope(function() {
|
||||
function bufferToHex(source, isUuid) {
|
||||
if (source instanceof Uint8Array) {
|
||||
} else if (ArrayBuffer.isView(source)) {
|
||||
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
||||
source = new Uint8Array(
|
||||
source.buffer,
|
||||
source.byteOffset,
|
||||
source.byteLength,
|
||||
);
|
||||
} else if (Array.isArray(source)) {
|
||||
source = Uint8Array.from(source);
|
||||
}
|
||||
@@ -56,22 +62,28 @@ goog.scope(function() {
|
||||
const spacer = isUuid ? "-" : "";
|
||||
|
||||
let i = 0;
|
||||
return (hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] + spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] + spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] + spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] + spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]]);
|
||||
return (
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
spacer +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]] +
|
||||
hexMap[source[i++]]
|
||||
);
|
||||
}
|
||||
|
||||
self.hexToBuffer = hexToBuffer;
|
||||
@@ -87,8 +99,10 @@ goog.scope(function() {
|
||||
// for base16 (hex), base32, or base64 encoding in a standards
|
||||
// compliant manner.
|
||||
|
||||
function getBaseCodec (ALPHABET) {
|
||||
if (ALPHABET.length >= 255) { throw new TypeError("Alphabet too long"); }
|
||||
function getBaseCodec(ALPHABET) {
|
||||
if (ALPHABET.length >= 255) {
|
||||
throw new TypeError("Alphabet too long");
|
||||
}
|
||||
let BASE_MAP = new Uint8Array(256);
|
||||
for (let j = 0; j < BASE_MAP.length; j++) {
|
||||
BASE_MAP[j] = 255;
|
||||
@@ -96,22 +110,32 @@ goog.scope(function() {
|
||||
for (let i = 0; i < ALPHABET.length; i++) {
|
||||
let x = ALPHABET.charAt(i);
|
||||
let xc = x.charCodeAt(0);
|
||||
if (BASE_MAP[xc] !== 255) { throw new TypeError(x + " is ambiguous"); }
|
||||
if (BASE_MAP[xc] !== 255) {
|
||||
throw new TypeError(x + " is ambiguous");
|
||||
}
|
||||
BASE_MAP[xc] = i;
|
||||
}
|
||||
let BASE = ALPHABET.length;
|
||||
let LEADER = ALPHABET.charAt(0);
|
||||
let FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up
|
||||
let iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up
|
||||
function encode (source) {
|
||||
function encode(source) {
|
||||
if (source instanceof Uint8Array) {
|
||||
} else if (ArrayBuffer.isView(source)) {
|
||||
source = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
||||
source = new Uint8Array(
|
||||
source.buffer,
|
||||
source.byteOffset,
|
||||
source.byteLength,
|
||||
);
|
||||
} else if (Array.isArray(source)) {
|
||||
source = Uint8Array.from(source);
|
||||
}
|
||||
if (!(source instanceof Uint8Array)) { throw new TypeError("Expected Uint8Array"); }
|
||||
if (source.length === 0) { return ""; }
|
||||
if (!(source instanceof Uint8Array)) {
|
||||
throw new TypeError("Expected Uint8Array");
|
||||
}
|
||||
if (source.length === 0) {
|
||||
return "";
|
||||
}
|
||||
// Skip & count leading zeroes.
|
||||
let zeroes = 0;
|
||||
let length = 0;
|
||||
@@ -129,12 +153,18 @@ goog.scope(function() {
|
||||
let carry = source[pbegin];
|
||||
// Apply "b58 = b58 * 256 + ch".
|
||||
let i = 0;
|
||||
for (let it1 = size - 1; (carry !== 0 || i < length) && (it1 !== -1); it1--, i++) {
|
||||
for (
|
||||
let it1 = size - 1;
|
||||
(carry !== 0 || i < length) && it1 !== -1;
|
||||
it1--, i++
|
||||
) {
|
||||
carry += (256 * b58[it1]) >>> 0;
|
||||
b58[it1] = (carry % BASE) >>> 0;
|
||||
b58[it1] = carry % BASE >>> 0;
|
||||
carry = (carry / BASE) >>> 0;
|
||||
}
|
||||
if (carry !== 0) { throw new Error("Non-zero carry"); }
|
||||
if (carry !== 0) {
|
||||
throw new Error("Non-zero carry");
|
||||
}
|
||||
length = i;
|
||||
pbegin++;
|
||||
}
|
||||
@@ -145,13 +175,19 @@ goog.scope(function() {
|
||||
}
|
||||
// Translate the result into a string.
|
||||
let str = LEADER.repeat(zeroes);
|
||||
for (; it2 < size; ++it2) { str += ALPHABET.charAt(b58[it2]); }
|
||||
for (; it2 < size; ++it2) {
|
||||
str += ALPHABET.charAt(b58[it2]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function decodeUnsafe (source) {
|
||||
if (typeof source !== "string") { throw new TypeError("Expected String"); }
|
||||
if (source.length === 0) { return new Uint8Array(); }
|
||||
function decodeUnsafe(source) {
|
||||
if (typeof source !== "string") {
|
||||
throw new TypeError("Expected String");
|
||||
}
|
||||
if (source.length === 0) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
let psz = 0;
|
||||
// Skip and count leading '1's.
|
||||
let zeroes = 0;
|
||||
@@ -161,21 +197,29 @@ goog.scope(function() {
|
||||
psz++;
|
||||
}
|
||||
// Allocate enough space in big-endian base256 representation.
|
||||
let size = (((source.length - psz) * FACTOR) + 1) >>> 0; // log(58) / log(256), rounded up.
|
||||
let size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.
|
||||
let b256 = new Uint8Array(size);
|
||||
// Process the characters.
|
||||
while (source[psz]) {
|
||||
// Decode character
|
||||
let carry = BASE_MAP[source.charCodeAt(psz)];
|
||||
// Invalid character
|
||||
if (carry === 255) { return; }
|
||||
if (carry === 255) {
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
for (let it3 = size - 1; (carry !== 0 || i < length) && (it3 !== -1); it3--, i++) {
|
||||
for (
|
||||
let it3 = size - 1;
|
||||
(carry !== 0 || i < length) && it3 !== -1;
|
||||
it3--, i++
|
||||
) {
|
||||
carry += (BASE * b256[it3]) >>> 0;
|
||||
b256[it3] = (carry % 256) >>> 0;
|
||||
b256[it3] = carry % 256 >>> 0;
|
||||
carry = (carry / 256) >>> 0;
|
||||
}
|
||||
if (carry !== 0) { throw new Error("Non-zero carry"); }
|
||||
if (carry !== 0) {
|
||||
throw new Error("Non-zero carry");
|
||||
}
|
||||
length = i;
|
||||
psz++;
|
||||
}
|
||||
@@ -192,20 +236,22 @@ goog.scope(function() {
|
||||
return vch;
|
||||
}
|
||||
|
||||
function decode (string) {
|
||||
function decode(string) {
|
||||
let buffer = decodeUnsafe(string);
|
||||
if (buffer) { return buffer; }
|
||||
if (buffer) {
|
||||
return buffer;
|
||||
}
|
||||
throw new Error("Non-base" + BASE + " character");
|
||||
}
|
||||
|
||||
return {
|
||||
encode: encode,
|
||||
decodeUnsafe: decodeUnsafe,
|
||||
decode: decode
|
||||
decode: decode,
|
||||
};
|
||||
}
|
||||
// MORE bases here: https://github.com/cryptocoinjs/base-x/tree/master
|
||||
const BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const BASE62 =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
self.bufferToBase62 = getBaseCodec(BASE62).encode;
|
||||
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
goog.provide("app.common.svg.path.arc_to_bezier");
|
||||
|
||||
// https://raw.githubusercontent.com/fontello/svgpath/master/lib/a2c.js
|
||||
goog.scope(function() {
|
||||
goog.scope(function () {
|
||||
const self = app.common.svg.path.arc_to_bezier;
|
||||
|
||||
var TAU = Math.PI * 2;
|
||||
@@ -27,20 +27,23 @@ goog.scope(function() {
|
||||
// we can use simplified math (without length normalization)
|
||||
//
|
||||
function unit_vector_angle(ux, uy, vx, vy) {
|
||||
var sign = (ux * vy - uy * vx < 0) ? -1 : 1;
|
||||
var dot = ux * vx + uy * vy;
|
||||
var sign = ux * vy - uy * vx < 0 ? -1 : 1;
|
||||
var dot = ux * vx + uy * vy;
|
||||
|
||||
// Add this to work with arbitrary vectors:
|
||||
// dot /= Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
|
||||
|
||||
// rounding errors, e.g. -1.0000000000000002 can screw up this
|
||||
if (dot > 1.0) { dot = 1.0; }
|
||||
if (dot < -1.0) { dot = -1.0; }
|
||||
if (dot > 1.0) {
|
||||
dot = 1.0;
|
||||
}
|
||||
if (dot < -1.0) {
|
||||
dot = -1.0;
|
||||
}
|
||||
|
||||
return sign * Math.acos(dot);
|
||||
}
|
||||
|
||||
|
||||
// Convert from endpoint to center parameterization,
|
||||
// see http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
||||
//
|
||||
@@ -53,11 +56,11 @@ goog.scope(function() {
|
||||
// points. After that, rotate it to line up ellipse axes with coordinate
|
||||
// axes.
|
||||
//
|
||||
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
||||
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
||||
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
|
||||
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
|
||||
|
||||
var rx_sq = rx * rx;
|
||||
var ry_sq = ry * ry;
|
||||
var rx_sq = rx * rx;
|
||||
var ry_sq = ry * ry;
|
||||
var x1p_sq = x1p * x1p;
|
||||
var y1p_sq = y1p * y1p;
|
||||
|
||||
@@ -66,33 +69,33 @@ goog.scope(function() {
|
||||
// Compute coordinates of the centre of this ellipse (cx', cy')
|
||||
// in the new coordinate system.
|
||||
//
|
||||
var radicant = (rx_sq * ry_sq) - (rx_sq * y1p_sq) - (ry_sq * x1p_sq);
|
||||
var radicant = rx_sq * ry_sq - rx_sq * y1p_sq - ry_sq * x1p_sq;
|
||||
|
||||
if (radicant < 0) {
|
||||
// due to rounding errors it might be e.g. -1.3877787807814457e-17
|
||||
radicant = 0;
|
||||
}
|
||||
|
||||
radicant /= (rx_sq * y1p_sq) + (ry_sq * x1p_sq);
|
||||
radicant /= rx_sq * y1p_sq + ry_sq * x1p_sq;
|
||||
radicant = Math.sqrt(radicant) * (fa === fs ? -1 : 1);
|
||||
|
||||
var cxp = radicant * rx/ry * y1p;
|
||||
var cyp = radicant * -ry/rx * x1p;
|
||||
var cxp = ((radicant * rx) / ry) * y1p;
|
||||
var cyp = ((radicant * -ry) / rx) * x1p;
|
||||
|
||||
// Step 3.
|
||||
//
|
||||
// Transform back to get centre coordinates (cx, cy) in the original
|
||||
// coordinate system.
|
||||
//
|
||||
var cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2;
|
||||
var cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2;
|
||||
var cx = cos_phi * cxp - sin_phi * cyp + (x1 + x2) / 2;
|
||||
var cy = sin_phi * cxp + cos_phi * cyp + (y1 + y2) / 2;
|
||||
|
||||
// Step 4.
|
||||
//
|
||||
// Compute angles (theta1, delta_theta).
|
||||
//
|
||||
var v1x = (x1p - cxp) / rx;
|
||||
var v1y = (y1p - cyp) / ry;
|
||||
var v1x = (x1p - cxp) / rx;
|
||||
var v1y = (y1p - cyp) / ry;
|
||||
var v2x = (-x1p - cxp) / rx;
|
||||
var v2y = (-y1p - cyp) / ry;
|
||||
|
||||
@@ -106,7 +109,7 @@ goog.scope(function() {
|
||||
delta_theta += TAU;
|
||||
}
|
||||
|
||||
return [ cx, cy, theta1, delta_theta ];
|
||||
return [cx, cy, theta1, delta_theta];
|
||||
}
|
||||
|
||||
//
|
||||
@@ -114,24 +117,33 @@ goog.scope(function() {
|
||||
// see http://math.stackexchange.com/questions/873224
|
||||
//
|
||||
function approximate_unit_arc(theta1, delta_theta) {
|
||||
var alpha = 4/3 * Math.tan(delta_theta/4);
|
||||
var alpha = (4 / 3) * Math.tan(delta_theta / 4);
|
||||
|
||||
var x1 = Math.cos(theta1);
|
||||
var y1 = Math.sin(theta1);
|
||||
var x2 = Math.cos(theta1 + delta_theta);
|
||||
var y2 = Math.sin(theta1 + delta_theta);
|
||||
|
||||
return [ x1, y1, x1 - y1*alpha, y1 + x1*alpha, x2 + y2*alpha, y2 - x2*alpha, x2, y2 ];
|
||||
return [
|
||||
x1,
|
||||
y1,
|
||||
x1 - y1 * alpha,
|
||||
y1 + x1 * alpha,
|
||||
x2 + y2 * alpha,
|
||||
y2 - x2 * alpha,
|
||||
x2,
|
||||
y2,
|
||||
];
|
||||
}
|
||||
|
||||
function calculate_beziers(x1, y1, x2, y2, fa, fs, rx, ry, phi) {
|
||||
var sin_phi = Math.sin(phi * TAU / 360);
|
||||
var cos_phi = Math.cos(phi * TAU / 360);
|
||||
var sin_phi = Math.sin((phi * TAU) / 360);
|
||||
var cos_phi = Math.cos((phi * TAU) / 360);
|
||||
|
||||
// Make sure radii are valid
|
||||
//
|
||||
var x1p = cos_phi*(x1-x2)/2 + sin_phi*(y1-y2)/2;
|
||||
var y1p = -sin_phi*(x1-x2)/2 + cos_phi*(y1-y2)/2;
|
||||
var x1p = (cos_phi * (x1 - x2)) / 2 + (sin_phi * (y1 - y2)) / 2;
|
||||
var y1p = (-sin_phi * (x1 - x2)) / 2 + (cos_phi * (y1 - y2)) / 2;
|
||||
|
||||
// console.log("L", x1p, y1p)
|
||||
|
||||
@@ -145,7 +157,6 @@ goog.scope(function() {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// Compensate out-of-range radii
|
||||
//
|
||||
rx = Math.abs(rx);
|
||||
@@ -157,25 +168,20 @@ goog.scope(function() {
|
||||
ry *= Math.sqrt(lambda);
|
||||
}
|
||||
|
||||
|
||||
// Get center parameters (cx, cy, theta1, delta_theta)
|
||||
//
|
||||
var cc = get_arc_center(x1, y1, x2, y2, fa, fs, rx, ry, sin_phi, cos_phi);
|
||||
|
||||
|
||||
var result = [];
|
||||
var theta1 = cc[2];
|
||||
var delta_theta = cc[3];
|
||||
|
||||
|
||||
|
||||
// Split an arc to multiple segments, so each segment
|
||||
// will be less than τ/4 (= 90°)
|
||||
//
|
||||
var segments = Math.max(Math.ceil(Math.abs(delta_theta) / (TAU / 4)), 1);
|
||||
delta_theta /= segments;
|
||||
|
||||
|
||||
for (var i = 0; i < segments; i++) {
|
||||
var item = approximate_unit_arc(theta1, delta_theta);
|
||||
result.push(item);
|
||||
@@ -195,8 +201,8 @@ goog.scope(function() {
|
||||
y *= ry;
|
||||
|
||||
// rotate
|
||||
var xp = cos_phi*x - sin_phi*y;
|
||||
var yp = sin_phi*x + cos_phi*y;
|
||||
var xp = cos_phi * x - sin_phi * y;
|
||||
var yp = sin_phi * x + cos_phi * y;
|
||||
|
||||
// translate
|
||||
curve[i + 0] = xp + cc[0];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,16 +10,18 @@
|
||||
goog.require("app.common.encoding_impl");
|
||||
goog.provide("app.common.uuid_impl");
|
||||
|
||||
goog.scope(function() {
|
||||
goog.scope(function () {
|
||||
const global = goog.global;
|
||||
const encoding = app.common.encoding_impl;
|
||||
const encoding = app.common.encoding_impl;
|
||||
const self = app.common.uuid_impl;
|
||||
|
||||
const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00
|
||||
|
||||
const fill = (() => {
|
||||
if (typeof global.crypto !== "undefined" &&
|
||||
typeof global.crypto.getRandomValues !== "undefined") {
|
||||
if (
|
||||
typeof global.crypto !== "undefined" &&
|
||||
typeof global.crypto.getRandomValues !== "undefined"
|
||||
) {
|
||||
return (buf) => {
|
||||
global.crypto.getRandomValues(buf);
|
||||
return buf;
|
||||
@@ -30,7 +32,7 @@ goog.scope(function() {
|
||||
|
||||
return (buf) => {
|
||||
const bytes = randomBytes(buf.length);
|
||||
buf.set(bytes)
|
||||
buf.set(bytes);
|
||||
return buf;
|
||||
};
|
||||
} else {
|
||||
@@ -39,8 +41,10 @@ goog.scope(function() {
|
||||
|
||||
return (buf) => {
|
||||
for (let i = 0, r; i < buf.length; i++) {
|
||||
if ((i & 0x03) === 0) { r = Math.random() * 0x100000000; }
|
||||
buf[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
||||
if ((i & 0x03) === 0) {
|
||||
r = Math.random() * 0x100000000;
|
||||
}
|
||||
buf[i] = (r >>> ((i & 0x03) << 3)) & 0xff;
|
||||
}
|
||||
return buf;
|
||||
};
|
||||
@@ -50,31 +54,38 @@ goog.scope(function() {
|
||||
function toHexString(buf) {
|
||||
const hexMap = encoding.hexMap;
|
||||
let i = 0;
|
||||
return (hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] + '-' +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] + '-' +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] + '-' +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] + '-' +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]]);
|
||||
};
|
||||
return (
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
"-" +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
"-" +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
"-" +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
"-" +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]] +
|
||||
hexMap[buf[i++]]
|
||||
);
|
||||
}
|
||||
|
||||
function getBigUint64(view, byteOffset, le) {
|
||||
const a = view.getUint32(byteOffset, le);
|
||||
const b = view.getUint32(byteOffset + 4, le);
|
||||
const leMask = Number(!!le);
|
||||
const beMask = Number(!le);
|
||||
return ((BigInt(a * beMask + b * leMask) << 32n) |
|
||||
(BigInt(a * leMask + b * beMask)));
|
||||
return (
|
||||
(BigInt(a * beMask + b * leMask) << 32n) | BigInt(a * leMask + b * beMask)
|
||||
);
|
||||
}
|
||||
|
||||
function setBigUint64(view, byteOffset, value, le) {
|
||||
@@ -83,8 +94,7 @@ goog.scope(function() {
|
||||
if (le) {
|
||||
view.setUint32(byteOffset + 4, hi, le);
|
||||
view.setUint32(byteOffset, lo, le);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
view.setUint32(byteOffset, hi, le);
|
||||
view.setUint32(byteOffset + 4, lo, le);
|
||||
}
|
||||
@@ -104,17 +114,18 @@ goog.scope(function() {
|
||||
}
|
||||
|
||||
self.shortID = (function () {
|
||||
const buff = new ArrayBuffer(8);
|
||||
const buff = new ArrayBuffer(8);
|
||||
const int8 = new Uint8Array(buff);
|
||||
const view = new DataView(buff);
|
||||
const view = new DataView(buff);
|
||||
|
||||
const base = 0x0000_0000_0000_0000n;
|
||||
|
||||
return function shortID(ts) {
|
||||
const tss = currentTimestamp(timeRef);
|
||||
const msb = (base
|
||||
| (nextLong() & 0xffff_ffff_0000_0000n)
|
||||
| (tss & 0x0000_0000_ffff_ffffn));
|
||||
const msb =
|
||||
base |
|
||||
(nextLong() & 0xffff_ffff_0000_0000n) |
|
||||
(tss & 0x0000_0000_ffff_ffffn);
|
||||
setBigUint64(view, 0, msb, false);
|
||||
return encoding.toBase62(int8);
|
||||
};
|
||||
@@ -139,9 +150,9 @@ goog.scope(function() {
|
||||
const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space
|
||||
|
||||
let countCs = 0n;
|
||||
let lastRd = 0n;
|
||||
let lastCs = 0n;
|
||||
let lastTs = 0n;
|
||||
let lastRd = 0n;
|
||||
let lastCs = 0n;
|
||||
let lastTs = 0n;
|
||||
let baseMsb = 0x0000_0000_0000_8000n;
|
||||
let baseLsb = 0x8000_0000_0000_0000n;
|
||||
|
||||
@@ -149,12 +160,9 @@ goog.scope(function() {
|
||||
lastCs = nextLong() & maxCs;
|
||||
|
||||
const create = function create(ts, lastRd, lastCs) {
|
||||
const msb = (baseMsb
|
||||
| (lastRd & 0xffff_ffff_ffff_0fffn));
|
||||
const msb = baseMsb | (lastRd & 0xffff_ffff_ffff_0fffn);
|
||||
|
||||
const lsb = (baseLsb
|
||||
| ((ts << 14n) & 0x3fff_ffff_ffff_c000n)
|
||||
| lastCs);
|
||||
const lsb = baseLsb | ((ts << 14n) & 0x3fff_ffff_ffff_c000n) | lastCs;
|
||||
|
||||
setBigUint64(view, 0, msb, false);
|
||||
setBigUint64(view, 8, lsb, false);
|
||||
@@ -167,10 +175,10 @@ goog.scope(function() {
|
||||
let ts = currentTimestamp(timeRef);
|
||||
|
||||
// Protect from clock regression
|
||||
if ((ts - lastTs) < 0) {
|
||||
lastRd = (lastRd
|
||||
& 0x0000_0000_0000_0f00n
|
||||
| (nextLong() & 0xffff_ffff_ffff_f0ffn));
|
||||
if (ts - lastTs < 0) {
|
||||
lastRd =
|
||||
(lastRd & 0x0000_0000_0000_0f00n) |
|
||||
(nextLong() & 0xffff_ffff_ffff_f0ffn);
|
||||
countCs = 0n;
|
||||
continue;
|
||||
}
|
||||
@@ -209,63 +217,63 @@ goog.scope(function() {
|
||||
|
||||
// Parse ........-....-....-####-............
|
||||
int8[8] = (rest = parseInt(uuid.slice(19, 23), 16)) >>> 8;
|
||||
int8[9] = rest & 0xff,
|
||||
|
||||
// Parse ........-....-....-....-############
|
||||
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
||||
int8[10] = ((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff;
|
||||
(int8[9] = rest & 0xff),
|
||||
// Parse ........-....-....-....-############
|
||||
// (Use "/" to avoid 32-bit truncation when bit-shifting high-order bytes)
|
||||
(int8[10] =
|
||||
((rest = parseInt(uuid.slice(24, 36), 16)) / 0x10000000000) & 0xff);
|
||||
int8[11] = (rest / 0x100000000) & 0xff;
|
||||
int8[12] = (rest >>> 24) & 0xff;
|
||||
int8[13] = (rest >>> 16) & 0xff;
|
||||
int8[14] = (rest >>> 8) & 0xff;
|
||||
int8[15] = rest & 0xff;
|
||||
}
|
||||
};
|
||||
|
||||
const fromPair = (hi, lo) => {
|
||||
view.setBigInt64(0, hi);
|
||||
view.setBigInt64(8, lo);
|
||||
return encoding.bufferToHex(int8, true);
|
||||
}
|
||||
};
|
||||
|
||||
const getHi = (uuid) => {
|
||||
fillBytes(uuid);
|
||||
return view.getBigInt64(0);
|
||||
}
|
||||
};
|
||||
|
||||
const getLo = (uuid) => {
|
||||
fillBytes(uuid);
|
||||
return view.getBigInt64(8);
|
||||
}
|
||||
};
|
||||
|
||||
const getBytes = (uuid) => {
|
||||
fillBytes(uuid);
|
||||
return Int8Array.from(int8);
|
||||
}
|
||||
};
|
||||
|
||||
const getUnsignedParts = (uuid) => {
|
||||
fillBytes(uuid);
|
||||
const result = new Uint32Array(4);
|
||||
|
||||
result[0] = view.getUint32(0)
|
||||
result[0] = view.getUint32(0);
|
||||
result[1] = view.getUint32(4);
|
||||
result[2] = view.getUint32(8);
|
||||
result[3] = view.getUint32(12);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const fromUnsignedParts = (a, b, c, d) => {
|
||||
view.setUint32(0, a)
|
||||
view.setUint32(4, b)
|
||||
view.setUint32(8, c)
|
||||
view.setUint32(12, d)
|
||||
view.setUint32(0, a);
|
||||
view.setUint32(4, b);
|
||||
view.setUint32(8, c);
|
||||
view.setUint32(12, d);
|
||||
return encoding.bufferToHex(int8, true);
|
||||
}
|
||||
};
|
||||
|
||||
const fromArray = (u8data) => {
|
||||
int8.set(u8data);
|
||||
return encoding.bufferToHex(int8, true);
|
||||
}
|
||||
};
|
||||
|
||||
const setTag = (tag) => {
|
||||
tag = BigInt.asUintN(64, "" + tag);
|
||||
@@ -273,9 +281,9 @@ goog.scope(function() {
|
||||
throw new Error("illegal arguments: tag value should fit in 4bits");
|
||||
}
|
||||
|
||||
lastRd = (lastRd
|
||||
& 0xffff_ffff_ffff_f0ffn
|
||||
| ((tag << 8) & 0x0000_0000_0000_0f00n));
|
||||
lastRd =
|
||||
(lastRd & 0xffff_ffff_ffff_f0ffn) |
|
||||
((tag << 8) & 0x0000_0000_0000_0f00n);
|
||||
};
|
||||
|
||||
factory.create = create;
|
||||
@@ -290,9 +298,9 @@ goog.scope(function() {
|
||||
return factory;
|
||||
})();
|
||||
|
||||
self.shortV8 = function(uuid) {
|
||||
self.shortV8 = function (uuid) {
|
||||
const buff = encoding.hexToBuffer(uuid);
|
||||
const short = new Uint8Array(buff, 4);
|
||||
const short = new Uint8Array(buff, 4);
|
||||
return encoding.bufferToBase62(short);
|
||||
};
|
||||
|
||||
@@ -307,7 +315,7 @@ goog.scope(function() {
|
||||
return self.v8.fromPair(hi, lo);
|
||||
};
|
||||
|
||||
self.fromBytes = function(data) {
|
||||
self.fromBytes = function (data) {
|
||||
if (data instanceof Uint8Array) {
|
||||
return self.v8.fromArray(data);
|
||||
} else if (data instanceof Int8Array) {
|
||||
@@ -325,15 +333,15 @@ goog.scope(function() {
|
||||
return self.v8.getUnsignedParts(uuid);
|
||||
};
|
||||
|
||||
self.fromUnsignedParts = function(a,b,c,d) {
|
||||
return self.v8.fromUnsignedParts(a,b,c,d);
|
||||
self.fromUnsignedParts = function (a, b, c, d) {
|
||||
return self.v8.fromUnsignedParts(a, b, c, d);
|
||||
};
|
||||
|
||||
self.getHi = function (uuid) {
|
||||
return self.v8.getHi(uuid);
|
||||
}
|
||||
};
|
||||
|
||||
self.getLo = function (uuid) {
|
||||
return self.v8.getLo(uuid);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -67,8 +67,11 @@ export class WeakEqMap {
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
if (key === null || (typeof key !== 'object' && typeof key !== 'function')) {
|
||||
throw new TypeError('WeakEqMap keys must be objects (like WeakMap).');
|
||||
if (
|
||||
key === null ||
|
||||
(typeof key !== "object" && typeof key !== "function")
|
||||
) {
|
||||
throw new TypeError("WeakEqMap keys must be objects (like WeakMap).");
|
||||
}
|
||||
const hash = this._hash(key);
|
||||
const bucket = this._getBucket(hash);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#kaocha/v1
|
||||
{:tests [{:id :unit
|
||||
:test-paths ["test"]}]
|
||||
:kaocha/reporter [kaocha.report/dots]}
|
||||
{:tests [{:id :unit
|
||||
:test-paths ["test"]}]
|
||||
:kaocha/reporter [kaocha.report/dots]}
|
||||
|
||||
@@ -18,6 +18,7 @@ RUN set -ex; \
|
||||
curl \
|
||||
bash \
|
||||
git \
|
||||
ripgrep \
|
||||
\
|
||||
curl \
|
||||
ca-certificates \
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"watch": "pnpm run watch:app",
|
||||
"build:app": "clojure -M:dev:shadow-cljs release main",
|
||||
"build": "pnpm run clear:shadow-cache && pnpm run build:app",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/",
|
||||
"lint:clj": "cljfmt check --parallel src/ && clj-kondo --parallel --lint src/"
|
||||
"fmt": "cljfmt fix --parallel=true src/",
|
||||
"check-fmt": "cljfmt check --parallel=true src/",
|
||||
"lint": "clj-kondo --parallel --lint src/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,15 @@
|
||||
"build:app:main": "clojure -M:dev:shadow-cljs release main worker",
|
||||
"build:app:worker": "clojure -M:dev:shadow-cljs release worker",
|
||||
"build:app": "pnpm run clear:shadow-cache && pnpm run build:app:main && pnpm run build:app:libs",
|
||||
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
|
||||
"check-fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js",
|
||||
"check-fmt:scss": "prettier -c resources/styles -c src/**/*.scss",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||
"fmt:scss": "prettier -c resources/styles -c src/**/*.scss -w",
|
||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
||||
"lint:js": "prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
||||
"lint:scss": "prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:clj": "clj-kondo --parallel --lint ../common/src src/",
|
||||
"lint:js": "exit 0",
|
||||
"lint:scss": "exit 0",
|
||||
"build:test": "clojure -M:dev:shadow-cljs compile test",
|
||||
"test": "pnpm run build:test && node target/tests/test.js",
|
||||
"test:storybook": "vitest run --project=storybook",
|
||||
|
||||
@@ -26,8 +26,9 @@
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"build": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs release library",
|
||||
"build:bundle": "./scripts/build",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"lint:clj": "cljfmt check --parallel=false src/ test/ && clj-kondo --parallel --lint src/",
|
||||
"fmt": "cljfmt fix --parallel=true src/ test/",
|
||||
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
||||
"lint": "clj-kondo --parallel --lint src/",
|
||||
"test": "node --test",
|
||||
"watch:test": "node --test --watch",
|
||||
"watch": "pnpm run clear:shadow-cache && clojure -M:dev:shadow-cljs watch library"
|
||||
|
||||
13
scripts/check-fmt
Executable file
13
scripts/check-fmt
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cljfmt --parallel=true check \
|
||||
common/src/ \
|
||||
common/test/ \
|
||||
frontend/src/ \
|
||||
frontend/test/ \
|
||||
backend/src/ \
|
||||
backend/test/ \
|
||||
exporter/src/ \
|
||||
library/src;
|
||||
10
scripts/lint
10
scripts/lint
@@ -2,16 +2,6 @@
|
||||
|
||||
set -ex
|
||||
|
||||
cljfmt check --parallel=true \
|
||||
common/src/ \
|
||||
common/test/ \
|
||||
frontend/src/ \
|
||||
frontend/test/ \
|
||||
backend/src/ \
|
||||
backend/test/ \
|
||||
exporter/src/ \
|
||||
library/src;
|
||||
|
||||
clj-kondo --parallel=true --lint common/src;
|
||||
clj-kondo --parallel=true --lint frontend/src;
|
||||
clj-kondo --parallel=true --lint backend/src;
|
||||
|
||||
Reference in New Issue
Block a user