@ng-prism/core 22.3.0 → 22.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +13 -13
  2. package/dist/app/canvas/canvas-bg.styles.d.ts +7 -12
  3. package/dist/app/canvas/canvas-bg.styles.d.ts.map +1 -1
  4. package/dist/app/canvas/canvas-bg.styles.js +35 -37
  5. package/dist/app/canvas/measure-geometry.d.ts +140 -0
  6. package/dist/app/canvas/measure-geometry.d.ts.map +1 -0
  7. package/dist/app/canvas/measure-geometry.js +217 -0
  8. package/dist/app/canvas/measure-keyboard.d.ts +20 -0
  9. package/dist/app/canvas/measure-keyboard.d.ts.map +1 -0
  10. package/dist/app/canvas/measure-keyboard.js +34 -0
  11. package/dist/app/canvas/measure-quad.d.ts +114 -0
  12. package/dist/app/canvas/measure-quad.d.ts.map +1 -0
  13. package/dist/app/canvas/measure-quad.js +173 -0
  14. package/dist/app/canvas/measure-snap.d.ts +35 -0
  15. package/dist/app/canvas/measure-snap.d.ts.map +1 -0
  16. package/dist/app/canvas/measure-snap.js +63 -0
  17. package/dist/app/canvas/prism-canvas-measure.component.d.ts +142 -0
  18. package/dist/app/canvas/prism-canvas-measure.component.d.ts.map +1 -0
  19. package/dist/app/canvas/prism-canvas-measure.component.js +554 -0
  20. package/dist/app/canvas/prism-canvas-pin-row.component.d.ts +23 -0
  21. package/dist/app/canvas/prism-canvas-pin-row.component.d.ts.map +1 -0
  22. package/dist/app/canvas/prism-canvas-pin-row.component.js +64 -0
  23. package/dist/app/canvas/prism-canvas-toolbar.component.d.ts +22 -39
  24. package/dist/app/canvas/prism-canvas-toolbar.component.d.ts.map +1 -1
  25. package/dist/app/canvas/prism-canvas-toolbar.component.js +61 -69
  26. package/dist/app/canvas/prism-template-popover.component.js +1 -1
  27. package/dist/app/canvas/viewport-snap.d.ts +1 -1
  28. package/dist/app/canvas/viewport-snap.js +1 -1
  29. package/dist/app/component-head/head-metrics.d.ts +2 -2
  30. package/dist/app/component-head/head-metrics.js +1 -1
  31. package/dist/app/component-head/prism-component-head.component.d.ts +2 -2
  32. package/dist/app/component-head/prism-component-head.component.d.ts.map +1 -1
  33. package/dist/app/component-head/prism-component-head.component.js +11 -18
  34. package/dist/app/component-head/prism-head-gauge.component.js +1 -1
  35. package/dist/app/component-head/prism-head-info.component.d.ts +1 -1
  36. package/dist/app/component-head/prism-head-info.component.js +2 -2
  37. package/dist/app/component-head/prism-stat.component.d.ts +1 -1
  38. package/dist/app/component-head/prism-stat.component.js +2 -2
  39. package/dist/app/directives/prism-resizer.directive.d.ts +1 -1
  40. package/dist/app/directives/prism-resizer.directive.js +2 -2
  41. package/dist/app/directives/resize-value.d.ts +2 -2
  42. package/dist/app/directives/resize-value.js +2 -2
  43. package/dist/app/discovery-manifest.d.ts +2 -2
  44. package/dist/app/discovery-manifest.d.ts.map +1 -1
  45. package/dist/app/discovery-manifest.js +11 -15
  46. package/dist/app/header/prism-header.component.js +1 -1
  47. package/dist/app/icons/icon-registry.d.ts +2 -2
  48. package/dist/app/icons/icon-registry.d.ts.map +1 -1
  49. package/dist/app/icons/icon-registry.js +3 -2
  50. package/dist/app/icons/prism-icon.component.d.ts +2 -2
  51. package/dist/app/icons/prism-icon.component.js +4 -4
  52. package/dist/app/index.d.ts.map +1 -1
  53. package/dist/app/index.js +14 -21
  54. package/dist/app/overview/overview-visibility.d.ts +1 -1
  55. package/dist/app/overview/overview-visibility.js +1 -1
  56. package/dist/app/overview/prism-overview-cell.component.d.ts +2 -2
  57. package/dist/app/overview/prism-overview-cell.component.d.ts.map +1 -1
  58. package/dist/app/overview/prism-overview-cell.component.js +9 -10
  59. package/dist/app/overview/prism-overview.component.d.ts +2 -2
  60. package/dist/app/overview/prism-overview.component.js +2 -2
  61. package/dist/app/page-renderer/prism-page-renderer.component.js +1 -1
  62. package/dist/app/panels/a11y/a11y-audit.service.js +2 -2
  63. package/dist/app/panels/a11y/a11y-summary.d.ts +3 -4
  64. package/dist/app/panels/a11y/a11y-summary.d.ts.map +1 -1
  65. package/dist/app/panels/a11y/a11y-summary.js +3 -4
  66. package/dist/app/panels/a11y/a11y-thresholds.d.ts +1 -1
  67. package/dist/app/panels/a11y/a11y-thresholds.d.ts.map +1 -1
  68. package/dist/app/panels/a11y/a11y-thresholds.js +1 -1
  69. package/dist/app/panels/a11y/a11y.types.d.ts +1 -1
  70. package/dist/app/panels/a11y/a11y.types.d.ts.map +1 -1
  71. package/dist/app/panels/builtin-navigation-decorations.d.ts +1 -1
  72. package/dist/app/panels/builtin-navigation-decorations.js +1 -1
  73. package/dist/app/panels/controls/non-editable-input-types.js +1 -1
  74. package/dist/app/panels/panel-host/panel-badge.d.ts +1 -1
  75. package/dist/app/provide-prism.js +1 -1
  76. package/dist/app/renderer/known-inputs.d.ts +1 -1
  77. package/dist/app/renderer/known-inputs.js +1 -1
  78. package/dist/app/renderer/overlay-resolver.d.ts +17 -3
  79. package/dist/app/renderer/overlay-resolver.d.ts.map +1 -1
  80. package/dist/app/renderer/overlay-resolver.js +9 -4
  81. package/dist/app/renderer/prism-renderer.component.d.ts +3 -3
  82. package/dist/app/renderer/prism-renderer.component.d.ts.map +1 -1
  83. package/dist/app/renderer/prism-renderer.component.js +25 -16
  84. package/dist/app/renderer/projectable-content.js +3 -3
  85. package/dist/app/services/prism-canvas.service.d.ts +12 -1
  86. package/dist/app/services/prism-canvas.service.d.ts.map +1 -1
  87. package/dist/app/services/prism-canvas.service.js +23 -5
  88. package/dist/app/services/prism-capture.service.d.ts +24 -50
  89. package/dist/app/services/prism-capture.service.d.ts.map +1 -1
  90. package/dist/app/services/prism-capture.service.js +60 -140
  91. package/dist/app/services/prism-measure.service.d.ts +44 -0
  92. package/dist/app/services/prism-measure.service.d.ts.map +1 -0
  93. package/dist/app/services/prism-measure.service.js +91 -0
  94. package/dist/app/services/prism-panel.service.d.ts +2 -2
  95. package/dist/app/services/prism-panel.service.js +2 -2
  96. package/dist/app/services/prism-persistence.service.js +1 -1
  97. package/dist/app/services/prism-renderer.service.js +1 -1
  98. package/dist/app/services/prism-url-state.service.js +1 -1
  99. package/dist/app/services/prism-variant-bg.service.d.ts +10 -24
  100. package/dist/app/services/prism-variant-bg.service.d.ts.map +1 -1
  101. package/dist/app/services/prism-variant-bg.service.js +10 -24
  102. package/dist/app/shared/prism-metric-badge.component.d.ts +9 -15
  103. package/dist/app/shared/prism-metric-badge.component.d.ts.map +1 -1
  104. package/dist/app/shared/prism-metric-badge.component.js +10 -16
  105. package/dist/app/shell/prism-shell.component.d.ts.map +1 -1
  106. package/dist/app/shell/prism-shell.component.js +12 -9
  107. package/dist/app/sidebar/navigation-decorations.d.ts +4 -4
  108. package/dist/app/sidebar/navigation-decorations.js +2 -2
  109. package/dist/app/sidebar/prism-sidebar.component.d.ts +2 -2
  110. package/dist/app/sidebar/prism-sidebar.component.js +3 -3
  111. package/dist/app/theme/prism-default-theme.js +3 -3
  112. package/dist/app/variant-ribbon/spectrum.d.ts +1 -1
  113. package/dist/app/variant-ribbon/spectrum.js +1 -1
  114. package/dist/app/view-tab-bar/next-view-id.d.ts +1 -1
  115. package/dist/app/view-tab-bar/next-view-id.js +1 -1
  116. package/dist/builder/a11y/a11y-report-reader.d.ts +1 -1
  117. package/dist/builder/a11y/a11y-report-reader.js +1 -1
  118. package/dist/builder/manifest/runtime-manifest.generator.js +6 -2
  119. package/dist/builder/plugin-runner/plugin-runner.js +1 -1
  120. package/dist/builder/scanner/ast-utils.d.ts +46 -2
  121. package/dist/builder/scanner/ast-utils.d.ts.map +1 -1
  122. package/dist/builder/scanner/ast-utils.js +337 -31
  123. package/dist/builder/scanner/class-hierarchy.d.ts +33 -0
  124. package/dist/builder/scanner/class-hierarchy.d.ts.map +1 -0
  125. package/dist/builder/scanner/class-hierarchy.js +87 -0
  126. package/dist/builder/scanner/component.scanner.d.ts +7 -1
  127. package/dist/builder/scanner/component.scanner.d.ts.map +1 -1
  128. package/dist/builder/scanner/component.scanner.js +106 -58
  129. package/dist/builder/scanner/input.extractor.d.ts +7 -4
  130. package/dist/builder/scanner/input.extractor.d.ts.map +1 -1
  131. package/dist/builder/scanner/input.extractor.js +153 -55
  132. package/dist/builder/scanner/manifest.generator.js +6 -2
  133. package/dist/builder/scanner/scanner.d.ts +3 -1
  134. package/dist/builder/scanner/scanner.d.ts.map +1 -1
  135. package/dist/builder/scanner/scanner.js +6 -4
  136. package/dist/builder/scanner/showcase-diagnostics.d.ts +25 -0
  137. package/dist/builder/scanner/showcase-diagnostics.d.ts.map +1 -0
  138. package/dist/builder/scanner/showcase-diagnostics.js +96 -0
  139. package/dist/builder/shared/prism-pipeline.d.ts +1 -1
  140. package/dist/builder/shared/prism-pipeline.d.ts.map +1 -1
  141. package/dist/builder/shared/prism-pipeline.js +12 -14
  142. package/dist/builder/watcher/prism-watcher.js +2 -2
  143. package/dist/decorator/showcase.types.d.ts +11 -9
  144. package/dist/decorator/showcase.types.d.ts.map +1 -1
  145. package/dist/index.js +3 -3
  146. package/dist/plugin/define-config.js +1 -1
  147. package/dist/plugin/index.d.ts.map +1 -1
  148. package/dist/plugin/index.js +5 -9
  149. package/dist/plugin/plugin.types.d.ts +30 -13
  150. package/dist/plugin/plugin.types.d.ts.map +1 -1
  151. package/dist/schematics/migrations/v22-0-cache-dir-rename/index.js +2 -2
  152. package/dist/schematics/ng-add/index.js +4 -4
  153. package/dist/shared/canvas-bg.type.d.ts +5 -6
  154. package/dist/shared/canvas-bg.type.d.ts.map +1 -1
  155. package/dist/shared/canvas-bg.type.js +4 -5
  156. package/dist/shared/measure.type.d.ts +52 -0
  157. package/dist/shared/measure.type.d.ts.map +1 -0
  158. package/dist/shared/measure.type.js +21 -0
  159. package/dist/shared/variant-bg.d.ts +20 -31
  160. package/dist/shared/variant-bg.d.ts.map +1 -1
  161. package/dist/shared/variant-bg.js +20 -31
  162. package/dist/shared/viewport.type.d.ts +15 -19
  163. package/dist/shared/viewport.type.d.ts.map +1 -1
  164. package/dist/shared/viewport.type.js +15 -19
  165. package/dist/transformer/showcase-strip.transformer.js +1 -1
  166. package/package.json +5 -5
package/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # ng-prism
2
2
 
3
- **ng-prism** is the Angular-native Storybook alternative. A lightweight component showcase tool that scans your library at build time and renders interactive demos — annotate components with `@Showcase`, no separate story files needed.
3
+ **ng-prism** is the Angular-native Storybook alternative. A lightweight component showcase tool that scans your library at build time and renders interactive demos. Annotate components with `@Showcase`; no separate story files needed.
4
4
 
5
5
  [![Angular](https://img.shields.io/badge/Angular-20%20%7C%2021%20%7C%2022-dd0031)](https://angular.dev)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.5+-3178c6)](https://www.typescriptlang.org)
7
7
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
8
8
 
9
- **[Live Demo](https://dyingangel666.github.io/ng-prism/demo/?component=ButtonComponent)** · **[Documentation](https://dyingangel666.github.io/ng-prism/)**
9
+ **[Live Demo](https://ng-prism.github.io/ng-prism/demo/?component=ButtonComponent)** · **[Documentation](https://ng-prism.github.io/ng-prism/)**
10
10
 
11
11
  ## Features
12
12
 
13
- - **Zero-config discovery** — TypeScript Compiler API scans your library at build time
14
- - **Signal-native** — works with `input()` / `output()` signals
15
- - **Directive support** — showcase directives with configurable host elements
16
- - **Plugin architecture** — JSDoc, A11y, Figma, Performance, Box Model, Coverage
17
- - **Live Controls** — auto-generated input controls with type-aware editors
18
- - **Code Snippets** — live-updating Angular template snippets per variant
19
- - **Component Pages** — free-form demo pages for complex components
20
- - **Deep-linking** — URL state sync for sharing specific component/variant/view
21
- - **Themeable** — full CSS custom property system, replaceable UI sections
13
+ - **Zero-config discovery**: TypeScript Compiler API scans your library at build time
14
+ - **Signal-native**: works with `input()` / `output()` signals
15
+ - **Directive support**: showcase directives with configurable host elements
16
+ - **Plugin architecture**: JSDoc, A11y, Figma, Performance, Box Model, Coverage
17
+ - **Live Controls**: auto-generated input controls with type-aware editors
18
+ - **Code Snippets**: live-updating Angular template snippets per variant
19
+ - **Component Pages**: free-form demo pages for complex components
20
+ - **Deep-linking**: URL state sync for sharing specific component/variant/view
21
+ - **Themeable**: full CSS custom property system, replaceable UI sections
22
22
 
23
23
  ## Quick Start
24
24
 
@@ -70,7 +70,7 @@ This creates the prism app project, configures Angular builders, and generates `
70
70
  ng run my-lib:prism
71
71
  ```
72
72
 
73
- Open `http://localhost:4400` — your component appears in the sidebar with live controls, code snippets, and variant tabs.
73
+ Open `http://localhost:4400`. Your component appears in the sidebar with live controls, code snippets, and variant tabs.
74
74
 
75
75
  ## Configuration
76
76
 
@@ -142,7 +142,7 @@ Link to a `@Showcase`-decorated component for combined API docs + custom renderi
142
142
  | Coverage | `@ng-prism/plugin-coverage` | Per-component test coverage from Istanbul/v8 |
143
143
  | VRT | `@ng-prism/plugin-visual-regression` | Per-variant visual regression report from a screenshot runner |
144
144
 
145
- > **Note:** Accessibility auditing (axe-core) is built into ng-prism core — no plugin needed.
145
+ > **Note:** Accessibility auditing (axe-core) is built into ng-prism core, so no plugin is needed.
146
146
 
147
147
  ## Requirements
148
148
 
@@ -2,18 +2,13 @@
2
2
  * What each `CanvasBg` value paints, shared by the Playground stage
3
3
  * (`prism-renderer`) and the Overview cells (`prism-overview-cell`).
4
4
  *
5
- * Append this as the **last** entry of a component's `styles` array. The
6
- * selectors are bare attribute selectors, so after Angular's encapsulation
7
- * rewrite they carry the same specificity as the `.some-stage` class rule they
8
- * have to override — source order is what decides, and only last place gets it
9
- * right.
5
+ * Must be the last entry of a component's `styles` array. After encapsulation
6
+ * these attribute selectors have the same specificity as the `.some-stage`
7
+ * class rule they override, so source order decides.
10
8
  *
11
- * The consuming element supplies its own `background-color`; `light` and `dark`
12
- * override it, the rest leave it alone.
13
- *
14
- * Capture mode is unaffected by where these rules live: `CAPTURE_STYLES` in
15
- * `prism-capture.service.ts` is a global stylesheet using `!important`, which
16
- * beats any component-scoped rule regardless.
9
+ * The consuming element supplies its own `background-color`; only `light` and
10
+ * `dark` override it. Capture mode is unaffected: `CAPTURE_STYLES` is a global
11
+ * stylesheet using `!important`.
17
12
  */
18
- export declare const CANVAS_BG_STYLES = "\n [data-bg=\"dots\"] {\n background-image: radial-gradient(circle, var(--prism-dot) 1px, transparent 1px);\n background-size: 20px 20px;\n }\n\n [data-bg=\"plain\"] {\n background-image: none;\n }\n\n /* Flat, not dotted. \"light\" and \"dark\" name a surface a component was\n designed against \u2014 and they are the two backgrounds whose colour is\n absolute rather than a theme token, which is what makes them the values\n to declare for a screenshot baseline. \"dots\" already exists for anyone\n who wants the grid. */\n /* These two also re-point --prism-measure, and that is not decoration.\n Every other canvas overlay is drawn in a theme colour over a theme\n surface, so the two move together. These two backgrounds are absolute:\n \"light\" stays near-white while the app runs the dark theme, and \"dark\"\n stays near-black while it runs the light one. The measurement colour\n picked for one theme is then sitting on the other theme's ground \u2014\n measured, the dark theme's blue reaches 1.8:1 on the light background,\n which is a viewport overlay you cannot see. Pinning the colour to the\n ground rather than to the theme is what makes \"always visible\" true\n instead of true-in-the-common-case.\n\n Literal values because this file is theme-independent by construction \u2014\n it is the one place that already hard-codes the two absolute grounds\n below, for the same reason. Keep them in step with --prism-measure in\n prism-default-theme.ts; semantic-colours.spec.ts fails if they drift. */\n [data-bg=\"light\"] {\n background-color: var(--prism-void-light, #f7f5fc);\n background-image: none;\n --prism-measure: #004d8a;\n }\n [data-bg=\"dark\"] {\n background-color: var(--prism-void-dark, #07050f);\n background-image: none;\n --prism-measure: #41bcff;\n }\n\n /* \"transparent\" shares the checkerboard on purpose. The two say the same\n thing in the two media the canvas has: while browsing, the checkerboard\n is already the UI's word for \"no surface here\"; in a capture it becomes\n literal transparency. A stage that were really see-through in the app\n would just show the shell through the canvas, which means nothing. The\n split between the two lives entirely in CAPTURE_STYLES. */\n [data-bg=\"checker\"],\n [data-bg=\"transparent\"] {\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 16px 16px;\n background-position: 0 0, 0 8px, 8px -8px, -8px 0;\n }\n";
13
+ export declare const CANVAS_BG_STYLES = "\n [data-bg=\"dots\"] {\n background-image: radial-gradient(circle, var(--prism-dot) 1px, transparent 1px);\n background-size: 20px 20px;\n }\n\n [data-bg=\"plain\"] {\n background-image: none;\n }\n\n /* Flat, not dotted. \"light\" and \"dark\" name the surface a component was\n designed against, and their colour is absolute rather than a theme token,\n which makes them the values to declare for a screenshot baseline. Use\n \"dots\" for the grid. */\n /* These two also re-point --prism-measure. Other overlays use theme colours\n on theme surfaces, but these grounds ignore the theme: \"light\" stays\n near-white in the dark theme and \"dark\" stays near-black in the light one.\n The dark theme's magenta reaches only 2.85:1 on the light ground, under the\n 4.5:1 its 9px readout needs, so the measurement colour has to follow the\n ground, not the theme.\n\n --prism-measure-plate follows it for the same reason. It is the surface\n the measuring tool's value label paints itself on, so the label reads as a\n gap in the measurement line rather than a badge on top of it. At its\n --prism-stage fallback it would be the theme's stage under a re-pointed\n foreground: the light theme's magenta on the dark theme's stage reads\n 3.71:1, the dark theme's on white 3.08:1. Pinned to the ground the same way,\n the pairs reach 4.54:1 and 6.57:1. A color-mix against the ground would\n work too, but this file already states both grounds literally, and one\n mechanism per decision is enough.\n\n Literal values because this file is theme-independent, like the two\n grounds below. Keep them in step with --prism-measure in\n prism-default-theme.ts; semantic-colours.spec.ts fails if they drift. */\n [data-bg=\"light\"] {\n background-color: var(--prism-void-light, #f7f5fc);\n background-image: none;\n --prism-measure: #ca00c1;\n --prism-measure-plate: var(--prism-void-light, #f7f5fc);\n }\n [data-bg=\"dark\"] {\n background-color: var(--prism-void-dark, #07050f);\n background-image: none;\n --prism-measure: #f632ff;\n --prism-measure-plate: var(--prism-void-dark, #07050f);\n }\n\n /* \"transparent\" shares the checkerboard. In the app the checkerboard is\n already the UI's sign for \"no surface\", and a see-through stage would only\n show the shell behind it. Only CAPTURE_STYLES tells the two apart. */\n [data-bg=\"checker\"],\n [data-bg=\"transparent\"] {\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 16px 16px;\n background-position: 0 0, 0 8px, 8px -8px, -8px 0;\n }\n";
19
14
  //# sourceMappingURL=canvas-bg.styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"canvas-bg.styles.d.ts","sourceRoot":"","sources":["../../../src/app/canvas/canvas-bg.styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,gBAAgB,wwFAyD5B,CAAC"}
1
+ {"version":3,"file":"canvas-bg.styles.d.ts","sourceRoot":"","sources":["../../../src/app/canvas/canvas-bg.styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,62FA4D5B,CAAC"}
@@ -2,18 +2,13 @@
2
2
  * What each `CanvasBg` value paints, shared by the Playground stage
3
3
  * (`prism-renderer`) and the Overview cells (`prism-overview-cell`).
4
4
  *
5
- * Append this as the **last** entry of a component's `styles` array. The
6
- * selectors are bare attribute selectors, so after Angular's encapsulation
7
- * rewrite they carry the same specificity as the `.some-stage` class rule they
8
- * have to override — source order is what decides, and only last place gets it
9
- * right.
5
+ * Must be the last entry of a component's `styles` array. After encapsulation
6
+ * these attribute selectors have the same specificity as the `.some-stage`
7
+ * class rule they override, so source order decides.
10
8
  *
11
- * The consuming element supplies its own `background-color`; `light` and `dark`
12
- * override it, the rest leave it alone.
13
- *
14
- * Capture mode is unaffected by where these rules live: `CAPTURE_STYLES` in
15
- * `prism-capture.service.ts` is a global stylesheet using `!important`, which
16
- * beats any component-scoped rule regardless.
9
+ * The consuming element supplies its own `background-color`; only `light` and
10
+ * `dark` override it. Capture mode is unaffected: `CAPTURE_STYLES` is a global
11
+ * stylesheet using `!important`.
17
12
  */
18
13
  export const CANVAS_BG_STYLES = `
19
14
  [data-bg="dots"] {
@@ -25,43 +20,46 @@ export const CANVAS_BG_STYLES = `
25
20
  background-image: none;
26
21
  }
27
22
 
28
- /* Flat, not dotted. "light" and "dark" name a surface a component was
29
- designed against — and they are the two backgrounds whose colour is
30
- absolute rather than a theme token, which is what makes them the values
31
- to declare for a screenshot baseline. "dots" already exists for anyone
32
- who wants the grid. */
33
- /* These two also re-point --prism-measure, and that is not decoration.
34
- Every other canvas overlay is drawn in a theme colour over a theme
35
- surface, so the two move together. These two backgrounds are absolute:
36
- "light" stays near-white while the app runs the dark theme, and "dark"
37
- stays near-black while it runs the light one. The measurement colour
38
- picked for one theme is then sitting on the other theme's ground —
39
- measured, the dark theme's blue reaches 1.8:1 on the light background,
40
- which is a viewport overlay you cannot see. Pinning the colour to the
41
- ground rather than to the theme is what makes "always visible" true
42
- instead of true-in-the-common-case.
23
+ /* Flat, not dotted. "light" and "dark" name the surface a component was
24
+ designed against, and their colour is absolute rather than a theme token,
25
+ which makes them the values to declare for a screenshot baseline. Use
26
+ "dots" for the grid. */
27
+ /* These two also re-point --prism-measure. Other overlays use theme colours
28
+ on theme surfaces, but these grounds ignore the theme: "light" stays
29
+ near-white in the dark theme and "dark" stays near-black in the light one.
30
+ The dark theme's magenta reaches only 2.85:1 on the light ground, under the
31
+ 4.5:1 its 9px readout needs, so the measurement colour has to follow the
32
+ ground, not the theme.
33
+
34
+ --prism-measure-plate follows it for the same reason. It is the surface
35
+ the measuring tool's value label paints itself on, so the label reads as a
36
+ gap in the measurement line rather than a badge on top of it. At its
37
+ --prism-stage fallback it would be the theme's stage under a re-pointed
38
+ foreground: the light theme's magenta on the dark theme's stage reads
39
+ 3.71:1, the dark theme's on white 3.08:1. Pinned to the ground the same way,
40
+ the pairs reach 4.54:1 and 6.57:1. A color-mix against the ground would
41
+ work too, but this file already states both grounds literally, and one
42
+ mechanism per decision is enough.
43
43
 
44
- Literal values because this file is theme-independent by construction —
45
- it is the one place that already hard-codes the two absolute grounds
46
- below, for the same reason. Keep them in step with --prism-measure in
44
+ Literal values because this file is theme-independent, like the two
45
+ grounds below. Keep them in step with --prism-measure in
47
46
  prism-default-theme.ts; semantic-colours.spec.ts fails if they drift. */
48
47
  [data-bg="light"] {
49
48
  background-color: var(--prism-void-light, #f7f5fc);
50
49
  background-image: none;
51
- --prism-measure: #004d8a;
50
+ --prism-measure: #ca00c1;
51
+ --prism-measure-plate: var(--prism-void-light, #f7f5fc);
52
52
  }
53
53
  [data-bg="dark"] {
54
54
  background-color: var(--prism-void-dark, #07050f);
55
55
  background-image: none;
56
- --prism-measure: #41bcff;
56
+ --prism-measure: #f632ff;
57
+ --prism-measure-plate: var(--prism-void-dark, #07050f);
57
58
  }
58
59
 
59
- /* "transparent" shares the checkerboard on purpose. The two say the same
60
- thing in the two media the canvas has: while browsing, the checkerboard
61
- is already the UI's word for "no surface here"; in a capture it becomes
62
- literal transparency. A stage that were really see-through in the app
63
- would just show the shell through the canvas, which means nothing. The
64
- split between the two lives entirely in CAPTURE_STYLES. */
60
+ /* "transparent" shares the checkerboard. In the app the checkerboard is
61
+ already the UI's sign for "no surface", and a see-through stage would only
62
+ show the shell behind it. Only CAPTURE_STYLES tells the two apart. */
65
63
  [data-bg="checker"],
66
64
  [data-bg="transparent"] {
67
65
  background-image:
@@ -0,0 +1,140 @@
1
+ import { type MeasurePoint } from '../../shared/measure.type.js';
2
+ /**
3
+ * A measured value as text, without a unit.
4
+ *
5
+ * The caller appends the unit: the primary measurement carries `px` like the
6
+ * viewport dimension line does, the four-sided readout does not, because
7
+ * four `px` in a small space is noise.
8
+ */
9
+ export declare function formatMeasure(cssPx: number): string;
10
+ export interface Vec {
11
+ x: number;
12
+ y: number;
13
+ }
14
+ /** A measurement, reduced to exactly what the template draws. */
15
+ export interface RenderedLine {
16
+ a: Vec;
17
+ b: Vec;
18
+ tickA: [Vec, Vec];
19
+ tickB: [Vec, Vec];
20
+ label: Vec;
21
+ text: string;
22
+ pinned: boolean;
23
+ /**
24
+ * The snapped-to edges, retraced as a dotted line (Spec §4.4).
25
+ *
26
+ * Without them you can see *that* the tool latched, but not *onto what*
27
+ * — and with a 2px border, the border, padding and content edges sit
28
+ * exactly 2 pixels apart. Which one was meant is otherwise impossible to
29
+ * tell. Always empty for the alt-hover quad readout: a span between two
30
+ * elements has no single snapped-to edge to retrace.
31
+ */
32
+ echoes: Array<[Vec, Vec]>;
33
+ }
34
+ /**
35
+ * A zoom that is never 0.
36
+ *
37
+ * The same guard the renderer template gives the viewport grips with
38
+ * `canvasService.zoom() || 1`. A division by zero here produces no visible
39
+ * problem but an invisible one: `NaN` in an SVG attribute is silently
40
+ * discarded, and the measurement simply vanishes.
41
+ *
42
+ * Exported rather than private because `measure-quad.ts` divides by zoom
43
+ * too. It used to carry its own `|| 1`, which let a negative zoom straight
44
+ * through and mirrored the measurement instead of dropping it — two guards
45
+ * that disagree are worse than one, so there is only one.
46
+ */
47
+ export declare function safeZoom(zoom: number): number;
48
+ /** Screen point -> `.demo-wrap`-local CSS pixels. */
49
+ export declare function toLocal(screen: Vec, origin: Vec, zoom: number): Vec;
50
+ /** The reverse of {@link toLocal}. */
51
+ export declare function toScreen(local: Vec, origin: Vec, zoom: number): Vec;
52
+ /**
53
+ * Viewport coordinates -> the overlay host's own box.
54
+ *
55
+ * Every rect the measuring tool reads — `.demo-wrap`'s for the projection
56
+ * origin, the specimen's for the label reference point, an element's for the
57
+ * four-sided readout — comes out of `getBoundingClientRect()` and is
58
+ * therefore measured from the top-left corner of the *window*. Nothing the
59
+ * overlay draws lives in that space: the SVG is `inset: 0` on the host, so
60
+ * its user coordinates start at the host's top-left corner, and `.m-label`
61
+ * is placed with `left`/`top` against the same corner. The shell puts a
62
+ * header above the stage and a sidebar beside it, so handing a viewport
63
+ * coordinate straight to the template pushes every line, tick, echo and
64
+ * label down and to the right by the stage's screen offset — largely off
65
+ * the visible area, while the stored numbers stay perfectly correct.
66
+ *
67
+ * Pure, and taking the host rect rather than reading one, because jsdom has
68
+ * no layout engine: every rect it reports is zero, so a test routed through
69
+ * the DOM could not tell this subtraction from its absence. The same seam
70
+ * `watchGeometry` opens for `ResizeObserver` in the component.
71
+ */
72
+ export declare function toHostSpace(point: Vec, hostRect: {
73
+ left: number;
74
+ top: number;
75
+ }): Vec;
76
+ export declare function measureDistance(a: Vec, b: Vec): number;
77
+ /** The two ends of the tick at point `at`, perpendicular to the line. */
78
+ export declare function tickEndpoints(a: Vec, b: Vec, at: 'a' | 'b'): [Vec, Vec];
79
+ /**
80
+ * Where the value sits.
81
+ *
82
+ * At the midpoint, as long as the line is long enough. Below that,
83
+ * perpendicular to the line, on the side that points away from `awayFrom` —
84
+ * otherwise the number lands directly on what it describes for short spans
85
+ * inside a single component.
86
+ */
87
+ export declare function labelPlacement(a: Vec, b: Vec, awayFrom: Vec): Vec;
88
+ /**
89
+ * `point`, pulled onto whichever of the eight 45-degree directions from `a` it
90
+ * lies nearest.
91
+ *
92
+ * Held while dragging, this is what makes an exactly horizontal, vertical or
93
+ * diagonal measurement reachable by hand — the same service Shift performs in
94
+ * Photoshop, Illustrator and Figma, which is where the reach for that key
95
+ * comes from.
96
+ *
97
+ * The projection is orthogonal rather than length-preserving: the component
98
+ * across the chosen direction is dropped, not folded into the result. A drag
99
+ * 240 across and 7 down therefore reports 240, which is the distance along the
100
+ * line actually drawn. Preserving the raw length instead would print the
101
+ * hypotenuse beside a line that is not the hypotenuse.
102
+ *
103
+ * Order matters at the call site. Edge snapping runs first and this runs on
104
+ * its result, so the snap's contribution on the preserved axis survives: a
105
+ * point latched to a left edge at x=338 and then forced horizontal stays at
106
+ * x=338. Constraining first and snapping afterwards would bend the line back
107
+ * off the axis the constraint was asked for.
108
+ */
109
+ export declare function constrainDirection(a: Vec, point: MeasurePoint): MeasurePoint;
110
+ /**
111
+ * `point` with every axis the document did not pin rounded to a whole CSS
112
+ * pixel.
113
+ *
114
+ * The decimals a free measurement shows are not sub-pixel geometry — they are
115
+ * the specimen's own offset leaking into the reading. `.demo-wrap` is centred
116
+ * in the stage, so its left edge sits at something like 824.3594, and a point
117
+ * that latched onto nothing is the cursor minus exactly that. The fraction
118
+ * therefore describes where the specimen happens to sit, which is of no
119
+ * interest, and it appears on practically every free drag.
120
+ *
121
+ * Rounding per axis rather than per point is what keeps this from throwing
122
+ * away the fractions that *are* real. `pointAt` only ever pins one axis: a
123
+ * point latched to a left edge takes that edge's exact x while its y is still
124
+ * the raw cursor carrying the same offset. Measured against the shipped
125
+ * fixtures, a fifth of the distances between a parent and its child are
126
+ * genuinely fractional — a toggle knob centred at 2.5px in its track, an
127
+ * avatar's glyph sitting 6.8281 from one side and 6.8438 from the other.
128
+ * Rounding those away would hide the asymmetry a ruler exists to reveal.
129
+ *
130
+ * In CSS pixels, not screen pixels: at 200% zoom one whole CSS pixel is two
131
+ * screen pixels, and a whole pixel should mean what the readout prints.
132
+ *
133
+ * Deliberately not applied after {@link constrainDirection}. Projecting onto a
134
+ * diagonal multiplies by √2/2, so a constrained endpoint is fractional by
135
+ * construction and the length between two whole points on a 45-degree line is
136
+ * irrational anyway. Rounding there would trade the exact angle Shift just
137
+ * promised for a tidier number.
138
+ */
139
+ export declare function quantizeFree(point: MeasurePoint): MeasurePoint;
140
+ //# sourceMappingURL=measure-geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure-geometry.d.ts","sourceRoot":"","sources":["../../../src/app/canvas/measure-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqE,KAAK,YAAY,EAAgB,MAAM,8BAA8B,CAAC;AAElJ;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAInD;AAED,MAAM,WAAW,GAAG;IAChB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACb;AAED,iEAAiE;AACjE,MAAM,WAAW,YAAY;IACzB,CAAC,EAAE,GAAG,CAAC;IACP,CAAC,EAAE,GAAG,CAAC;IACP,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAClB,KAAK,EAAE,GAAG,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB;;;;;;;;OAQG;IACH,MAAM,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,qDAAqD;AACrD,wBAAgB,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAInE;AAED,sCAAsC;AACtC,wBAAgB,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAInE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,GAAG,CAEpF;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,MAAM,CAEtD;AAiBD,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,GAAG,GAAG,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CASvE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,GAAG,GAAG,CASjE;AA2CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,YAAY,GAAG,YAAY,CAa5E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAU9D"}
@@ -0,0 +1,217 @@
1
+ import { MEASURE_LABEL_MIN_SPAN, MEASURE_LABEL_OFFSET, MEASURE_TICK_LENGTH } from '../../shared/measure.type.js';
2
+ /**
3
+ * A measured value as text, without a unit.
4
+ *
5
+ * The caller appends the unit: the primary measurement carries `px` like the
6
+ * viewport dimension line does, the four-sided readout does not, because
7
+ * four `px` in a small space is noise.
8
+ */
9
+ export function formatMeasure(cssPx) {
10
+ const rounded = Math.round(cssPx * 10) / 10;
11
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
12
+ }
13
+ /**
14
+ * A zoom that is never 0.
15
+ *
16
+ * The same guard the renderer template gives the viewport grips with
17
+ * `canvasService.zoom() || 1`. A division by zero here produces no visible
18
+ * problem but an invisible one: `NaN` in an SVG attribute is silently
19
+ * discarded, and the measurement simply vanishes.
20
+ *
21
+ * Exported rather than private because `measure-quad.ts` divides by zoom
22
+ * too. It used to carry its own `|| 1`, which let a negative zoom straight
23
+ * through and mirrored the measurement instead of dropping it — two guards
24
+ * that disagree are worse than one, so there is only one.
25
+ */
26
+ export function safeZoom(zoom) {
27
+ return zoom > 0 && Number.isFinite(zoom) ? zoom : 1;
28
+ }
29
+ /** Screen point -> `.demo-wrap`-local CSS pixels. */
30
+ export function toLocal(screen, origin, zoom) {
31
+ const z = safeZoom(zoom);
32
+ return { x: (screen.x - origin.x) / z, y: (screen.y - origin.y) / z };
33
+ }
34
+ /** The reverse of {@link toLocal}. */
35
+ export function toScreen(local, origin, zoom) {
36
+ const z = safeZoom(zoom);
37
+ return { x: origin.x + local.x * z, y: origin.y + local.y * z };
38
+ }
39
+ /**
40
+ * Viewport coordinates -> the overlay host's own box.
41
+ *
42
+ * Every rect the measuring tool reads — `.demo-wrap`'s for the projection
43
+ * origin, the specimen's for the label reference point, an element's for the
44
+ * four-sided readout — comes out of `getBoundingClientRect()` and is
45
+ * therefore measured from the top-left corner of the *window*. Nothing the
46
+ * overlay draws lives in that space: the SVG is `inset: 0` on the host, so
47
+ * its user coordinates start at the host's top-left corner, and `.m-label`
48
+ * is placed with `left`/`top` against the same corner. The shell puts a
49
+ * header above the stage and a sidebar beside it, so handing a viewport
50
+ * coordinate straight to the template pushes every line, tick, echo and
51
+ * label down and to the right by the stage's screen offset — largely off
52
+ * the visible area, while the stored numbers stay perfectly correct.
53
+ *
54
+ * Pure, and taking the host rect rather than reading one, because jsdom has
55
+ * no layout engine: every rect it reports is zero, so a test routed through
56
+ * the DOM could not tell this subtraction from its absence. The same seam
57
+ * `watchGeometry` opens for `ResizeObserver` in the component.
58
+ */
59
+ export function toHostSpace(point, hostRect) {
60
+ return { x: point.x - hostRect.left, y: point.y - hostRect.top };
61
+ }
62
+ export function measureDistance(a, b) {
63
+ return Math.hypot(b.x - a.x, b.y - a.y);
64
+ }
65
+ /**
66
+ * Direction and normal of the line.
67
+ *
68
+ * The length falls to 0 for two identical points; the normal is then set to
69
+ * horizontal so the ticks have a defined direction instead of `NaN`.
70
+ */
71
+ function basis(a, b) {
72
+ const dx = b.x - a.x;
73
+ const dy = b.y - a.y;
74
+ const len = Math.hypot(dx, dy);
75
+ if (len === 0)
76
+ return { nx: 0, ny: 1, len: 0 };
77
+ return { nx: -dy / len, ny: dx / len, len };
78
+ }
79
+ /** The two ends of the tick at point `at`, perpendicular to the line. */
80
+ export function tickEndpoints(a, b, at) {
81
+ const { nx, ny } = basis(a, b);
82
+ const p = at === 'a' ? a : b;
83
+ const h = MEASURE_TICK_LENGTH / 2;
84
+ return [
85
+ { x: p.x - nx * h, y: p.y - ny * h },
86
+ { x: p.x + nx * h, y: p.y + ny * h }
87
+ ];
88
+ }
89
+ /**
90
+ * Where the value sits.
91
+ *
92
+ * At the midpoint, as long as the line is long enough. Below that,
93
+ * perpendicular to the line, on the side that points away from `awayFrom` —
94
+ * otherwise the number lands directly on what it describes for short spans
95
+ * inside a single component.
96
+ */
97
+ export function labelPlacement(a, b, awayFrom) {
98
+ const { nx, ny, len } = basis(a, b);
99
+ const mid = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
100
+ if (len >= MEASURE_LABEL_MIN_SPAN)
101
+ return mid;
102
+ const sign = (awayFrom.x - mid.x) * nx + (awayFrom.y - mid.y) * ny > 0 ? -1 : 1;
103
+ return { x: mid.x + nx * MEASURE_LABEL_OFFSET * sign, y: mid.y + ny * MEASURE_LABEL_OFFSET * sign };
104
+ }
105
+ /**
106
+ * The eight directions a constrained measurement may take, as exact unit
107
+ * vectors.
108
+ *
109
+ * Written out rather than computed from `Math.cos`/`Math.sin`, because
110
+ * `Math.cos(Math.PI / 2)` is 6.1e-17 and not zero. That residue would leave a
111
+ * "vertical" measurement a hair off vertical — invisible on screen, but it
112
+ * would make the guarantee this function exists to provide quietly false, and
113
+ * every test of it would have to be written in tolerances instead of equality.
114
+ */
115
+ const DIRECTIONS = [
116
+ { x: 1, y: 0 },
117
+ { x: Math.SQRT1_2, y: Math.SQRT1_2 },
118
+ { x: 0, y: 1 },
119
+ { x: -Math.SQRT1_2, y: Math.SQRT1_2 },
120
+ { x: -1, y: 0 },
121
+ { x: -Math.SQRT1_2, y: -Math.SQRT1_2 },
122
+ { x: 0, y: -1 },
123
+ { x: Math.SQRT1_2, y: -Math.SQRT1_2 }
124
+ ];
125
+ /**
126
+ * The snap reference `point` may keep once it has been projected along `u`.
127
+ *
128
+ * A constraint forces one coordinate and preserves the other, so a latched
129
+ * edge survives only when it lies on the preserved axis: forcing `y` keeps a
130
+ * `left`/`right` edge, forcing `x` keeps a `top`/`bottom` one. A diagonal
131
+ * moves the point on both axes at once and leaves it on no edge at all.
132
+ *
133
+ * Dropping it matters because the reference is what draws the dotted snap
134
+ * echo. Carried over unchecked, the echo would mark an edge the point has
135
+ * just been moved off — the tool would claim a precision it no longer has.
136
+ */
137
+ function keptSnap(snap, u) {
138
+ if (!snap)
139
+ return null;
140
+ if (u.y === 0)
141
+ return snap.side === 'left' || snap.side === 'right' ? snap : null;
142
+ if (u.x === 0)
143
+ return snap.side === 'top' || snap.side === 'bottom' ? snap : null;
144
+ return null;
145
+ }
146
+ /**
147
+ * `point`, pulled onto whichever of the eight 45-degree directions from `a` it
148
+ * lies nearest.
149
+ *
150
+ * Held while dragging, this is what makes an exactly horizontal, vertical or
151
+ * diagonal measurement reachable by hand — the same service Shift performs in
152
+ * Photoshop, Illustrator and Figma, which is where the reach for that key
153
+ * comes from.
154
+ *
155
+ * The projection is orthogonal rather than length-preserving: the component
156
+ * across the chosen direction is dropped, not folded into the result. A drag
157
+ * 240 across and 7 down therefore reports 240, which is the distance along the
158
+ * line actually drawn. Preserving the raw length instead would print the
159
+ * hypotenuse beside a line that is not the hypotenuse.
160
+ *
161
+ * Order matters at the call site. Edge snapping runs first and this runs on
162
+ * its result, so the snap's contribution on the preserved axis survives: a
163
+ * point latched to a left edge at x=338 and then forced horizontal stays at
164
+ * x=338. Constraining first and snapping afterwards would bend the line back
165
+ * off the axis the constraint was asked for.
166
+ */
167
+ export function constrainDirection(a, point) {
168
+ const dx = point.x - a.x;
169
+ const dy = point.y - a.y;
170
+ // `atan2(0, 0)` is 0, which would silently pin a not-yet-moved drag to the
171
+ // horizontal. There is no direction to snap to yet.
172
+ if (dx === 0 && dy === 0)
173
+ return point;
174
+ const octant = (Math.round(Math.atan2(dy, dx) / (Math.PI / 4)) + 8) % 8;
175
+ const u = DIRECTIONS[octant];
176
+ const along = dx * u.x + dy * u.y;
177
+ return { x: a.x + along * u.x, y: a.y + along * u.y, snap: keptSnap(point.snap, u) };
178
+ }
179
+ /**
180
+ * `point` with every axis the document did not pin rounded to a whole CSS
181
+ * pixel.
182
+ *
183
+ * The decimals a free measurement shows are not sub-pixel geometry — they are
184
+ * the specimen's own offset leaking into the reading. `.demo-wrap` is centred
185
+ * in the stage, so its left edge sits at something like 824.3594, and a point
186
+ * that latched onto nothing is the cursor minus exactly that. The fraction
187
+ * therefore describes where the specimen happens to sit, which is of no
188
+ * interest, and it appears on practically every free drag.
189
+ *
190
+ * Rounding per axis rather than per point is what keeps this from throwing
191
+ * away the fractions that *are* real. `pointAt` only ever pins one axis: a
192
+ * point latched to a left edge takes that edge's exact x while its y is still
193
+ * the raw cursor carrying the same offset. Measured against the shipped
194
+ * fixtures, a fifth of the distances between a parent and its child are
195
+ * genuinely fractional — a toggle knob centred at 2.5px in its track, an
196
+ * avatar's glyph sitting 6.8281 from one side and 6.8438 from the other.
197
+ * Rounding those away would hide the asymmetry a ruler exists to reveal.
198
+ *
199
+ * In CSS pixels, not screen pixels: at 200% zoom one whole CSS pixel is two
200
+ * screen pixels, and a whole pixel should mean what the readout prints.
201
+ *
202
+ * Deliberately not applied after {@link constrainDirection}. Projecting onto a
203
+ * diagonal multiplies by √2/2, so a constrained endpoint is fractional by
204
+ * construction and the length between two whole points on a 45-degree line is
205
+ * irrational anyway. Rounding there would trade the exact angle Shift just
206
+ * promised for a tidier number.
207
+ */
208
+ export function quantizeFree(point) {
209
+ const side = point.snap?.side;
210
+ const xPinned = side === 'left' || side === 'right';
211
+ const yPinned = side === 'top' || side === 'bottom';
212
+ return {
213
+ x: xPinned ? point.x : Math.round(point.x),
214
+ y: yPinned ? point.y : Math.round(point.y),
215
+ snap: point.snap
216
+ };
217
+ }
@@ -0,0 +1,20 @@
1
+ import type { Vec } from './measure-geometry.js';
2
+ /**
3
+ * The moved point — or `null` when the key does not belong here.
4
+ *
5
+ * Tab is deliberately not one of `nudge`'s keys. Spec §10 reserves it for
6
+ * stepping through the specimen's snap targets — the accessible substitute
7
+ * for the pointer, and arguably the more precise input, not a consolation
8
+ * prize for lacking one: edges are exactly what one wants to measure in the
9
+ * first place. Arrow keys only come in afterwards, to nudge off an edge once
10
+ * a free point is what's wanted; `nudge` only ever moves a point that
11
+ * already exists, it does not place one. Someone will eventually be tempted
12
+ * to fold Tab in here as "simpler" free-pixel movement — resist it, and see
13
+ * `onKey` in the component for why returning `null` here (rather than the
14
+ * unchanged point) is what makes that possible to resist safely.
15
+ *
16
+ * The step sizes are the pair Spec §10 fixes for this tool: 1px plain, 10px
17
+ * with Shift.
18
+ */
19
+ export declare function nudge(point: Vec, key: string, shift: boolean): Vec | null;
20
+ //# sourceMappingURL=measure-keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measure-keyboard.d.ts","sourceRoot":"","sources":["../../../src/app/canvas/measure-keyboard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAKjD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,GAAG,GAAG,IAAI,CAezE"}
@@ -0,0 +1,34 @@
1
+ const STEP = 1;
2
+ const STEP_COARSE = 10;
3
+ /**
4
+ * The moved point — or `null` when the key does not belong here.
5
+ *
6
+ * Tab is deliberately not one of `nudge`'s keys. Spec §10 reserves it for
7
+ * stepping through the specimen's snap targets — the accessible substitute
8
+ * for the pointer, and arguably the more precise input, not a consolation
9
+ * prize for lacking one: edges are exactly what one wants to measure in the
10
+ * first place. Arrow keys only come in afterwards, to nudge off an edge once
11
+ * a free point is what's wanted; `nudge` only ever moves a point that
12
+ * already exists, it does not place one. Someone will eventually be tempted
13
+ * to fold Tab in here as "simpler" free-pixel movement — resist it, and see
14
+ * `onKey` in the component for why returning `null` here (rather than the
15
+ * unchanged point) is what makes that possible to resist safely.
16
+ *
17
+ * The step sizes are the pair Spec §10 fixes for this tool: 1px plain, 10px
18
+ * with Shift.
19
+ */
20
+ export function nudge(point, key, shift) {
21
+ const d = shift ? STEP_COARSE : STEP;
22
+ switch (key) {
23
+ case 'ArrowRight':
24
+ return { x: point.x + d, y: point.y };
25
+ case 'ArrowLeft':
26
+ return { x: point.x - d, y: point.y };
27
+ case 'ArrowDown':
28
+ return { x: point.x, y: point.y + d };
29
+ case 'ArrowUp':
30
+ return { x: point.x, y: point.y - d };
31
+ default:
32
+ return null;
33
+ }
34
+ }