@fiscozen/composables 1.0.3 → 1.1.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @fiscozen/composables
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 1c2baef: layout page-templates extraction — Phase 0 (breakpoint groundwork).
8
+ - `@fiscozen/composables`: `useBreakpoints()` gains a reactive `current()` accessor that returns the name of the largest breakpoint whose `min-width` is currently matched (descending priority, inclusive `min-width` for boundary-parity with a `>=` check; order-independent).
9
+ - `@fiscozen/layout`: `FzLayout` now resolves its active breakpoint through `useBreakpoints().current()` instead of a bespoke `window.resize` listener, and gains an opt-out `disablePadding` prop. Padding stays on by default (`p-12`), so existing consumers render identically.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [1c2baef]
14
+ - @fiscozen/style@0.4.0
15
+
16
+ ## 1.0.4
17
+
18
+ ### Patch Changes
19
+
20
+ - a243ebb: Fix mobile dropdown menu issues: tapping the icon dropdown trigger again now closes the menu (it previously only ever re-opened it), and the floating menu no longer jumps to the top-left corner when its opener becomes hidden (e.g. an accordion collapses while the menu is still open).
21
+
3
22
  ## 1.0.3
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,108 @@
1
1
  # @fiscozen/composables
2
2
 
3
- ## useFloating and FzFloating
4
- This composable holds logic that can be added to a component in order to be positioned correctly as a floating window.
5
- The default "component wrapper" of the composable is `FzFloating`.
3
+ > For usage documentation, see [Storybook Documentation](../../apps/storybook/src/Composables.mdx)
4
+
5
+ ## Development
6
+
7
+ ### Setup
8
+
9
+ ```bash
10
+ pnpm install
11
+ pnpm --filter @fiscozen/composables build
12
+ ```
13
+
14
+ ### Code Organization
15
+
16
+ ```
17
+ src/
18
+ index.ts Re-exports everything (composables, types, utils, FzFloating)
19
+ types.ts Shared types (FzFloatingPosition, FzRect, FzUseCurrencyOptions, etc.)
20
+ utils/
21
+ index.ts Barrel file for utilities
22
+ number/
23
+ index.ts Currency format/parse helpers
24
+ position/
25
+ index.ts Floating position calculation helpers
26
+ FzFloating.vue Component wrapper for useFloating
27
+ composables/
28
+ index.ts Barrel file for all composables
29
+ useFloating.ts Floating element positioning
30
+ useMediaQuery.ts Reactive CSS media query matching
31
+ useBreakpoints.ts Responsive breakpoint detection (wraps useMediaQuery)
32
+ useClickOutside.ts Click-outside detection
33
+ useKeyDown.ts Keydown event listener
34
+ useKeyUp.ts Keyup event listener
35
+ useCurrency.ts Currency formatting (deprecated)
36
+ __tests__/
37
+ useFloating.spec.ts useFloating unit tests
38
+ FzFloating.spec.ts FzFloating component tests
39
+ number.spec.ts Currency/number utility tests
40
+ ```
41
+
42
+ ## Composables
43
+
44
+ ### useFloating
45
+
46
+ Positioning logic for floating/popover elements (tooltips, dropdowns, popovers).
47
+
48
+ **Architecture:**
49
+
50
+ - **Lookup tables**: `positionCalculators` (with opener) and `containerPositionCalculators` (without opener) provide O(1) position resolution for all 12 explicit positions (top/bottom/left/right + -start/-end variants).
51
+ - **Auto-position resolution**: `resolveAutoPosition` delegates to `getHighestAvailableSpacePos` for `auto`, `auto-vertical`, `auto-start`, `auto-end`, and their combinations.
52
+ - **Boundary corrections**: `applyBoundaryCorrections` clamps the final position within the container bounds, resetting transforms when the element would overflow.
53
+ - **Layout shift prevention**: The element is set to `position: fixed` immediately before rect calculations.
54
+ - **Reactive repositioning**: Uses `IntersectionObserver` to track visibility changes.
55
+
56
+ The `FzFloating` component (`src/FzFloating.vue`) is the default wrapper that exposes `useFloating` through a declarative template API.
57
+
58
+ ### useMediaQuery
59
+
60
+ Reactive wrapper around `window.matchMedia`. Returns a computed `Ref<boolean>` that updates when the media query match state changes. Lifecycle-aware: adds listener on mount, removes on unmount.
61
+
62
+ ### useBreakpoints
63
+
64
+ Higher-level abstraction over `useMediaQuery` for responsive design. Accepts a typed breakpoint map (`Record<T, '${number}px'>`) and returns `isGreater(bp)`, `isSmaller(bp)`, and `isInBetween(min, max)`.
65
+
66
+ ### useClickOutside
67
+
68
+ Detects clicks outside a target element. Accepts a component ref, a callback, and an optional element to scope the listener to (defaults to `document`). Supports dynamic listener re-attachment via `watch` when the scoped element changes.
69
+
70
+ ### useKeyDown
71
+
72
+ Attaches a `keydown` listener to a component ref. Lifecycle-managed (mount/unmount). Throws if component or callback is missing.
73
+
74
+ ### useKeyUp
75
+
76
+ Attaches a `keyup` listener to a component ref or to `document` when no component is provided. Argument order differs from `useKeyDown`: `(callback, component?)` — the component is optional.
77
+
78
+ ### useCurrency
79
+
80
+ **Deprecated.** Wraps `format` and `parse` from `src/utils/number`. Consumers should import those functions directly instead.
81
+
82
+ ## Testing
83
+
84
+ ### Running Tests
85
+
86
+ ```bash
87
+ pnpm --filter @fiscozen/composables test:unit
88
+ pnpm --filter @fiscozen/composables coverage
89
+ ```
90
+
91
+ ### Test Structure
92
+
93
+ - `useFloating.spec.ts` / `FzFloating.spec.ts`: Position calculation and component rendering.
94
+ - `number.spec.ts`: Currency format/parse utilities.
95
+
96
+ Tests mock `window.matchMedia` in `beforeEach`. Coverage target: >90%.
97
+
98
+ ## Dependencies
99
+
100
+ - `@fiscozen/style` (workspace) — breakpoint tokens used by `useBreakpoints` / `useMediaQuery`
101
+
102
+ ## Build
103
+
104
+ ```bash
105
+ pnpm --filter @fiscozen/composables build
106
+ ```
107
+
108
+ Runs Vite library build. Output goes to `dist/`.
@@ -0,0 +1,224 @@
1
+ body, html {
2
+ margin:0; padding: 0;
3
+ height: 100%;
4
+ }
5
+ body {
6
+ font-family: Helvetica Neue, Helvetica, Arial;
7
+ font-size: 14px;
8
+ color:#333;
9
+ }
10
+ .small { font-size: 12px; }
11
+ *, *:after, *:before {
12
+ -webkit-box-sizing:border-box;
13
+ -moz-box-sizing:border-box;
14
+ box-sizing:border-box;
15
+ }
16
+ h1 { font-size: 20px; margin: 0;}
17
+ h2 { font-size: 14px; }
18
+ pre {
19
+ font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20
+ margin: 0;
21
+ padding: 0;
22
+ -moz-tab-size: 2;
23
+ -o-tab-size: 2;
24
+ tab-size: 2;
25
+ }
26
+ a { color:#0074D9; text-decoration:none; }
27
+ a:hover { text-decoration:underline; }
28
+ .strong { font-weight: bold; }
29
+ .space-top1 { padding: 10px 0 0 0; }
30
+ .pad2y { padding: 20px 0; }
31
+ .pad1y { padding: 10px 0; }
32
+ .pad2x { padding: 0 20px; }
33
+ .pad2 { padding: 20px; }
34
+ .pad1 { padding: 10px; }
35
+ .space-left2 { padding-left:55px; }
36
+ .space-right2 { padding-right:20px; }
37
+ .center { text-align:center; }
38
+ .clearfix { display:block; }
39
+ .clearfix:after {
40
+ content:'';
41
+ display:block;
42
+ height:0;
43
+ clear:both;
44
+ visibility:hidden;
45
+ }
46
+ .fl { float: left; }
47
+ @media only screen and (max-width:640px) {
48
+ .col3 { width:100%; max-width:100%; }
49
+ .hide-mobile { display:none!important; }
50
+ }
51
+
52
+ .quiet {
53
+ color: #7f7f7f;
54
+ color: rgba(0,0,0,0.5);
55
+ }
56
+ .quiet a { opacity: 0.7; }
57
+
58
+ .fraction {
59
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60
+ font-size: 10px;
61
+ color: #555;
62
+ background: #E8E8E8;
63
+ padding: 4px 5px;
64
+ border-radius: 3px;
65
+ vertical-align: middle;
66
+ }
67
+
68
+ div.path a:link, div.path a:visited { color: #333; }
69
+ table.coverage {
70
+ border-collapse: collapse;
71
+ margin: 10px 0 0 0;
72
+ padding: 0;
73
+ }
74
+
75
+ table.coverage td {
76
+ margin: 0;
77
+ padding: 0;
78
+ vertical-align: top;
79
+ }
80
+ table.coverage td.line-count {
81
+ text-align: right;
82
+ padding: 0 5px 0 20px;
83
+ }
84
+ table.coverage td.line-coverage {
85
+ text-align: right;
86
+ padding-right: 10px;
87
+ min-width:20px;
88
+ }
89
+
90
+ table.coverage td span.cline-any {
91
+ display: inline-block;
92
+ padding: 0 5px;
93
+ width: 100%;
94
+ }
95
+ .missing-if-branch {
96
+ display: inline-block;
97
+ margin-right: 5px;
98
+ border-radius: 3px;
99
+ position: relative;
100
+ padding: 0 4px;
101
+ background: #333;
102
+ color: yellow;
103
+ }
104
+
105
+ .skip-if-branch {
106
+ display: none;
107
+ margin-right: 10px;
108
+ position: relative;
109
+ padding: 0 4px;
110
+ background: #ccc;
111
+ color: white;
112
+ }
113
+ .missing-if-branch .typ, .skip-if-branch .typ {
114
+ color: inherit !important;
115
+ }
116
+ .coverage-summary {
117
+ border-collapse: collapse;
118
+ width: 100%;
119
+ }
120
+ .coverage-summary tr { border-bottom: 1px solid #bbb; }
121
+ .keyline-all { border: 1px solid #ddd; }
122
+ .coverage-summary td, .coverage-summary th { padding: 10px; }
123
+ .coverage-summary tbody { border: 1px solid #bbb; }
124
+ .coverage-summary td { border-right: 1px solid #bbb; }
125
+ .coverage-summary td:last-child { border-right: none; }
126
+ .coverage-summary th {
127
+ text-align: left;
128
+ font-weight: normal;
129
+ white-space: nowrap;
130
+ }
131
+ .coverage-summary th.file { border-right: none !important; }
132
+ .coverage-summary th.pct { }
133
+ .coverage-summary th.pic,
134
+ .coverage-summary th.abs,
135
+ .coverage-summary td.pct,
136
+ .coverage-summary td.abs { text-align: right; }
137
+ .coverage-summary td.file { white-space: nowrap; }
138
+ .coverage-summary td.pic { min-width: 120px !important; }
139
+ .coverage-summary tfoot td { }
140
+
141
+ .coverage-summary .sorter {
142
+ height: 10px;
143
+ width: 7px;
144
+ display: inline-block;
145
+ margin-left: 0.5em;
146
+ background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147
+ }
148
+ .coverage-summary .sorted .sorter {
149
+ background-position: 0 -20px;
150
+ }
151
+ .coverage-summary .sorted-desc .sorter {
152
+ background-position: 0 -10px;
153
+ }
154
+ .status-line { height: 10px; }
155
+ /* yellow */
156
+ .cbranch-no { background: yellow !important; color: #111; }
157
+ /* dark red */
158
+ .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159
+ .low .chart { border:1px solid #C21F39 }
160
+ .highlighted,
161
+ .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162
+ background: #C21F39 !important;
163
+ }
164
+ /* medium red */
165
+ .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166
+ /* light red */
167
+ .low, .cline-no { background:#FCE1E5 }
168
+ /* light green */
169
+ .high, .cline-yes { background:rgb(230,245,208) }
170
+ /* medium green */
171
+ .cstat-yes { background:rgb(161,215,106) }
172
+ /* dark green */
173
+ .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174
+ .high .chart { border:1px solid rgb(77,146,33) }
175
+ /* dark yellow (gold) */
176
+ .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177
+ .medium .chart { border:1px solid #f9cd0b; }
178
+ /* light yellow */
179
+ .medium { background: #fff4c2; }
180
+
181
+ .cstat-skip { background: #ddd; color: #111; }
182
+ .fstat-skip { background: #ddd; color: #111 !important; }
183
+ .cbranch-skip { background: #ddd !important; color: #111; }
184
+
185
+ span.cline-neutral { background: #eaeaea; }
186
+
187
+ .coverage-summary td.empty {
188
+ opacity: .5;
189
+ padding-top: 4px;
190
+ padding-bottom: 4px;
191
+ line-height: 1;
192
+ color: #888;
193
+ }
194
+
195
+ .cover-fill, .cover-empty {
196
+ display:inline-block;
197
+ height: 12px;
198
+ }
199
+ .chart {
200
+ line-height: 0;
201
+ }
202
+ .cover-empty {
203
+ background: white;
204
+ }
205
+ .cover-full {
206
+ border-right: none !important;
207
+ }
208
+ pre.prettyprint {
209
+ border: none !important;
210
+ padding: 0 !important;
211
+ margin: 0 !important;
212
+ }
213
+ .com { color: #999 !important; }
214
+ .ignore-none { color: #999; font-weight: normal; }
215
+
216
+ .wrapper {
217
+ min-height: 100%;
218
+ height: auto !important;
219
+ height: 100%;
220
+ margin: 0 auto -48px;
221
+ }
222
+ .footer, .push {
223
+ height: 48px;
224
+ }
@@ -0,0 +1,87 @@
1
+ /* eslint-disable */
2
+ var jumpToCode = (function init() {
3
+ // Classes of code we would like to highlight in the file view
4
+ var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
5
+
6
+ // Elements to highlight in the file listing view
7
+ var fileListingElements = ['td.pct.low'];
8
+
9
+ // We don't want to select elements that are direct descendants of another match
10
+ var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
11
+
12
+ // Selector that finds elements on the page to which we can jump
13
+ var selector =
14
+ fileListingElements.join(', ') +
15
+ ', ' +
16
+ notSelector +
17
+ missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
18
+
19
+ // The NodeList of matching elements
20
+ var missingCoverageElements = document.querySelectorAll(selector);
21
+
22
+ var currentIndex;
23
+
24
+ function toggleClass(index) {
25
+ missingCoverageElements
26
+ .item(currentIndex)
27
+ .classList.remove('highlighted');
28
+ missingCoverageElements.item(index).classList.add('highlighted');
29
+ }
30
+
31
+ function makeCurrent(index) {
32
+ toggleClass(index);
33
+ currentIndex = index;
34
+ missingCoverageElements.item(index).scrollIntoView({
35
+ behavior: 'smooth',
36
+ block: 'center',
37
+ inline: 'center'
38
+ });
39
+ }
40
+
41
+ function goToPrevious() {
42
+ var nextIndex = 0;
43
+ if (typeof currentIndex !== 'number' || currentIndex === 0) {
44
+ nextIndex = missingCoverageElements.length - 1;
45
+ } else if (missingCoverageElements.length > 1) {
46
+ nextIndex = currentIndex - 1;
47
+ }
48
+
49
+ makeCurrent(nextIndex);
50
+ }
51
+
52
+ function goToNext() {
53
+ var nextIndex = 0;
54
+
55
+ if (
56
+ typeof currentIndex === 'number' &&
57
+ currentIndex < missingCoverageElements.length - 1
58
+ ) {
59
+ nextIndex = currentIndex + 1;
60
+ }
61
+
62
+ makeCurrent(nextIndex);
63
+ }
64
+
65
+ return function jump(event) {
66
+ if (
67
+ document.getElementById('fileSearch') === document.activeElement &&
68
+ document.activeElement != null
69
+ ) {
70
+ // if we're currently focused on the search input, we don't want to navigate
71
+ return;
72
+ }
73
+
74
+ switch (event.which) {
75
+ case 78: // n
76
+ case 74: // j
77
+ goToNext();
78
+ break;
79
+ case 66: // b
80
+ case 75: // k
81
+ case 80: // p
82
+ goToPrevious();
83
+ break;
84
+ }
85
+ };
86
+ })();
87
+ window.addEventListener('keydown', jumpToCode);