@visitwonders/assembly 0.15.0 → 0.16.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/README.md CHANGED
@@ -100,6 +100,14 @@ appends (rather than replaces) the Ember outlet view inside
100
100
 
101
101
  [Longer description of how to use the addon in apps.]
102
102
 
103
+ ## Migrations
104
+
105
+ Breaking changes — token retirements, behaviour shifts, and visual-parity diffs that downstream consumers should know about — are documented one-file-per-change under [`docs/migrations/`](docs/migrations).
106
+
107
+ Recent:
108
+
109
+ - [Token Extension and Focus Ring Upgrade](docs/migrations/2026-04-27-token-extension-and-focus-ring-upgrade.md) (2026-04-27): surface-vs-fill split, two-stop focus ring (WCAG 2.2 non-text-contrast), typography role sub-tokens, action + input matrices populated, halo tokens retired.
110
+
103
111
  ## Contributing
104
112
 
105
113
  See the [Contributing](CONTRIBUTING.md) guide for details.
@@ -0,0 +1 @@
1
+ export { default } from "@visitwonders/assembly/data/pagination-cluster";
@@ -0,0 +1 @@
1
+ export { default } from "@visitwonders/assembly/data/pagination";
@@ -40,15 +40,11 @@
40
40
 
41
41
  .button_e0e07a6eb[data-variant][data-tone]:focus-visible {
42
42
  outline: none;
43
- box-shadow:
44
- 0 0 0 2px var(--color-bg-surface),
45
- 0 0 0 4px var(--color-focus-ring-halo);
43
+ box-shadow: var(--focus-ring);
46
44
  }
47
45
 
48
46
  .button_e0e07a6eb[data-variant][data-tone="critical"]:focus-visible {
49
- box-shadow:
50
- 0 0 0 2px var(--color-bg-surface),
51
- 0 0 0 4px var(--color-focus-ring-halo-critical);
47
+ box-shadow: var(--focus-ring-critical);
52
48
  }
53
49
 
54
50
  /* ===================================
@@ -91,57 +87,72 @@
91
87
 
92
88
  /* ===================================
93
89
  * Solid Variant
90
+ *
91
+ * Solid variants are the canonical action-matrix consumers: bg + text
92
+ * come from `--color-action-{tone}-{bg|text}-*`. The `neutral` tone
93
+ * is a documented variant override — its visual is a "white card"
94
+ * (surface bg + control border + drop shadow), so it picks `bg` from
95
+ * `--color-bg-surface` rather than the matrix's `interactive` fill,
96
+ * but still pulls border/text/active-bg through the matrix.
94
97
  * =================================== */
95
98
 
96
99
  /* Primary */
97
100
  .button_e0e07a6eb[data-variant="solid"][data-tone="primary"] {
98
- background: var(--color-bg-fill-primary);
99
- color: var(--color-text-on-primary);
101
+ background: var(--color-action-primary-bg-default);
102
+ color: var(--color-action-primary-text-default);
100
103
  }
101
104
 
102
105
  .button_e0e07a6eb[data-variant="solid"][data-tone="primary"]:hover:not(:disabled) {
103
- background: var(--color-bg-fill-primary-hover);
106
+ background: var(--color-action-primary-bg-hover);
104
107
  }
105
108
 
106
109
  .button_e0e07a6eb[data-variant="solid"][data-tone="primary"]:active:not(:disabled) {
107
- background: var(--color-bg-fill-primary-active);
110
+ background: var(--color-action-primary-bg-pressed);
108
111
  }
109
112
 
110
- /* Neutral */
113
+ /* Neutral — variant override: bg stays on surface (white card),
114
+ border + text reach through the matrix. */
111
115
  .button_e0e07a6eb[data-variant="solid"][data-tone="neutral"] {
112
116
  background: var(--color-bg-surface);
113
- color: var(--color-text);
114
- border: 1px solid var(--color-border-control);
117
+ color: var(--color-action-neutral-text-default);
118
+ border: 1px solid var(--color-action-neutral-border-default);
115
119
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
116
120
  }
117
121
 
118
122
  .button_e0e07a6eb[data-variant="solid"][data-tone="neutral"]:hover:not(:disabled) {
119
- border-color: var(--color-border-control-hover);
123
+ border-color: var(--color-action-neutral-border-hover);
120
124
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.08);
121
125
  }
122
126
 
123
127
  .button_e0e07a6eb[data-variant="solid"][data-tone="neutral"]:active:not(:disabled) {
124
128
  background: var(--color-bg-neutral-subtle);
125
- border-color: var(--color-border-control-active);
129
+ border-color: var(--color-action-neutral-border-pressed);
126
130
  box-shadow: none;
127
131
  }
128
132
 
129
133
  /* Critical */
130
134
  .button_e0e07a6eb[data-variant="solid"][data-tone="critical"] {
131
- background: var(--color-bg-fill-critical);
132
- color: var(--color-text-on-critical);
135
+ background: var(--color-action-critical-bg-default);
136
+ color: var(--color-action-critical-text-default);
133
137
  }
134
138
 
135
139
  .button_e0e07a6eb[data-variant="solid"][data-tone="critical"]:hover:not(:disabled) {
136
- background: var(--color-bg-fill-critical-hover);
140
+ background: var(--color-action-critical-bg-hover);
137
141
  }
138
142
 
139
143
  .button_e0e07a6eb[data-variant="solid"][data-tone="critical"]:active:not(:disabled) {
140
- background: var(--color-bg-fill-critical-active);
144
+ background: var(--color-action-critical-bg-pressed);
141
145
  }
142
146
 
143
147
  /* ===================================
144
148
  * Outline Variant
149
+ *
150
+ * Outline / ghost / link variants stay semantic-direct: their `text`
151
+ * carries the tone (so `--color-text-{tone}` is the right anchor),
152
+ * not the matrix's `--color-action-*-text-*` cells which encode
153
+ * "text on a solid fill" (e.g. white-on-primary). The matrix's
154
+ * `border` cells likewise target form-control borders (gray-300);
155
+ * outline-neutral wants the heavier `--color-border-neutral`.
145
156
  * =================================== */
146
157
 
147
158
  /* Primary */
@@ -1,3 +1,4 @@
1
+ export { default as Pagination } from './pagination.js';
1
2
  export { default as SortableList } from './sortable-list.js';
2
3
  export { default as Table } from './table.js';
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,106 @@
1
+ // Pure helper for the Pagination component's page-cluster algorithm.
2
+ // Co-located here under src/data/ next to where pagination.gts will land.
3
+ // Spec contract: docs/specs/pagination.md §Behaviour > Page-cluster algorithm.
4
+ //
5
+ // This file has no Glimmer or DOM dependencies; it is a pure function so it
6
+ // can be unit-tested in isolation and reused by any future consumer.
7
+
8
+ // Required for the addon's rollup `app-reexports` pattern (`data/!(index).js`)
9
+ // to publish this file into `dist/`. Do not remove — without a default export
10
+ // the file is dropped from the build silently. Consumers should use the named
11
+ // exports below.
12
+ var paginationCluster = {};
13
+ function buildPageCluster(args) {
14
+ const {
15
+ totalPages,
16
+ currentPage,
17
+ siblingCount,
18
+ boundaryCount
19
+ } = args;
20
+ if (totalPages <= 0) return [];
21
+ if (totalPages === 1) return [{
22
+ kind: 'page',
23
+ page: 1
24
+ }];
25
+ const sibling = {
26
+ start: Math.max(1, currentPage - siblingCount),
27
+ end: Math.min(totalPages, currentPage + siblingCount)
28
+ };
29
+ const initial = [];
30
+ if (boundaryCount > 0) {
31
+ initial.push({
32
+ start: 1,
33
+ end: Math.min(boundaryCount, totalPages)
34
+ });
35
+ }
36
+ initial.push(sibling);
37
+ if (boundaryCount > 0) {
38
+ initial.push({
39
+ start: Math.max(1, totalPages - boundaryCount + 1),
40
+ end: totalPages
41
+ });
42
+ }
43
+ const merged = mergeOverlapping(initial);
44
+ const collapsed = collapseSinglePageGapIfOnly(merged);
45
+ return emit(collapsed, sibling);
46
+ }
47
+
48
+ // Merge intervals that overlap or are directly touching (gap < 1).
49
+ // Input intervals are already in non-decreasing-start order by construction
50
+ // (left boundary, sibling, right boundary).
51
+ function mergeOverlapping(intervals) {
52
+ const sorted = [...intervals].sort((a, b) => a.start - b.start);
53
+ const result = [];
54
+ for (const next of sorted) {
55
+ const last = result[result.length - 1];
56
+ if (last && next.start <= last.end + 1) {
57
+ last.end = Math.max(last.end, next.end);
58
+ } else {
59
+ result.push({
60
+ ...next
61
+ });
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+
67
+ // Rule 4: a single hidden page is never elided — but only when it is the
68
+ // ONLY remaining gap. With multiple gaps, single-page gaps stay as ellipses
69
+ // so the cluster shape is consistent. See spec §Behaviour for worked
70
+ // examples that pin this interpretation (T=7,C=4 keeps both ellipses;
71
+ // T=6,C=3 inlines its single gap because there is only one).
72
+ function collapseSinglePageGapIfOnly(intervals) {
73
+ if (intervals.length !== 2) return intervals;
74
+ const [first, second] = intervals;
75
+ const gapSize = second.start - first.end - 1;
76
+ if (gapSize !== 1) return intervals;
77
+ return [{
78
+ start: first.start,
79
+ end: second.end
80
+ }];
81
+ }
82
+ function emit(intervals, sibling) {
83
+ const items = [];
84
+ for (let i = 0; i < intervals.length; i++) {
85
+ const interval = intervals[i];
86
+ for (let p = interval.start; p <= interval.end; p++) {
87
+ items.push({
88
+ kind: 'page',
89
+ page: p
90
+ });
91
+ }
92
+ const next = intervals[i + 1];
93
+ if (next) {
94
+ const gapEnd = next.start - 1;
95
+ const key = gapEnd < sibling.start ? 'ellipsis-left' : 'ellipsis-right';
96
+ items.push({
97
+ kind: 'ellipsis',
98
+ key
99
+ });
100
+ }
101
+ }
102
+ return items;
103
+ }
104
+
105
+ export { buildPageCluster, paginationCluster as default };
106
+ //# sourceMappingURL=pagination-cluster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pagination-cluster.js","sources":["../../src/data/pagination-cluster.ts"],"sourcesContent":["// Pure helper for the Pagination component's page-cluster algorithm.\n// Co-located here under src/data/ next to where pagination.gts will land.\n// Spec contract: docs/specs/pagination.md §Behaviour > Page-cluster algorithm.\n//\n// This file has no Glimmer or DOM dependencies; it is a pure function so it\n// can be unit-tested in isolation and reused by any future consumer.\n\n// Required for the addon's rollup `app-reexports` pattern (`data/!(index).js`)\n// to publish this file into `dist/`. Do not remove — without a default export\n// the file is dropped from the build silently. Consumers should use the named\n// exports below.\nexport default {};\n\nexport type PageClusterItem =\n | { kind: 'page'; page: number }\n | { kind: 'ellipsis'; key: string };\n\nexport interface BuildClusterArgs {\n /** Total page count, T = ceil(totalItems / pageSize). Caller computes this. */\n totalPages: number;\n /** Current page, 1-indexed. Caller clamps to [1, totalPages] before passing. */\n currentPage: number;\n /** Pages shown either side of currentPage in the cluster. */\n siblingCount: number;\n /** Pages always shown at the start and end of the cluster. */\n boundaryCount: number;\n}\n\ninterface Interval {\n start: number;\n end: number;\n}\n\nexport function buildPageCluster(args: BuildClusterArgs): PageClusterItem[] {\n const { totalPages, currentPage, siblingCount, boundaryCount } = args;\n\n if (totalPages <= 0) return [];\n if (totalPages === 1) return [{ kind: 'page', page: 1 }];\n\n const sibling: Interval = {\n start: Math.max(1, currentPage - siblingCount),\n end: Math.min(totalPages, currentPage + siblingCount),\n };\n\n const initial: Interval[] = [];\n if (boundaryCount > 0) {\n initial.push({ start: 1, end: Math.min(boundaryCount, totalPages) });\n }\n initial.push(sibling);\n if (boundaryCount > 0) {\n initial.push({\n start: Math.max(1, totalPages - boundaryCount + 1),\n end: totalPages,\n });\n }\n\n const merged = mergeOverlapping(initial);\n const collapsed = collapseSinglePageGapIfOnly(merged);\n\n return emit(collapsed, sibling);\n}\n\n// Merge intervals that overlap or are directly touching (gap < 1).\n// Input intervals are already in non-decreasing-start order by construction\n// (left boundary, sibling, right boundary).\nfunction mergeOverlapping(intervals: Interval[]): Interval[] {\n const sorted = [...intervals].sort((a, b) => a.start - b.start);\n const result: Interval[] = [];\n for (const next of sorted) {\n const last = result[result.length - 1];\n if (last && next.start <= last.end + 1) {\n last.end = Math.max(last.end, next.end);\n } else {\n result.push({ ...next });\n }\n }\n return result;\n}\n\n// Rule 4: a single hidden page is never elided — but only when it is the\n// ONLY remaining gap. With multiple gaps, single-page gaps stay as ellipses\n// so the cluster shape is consistent. See spec §Behaviour for worked\n// examples that pin this interpretation (T=7,C=4 keeps both ellipses;\n// T=6,C=3 inlines its single gap because there is only one).\nfunction collapseSinglePageGapIfOnly(intervals: Interval[]): Interval[] {\n if (intervals.length !== 2) return intervals;\n const [first, second] = intervals as [Interval, Interval];\n const gapSize = second.start - first.end - 1;\n if (gapSize !== 1) return intervals;\n return [{ start: first.start, end: second.end }];\n}\n\nfunction emit(intervals: Interval[], sibling: Interval): PageClusterItem[] {\n const items: PageClusterItem[] = [];\n for (let i = 0; i < intervals.length; i++) {\n const interval = intervals[i]!;\n for (let p = interval.start; p <= interval.end; p++) {\n items.push({ kind: 'page', page: p });\n }\n const next = intervals[i + 1];\n if (next) {\n const gapEnd = next.start - 1;\n const key: string =\n gapEnd < sibling.start ? 'ellipsis-left' : 'ellipsis-right';\n items.push({ kind: 'ellipsis', key });\n }\n }\n return items;\n}\n"],"names":["buildPageCluster","args","totalPages","currentPage","siblingCount","boundaryCount","kind","page","sibling","start","Math","max","end","min","initial","push","merged","mergeOverlapping","collapsed","collapseSinglePageGapIfOnly","emit","intervals","sorted","sort","a","b","result","next","last","length","first","second","gapSize","items","i","interval","p","gapEnd","key"],"mappings":"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"}
@@ -0,0 +1,216 @@
1
+ /* src/data/pagination.css */
2
+ /* ============================================================================
3
+ Pagination Component Styles
4
+
5
+ Spec: docs/specs/pagination.md
6
+ Per docs/component-tokens.md, Pagination routes semantic-direct (no
7
+ layer-3 component-token matrix). Every value below is a semantic token.
8
+ ============================================================================ */
9
+
10
+ .pagination_e7638bd11 {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: space-between;
14
+ flex-wrap: wrap;
15
+ gap: var(--spacing-gap-md);
16
+ font-size: var(--font-size-md);
17
+ }
18
+
19
+ /* On `numbered`, there is no summary; the cluster sits alone (centred is fine
20
+ but the consumer gets to position it via the parent layout). */
21
+ .pagination_e7638bd11[data-variant="numbered"] {
22
+ justify-content: center;
23
+ }
24
+
25
+ /* On `compact`, the indicator + prev/next form a tight row. */
26
+ .pagination_e7638bd11[data-variant="compact"] {
27
+ justify-content: center;
28
+ gap: var(--spacing-gap-xs);
29
+ }
30
+
31
+ /* ============================================================================
32
+ Summary
33
+ ============================================================================ */
34
+
35
+ .summary_e7638bd11 {
36
+ color: var(--color-text-secondary);
37
+ }
38
+
39
+ /* ============================================================================
40
+ Controls (prev + cluster + next group)
41
+
42
+ The controls wrapper exists so prev/next sit OUTSIDE the cluster <ol>.
43
+ That keeps the <ol> semantically pure (only pages) and — load-bearingly —
44
+ isolates the cluster's reserved min-width from prev/next, so prev/next
45
+ anchor at fixed positions while the cluster's interior reflows.
46
+ ============================================================================ */
47
+
48
+ .controls_e7638bd11 {
49
+ display: flex;
50
+ align-items: center;
51
+ flex-wrap: wrap;
52
+ gap: var(--spacing-gap-xs);
53
+ row-gap: var(--spacing-gap-md);
54
+ }
55
+
56
+ /* ============================================================================
57
+ Page Cluster
58
+ ============================================================================ */
59
+
60
+ .cluster_e7638bd11 {
61
+ display: flex;
62
+ align-items: center;
63
+ flex-wrap: nowrap;
64
+ gap: var(--spacing-gap-xs);
65
+ margin: 0;
66
+ padding: 0;
67
+ list-style: none;
68
+
69
+ /* Reserve a stable footprint based on the maximum cluster width the
70
+ current sibling/boundary settings can produce, so prev/next don't
71
+ shift horizontally as the user clicks through pages. The actual
72
+ items centre within the reserved space when the cluster is shorter
73
+ than its maximum.
74
+
75
+ Formula: N items * sm-button-square + (N - 1) * gap, where N is
76
+ supplied via the `--pagination-max-items` runtime style prop on
77
+ the <ol>. Falls back to a reasonable default if the prop is missing
78
+ so the cluster never collapses. */
79
+ min-width: calc(
80
+ var(--pagination-max-items, 9) * var(--button-height-sm) +
81
+ (var(--pagination-max-items, 9) - 1) * var(--spacing-gap-xs)
82
+ );
83
+ justify-content: center;
84
+ }
85
+
86
+ .item_e7638bd11 {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ }
91
+
92
+ /* Uniform width across every numeric page button so 1-digit and 3-digit
93
+ pages render at the same minimum size. Buttons grow uniformly past the
94
+ minimum once content (e.g. "100") demands it; ellipses match. The
95
+ target selectors hit only the cluster's numbered controls — prev/next
96
+ keep their text-driven width. */
97
+ .cluster_e7638bd11 .item_e7638bd11 [data-test-pagination-page] {
98
+ min-width: var(--button-height-sm);
99
+ }
100
+
101
+ /* Current-page fill on the wrapped Button. The fill is decorative — the
102
+ load-bearing affordances are aria-current, focus ring, and text contrast.
103
+ See spec §Contrast and §Token gaps for follow-up. */
104
+ .cluster_e7638bd11 .item_e7638bd11 [data-current="true"] {
105
+ background: var(--color-bg-fill-interactive-selected);
106
+ }
107
+
108
+ /* ============================================================================
109
+ Ellipsis
110
+ ============================================================================ */
111
+
112
+ .ellipsis_e7638bd11 {
113
+ display: inline-flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ min-width: var(--button-height-sm);
117
+ height: var(--button-height-sm);
118
+ color: var(--color-text-tertiary);
119
+ user-select: none;
120
+ }
121
+
122
+ /* ============================================================================
123
+ Custom :prev / :next block alignment
124
+
125
+ When a consumer overrides <:prev> or <:next>, their icon + text content
126
+ lands in Button's default slot (`.button-label`). That slot is a span
127
+ with `flex: 1 1 auto; text-box: cap alphabetic` — sized for text, not
128
+ for inline icon+text composition. Targeting it inside Pagination's
129
+ prev/next Buttons makes it a flex container so the Icon and text
130
+ centre against each other regardless of intrinsic SVG height.
131
+
132
+ For the default (non-overridden) path the icon goes through `<:prefix>`
133
+ (which is already `display: flex; align-items: center`) and `.button-label`
134
+ contains only the text — so this rule is a no-op there.
135
+ ============================================================================ */
136
+
137
+ [data-test-pagination-prev] .button-label_e7638bd11,
138
+ [data-test-pagination-next] .button-label_e7638bd11 {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: var(--spacing-gap-xs);
142
+ }
143
+
144
+ /* ============================================================================
145
+ Compact indicator
146
+
147
+ Height pinned to button-height-sm so the indicator matches the prev/next
148
+ button height exactly — otherwise the row's tallest child sets the line
149
+ height and the buttons appear top-anchored.
150
+ ============================================================================ */
151
+
152
+ .indicator_e7638bd11 {
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ height: var(--button-height-sm);
157
+ padding: 0 var(--spacing-gap-xs);
158
+ color: var(--color-text-secondary);
159
+ white-space: nowrap;
160
+ }
161
+
162
+ /* ============================================================================
163
+ Jump-to-page form
164
+
165
+ The form is a horizontal flex row of [label] [input] [submit]. It sits to
166
+ the right of the navigation cluster with the wider --spacing-gap-md gap
167
+ (vs the --spacing-gap-xs that separates prev/next/first/last) so the form
168
+ reads as a distinct chunk, not a peer of the cluster controls.
169
+
170
+ NumberField wraps Label + input in a Control whose default orientation is
171
+ column. We override locally so the label sits inline with the input. The
172
+ selectors are scoped under the data-test attribute so we never bleed into
173
+ any other Form/NumberField on the page.
174
+ ============================================================================ */
175
+
176
+ .controls_e7638bd11 > .jump-to_e7638bd11 {
177
+ display: flex;
178
+ flex-direction: row;
179
+ align-items: center;
180
+ gap: var(--spacing-gap-xs);
181
+ margin-left: var(--spacing-gap-md);
182
+ }
183
+
184
+ /* Control wraps Label + input-wrapper. Default orientation is column (label
185
+ above input). Force row-flex so the label sits inline with the input, and
186
+ shrink-to-content via width: auto so it doesn't claim the parent's width. */
187
+ [data-test-pagination-jump-form] [data-test-form-control] {
188
+ flex-direction: row;
189
+ align-items: center;
190
+ gap: var(--spacing-gap-xs);
191
+ width: auto;
192
+ }
193
+
194
+ /* The default Label has padding-top tuned for column layout; reset for the
195
+ inline row so the label sits centred against the input. white-space: nowrap
196
+ prevents "Go to page" from wrapping when the form is squeezed.
197
+
198
+ We target [data-test-form-label] / [data-test-number-field-wrapper] rather
199
+ than the .label / .number-field-input-wrapper class names, because this CSS
200
+ is processed through ember-scoped-css — class selectors get a hash suffix
201
+ tied to Pagination's scope, so they don't match elements rendered by
202
+ external components (Label, NumberField). The data-test attributes are
203
+ untouched by scoping and reach the actual elements. */
204
+ [data-test-pagination-jump-form] [data-test-form-label] {
205
+ flex-shrink: 0;
206
+ padding-top: 0;
207
+ white-space: nowrap;
208
+ }
209
+
210
+ /* The number-field input wrapper is content-fluid by default; constrain it
211
+ to a fixed compact width so the form reads as a "type a small number"
212
+ affordance, not a "fill the page" input. */
213
+ [data-test-pagination-jump-form] [data-test-number-field-wrapper] {
214
+ width: 80px;
215
+ height: var(--button-height-sm);
216
+ }