asma-ui-core 3.84.0 → 3.86.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 +2 -0
- package/dist/asma-ui-core.css +1 -1
- package/dist/components/icons/index.js +2 -0
- package/dist/components/icons/prohibition-icon/ProhibitionIcon.js +2 -2
- package/dist/components/icons/prohibition-large-icon/ProhibitionLargeIcon.js +17 -0
- package/dist/components/icons/prohibition-large-icon/index.js +1 -0
- package/dist/components/icons/stylus-note-icon/StylusNoteIcon.js +4 -4
- package/dist/components/icons/stylus-note-large-icon/StylusNoteLargeIcon.js +17 -0
- package/dist/components/icons/stylus-note-large-icon/index.js +1 -0
- package/dist/components/inputs/button/StyledButton.js +4 -4
- package/dist/components/inputs/textarea/StyledTextarea.module.js +4 -4
- package/dist/components/navigation/menu/StyledMenuItem.js +6 -6
- package/dist/components/navigation/tabs/StyledTab.js +7 -7
- package/dist/index.js +256 -252
- package/dist/src/components/icons/index.d.ts +2 -0
- package/dist/src/components/icons/prohibition-icon/ProhibitionIcon.d.ts +1 -1
- package/dist/src/components/icons/prohibition-large-icon/ProhibitionLargeIcon.d.ts +7 -0
- package/dist/src/components/icons/prohibition-large-icon/index.d.ts +1 -0
- package/dist/src/components/icons/stylus-note-icon/StylusNoteIcon.d.ts +1 -1
- package/dist/src/components/icons/stylus-note-large-icon/StylusNoteLargeIcon.d.ts +7 -0
- package/dist/src/components/icons/stylus-note-large-icon/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/styles/index.css +103 -0
- package/src/styles/touchReadiness.interaction.test.tsx +161 -0
|
@@ -165,7 +165,9 @@ export * from './file-xls-icon';
|
|
|
165
165
|
export * from './file-image-icon';
|
|
166
166
|
export * from './draw-icon';
|
|
167
167
|
export * from './prohibition-icon';
|
|
168
|
+
export * from './prohibition-large-icon';
|
|
168
169
|
export * from './stylus-note-icon';
|
|
170
|
+
export * from './stylus-note-large-icon';
|
|
169
171
|
export * from './print-icon';
|
|
170
172
|
export * from './arrow-shrink-icon';
|
|
171
173
|
export * from './indeterminate-icon';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { SVGProps } from 'react';
|
|
2
|
-
/**
|
|
2
|
+
/** Design-system `not-allowed` glyph (24px grid), as used on the Ad Voca Reject buttons. */
|
|
3
3
|
export declare function ProhibitionIcon(props: SVGProps<SVGSVGElement>): JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SVGProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Thin-stroke `block` illustration on a 140px grid — the exact Ad Voca Figma export used inside
|
|
4
|
+
* the tinted result-page circle ("Document rejected"). Not interchangeable with `ProhibitionIcon`:
|
|
5
|
+
* the 24px glyph has a much heavier stroke and looks wrong at illustration sizes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ProhibitionLargeIcon(props: SVGProps<SVGSVGElement>): JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ProhibitionLargeIcon';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { SVGProps } from 'react';
|
|
2
|
-
/**
|
|
2
|
+
/** Design-system `stylus_note` glyph (24px grid), as used on the Ad Voca sign buttons. */
|
|
3
3
|
export declare function StylusNoteIcon(props: SVGProps<SVGSVGElement>): JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SVGProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Thin-stroke `stylus_note` illustration on a 140px grid — the exact Ad Voca Figma export used
|
|
4
|
+
* inside the tinted result-page circle ("Document signed"). Not interchangeable with
|
|
5
|
+
* `StylusNoteIcon`: the 24px glyph has a much heavier stroke and looks wrong at illustration sizes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function StylusNoteLargeIcon(props: SVGProps<SVGSVGElement>): JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './StylusNoteLargeIcon';
|
package/package.json
CHANGED
package/src/styles/index.css
CHANGED
|
@@ -47,6 +47,109 @@
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/*
|
|
51
|
+
* ASMA-8210 (TB-02) — touch-readiness primitives for the Ad Voca mobile TabBar/Menu work.
|
|
52
|
+
*
|
|
53
|
+
* Four opt-in utilities. Every one of them is inert until a finger or a mouse button is on the
|
|
54
|
+
* control, or until the viewport is mobile, so no resting style changes anywhere: the frozen
|
|
55
|
+
* barrel (`api-surface.test.ts`) and the VRT baselines — captured at 1280x720 with a fine
|
|
56
|
+
* pointer — are untouched by design, not by luck. They reach consumers through `dist/style.css`,
|
|
57
|
+
* the same route as `.noselect` and `.absolute-center`, so no new export is needed to use them.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately UNLAYERED (like the reduced-motion block above), not in `@layer utilities`:
|
|
60
|
+
* `tailwind.config.ts` sets `important: true`, and Tailwind v3 importantifies custom utilities
|
|
61
|
+
* inside `@layer utilities` too. That would turn `.asma-hit-area`'s `position: relative` into
|
|
62
|
+
* `!important`, making it unoverridable by Tailwind position utilities and by inline styles.
|
|
63
|
+
* Unlayered, these rules still sit after the `@tailwind utilities` output in source order, so
|
|
64
|
+
* the one `!important` we do want (`.asma-touch-target`, below) keeps beating `min-h-*`.
|
|
65
|
+
*
|
|
66
|
+
* Pick per control:
|
|
67
|
+
* .asma-touch-ready control that already has a designed `:active` (Button, Chip, Switch).
|
|
68
|
+
* .asma-pressable control with no pressed state at all (Tab, MenuItem, Link).
|
|
69
|
+
* .asma-hit-area isolated control that must reach 44px without growing.
|
|
70
|
+
* .asma-touch-target row in a stack, where growing on a phone is the honest fix.
|
|
71
|
+
*
|
|
72
|
+
* Sizes come from `--asma-touch-target-size` (44px, Apple HIG / WCAG 2.5.5 AAA; WCAG 2.5.8 AA
|
|
73
|
+
* asks only 24px, so this is the stricter of the two) and `--asma-pressed-opacity`. Both are
|
|
74
|
+
* plain custom properties: a component or a consumer retunes them without a new class.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
* Kills the grey iOS/Android tap flash and the 300ms double-tap-zoom delay that `touch-action:
|
|
79
|
+
* auto` implies. Both behaviours exist only for touch input, so a mouse or trackpad renders and
|
|
80
|
+
* behaves exactly as it did before.
|
|
81
|
+
*/
|
|
82
|
+
.asma-touch-ready,
|
|
83
|
+
.asma-pressable {
|
|
84
|
+
-webkit-tap-highlight-color: transparent;
|
|
85
|
+
touch-action: manipulation;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The replacement for the tap flash removed above — without it, touch would end up with *less*
|
|
90
|
+
* feedback than before. Opacity is background-agnostic (filled, outlined and bare controls all
|
|
91
|
+
* read the same) and layout-neutral, so it never reflows and never reaches a VRT baseline.
|
|
92
|
+
* Disabled controls stay inert, matching their `onClick` being detached.
|
|
93
|
+
*/
|
|
94
|
+
.asma-pressable:active:not(:disabled):not([aria-disabled='true']) {
|
|
95
|
+
opacity: var(--asma-pressed-opacity, 0.7);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* A 44px pointer target on a control that must keep its drawn size: a transparent, centred
|
|
100
|
+
* overlay that belongs to the control, so a press anywhere inside it activates the control
|
|
101
|
+
* itself. `width/height: 100%` keeps it at least as large as the control it sits on.
|
|
102
|
+
*
|
|
103
|
+
* The supplied `position: relative` carries no `!important` (see the unlayered note above), so
|
|
104
|
+
* an inline `style` always overrides it, and a Tailwind position utility (importantified by
|
|
105
|
+
* `important: true`) does too.
|
|
106
|
+
*
|
|
107
|
+
* Caveats, all of them reasons to reach for `.asma-touch-target` instead:
|
|
108
|
+
* - it is NOT mobile-gated: the enlarged pointer target applies to desktop mouse users too
|
|
109
|
+
* (harmless for isolated controls, surprising next to other clickable things);
|
|
110
|
+
* - it owns the element's `::after`, clobbering any decorative `::after` the control has;
|
|
111
|
+
* - an ancestor `overflow: hidden` clips it, and a scroll container counts it as overflow;
|
|
112
|
+
* - it covers nested interactive children, and neighbours closer together than 44px would
|
|
113
|
+
* overlap and steal each other's presses.
|
|
114
|
+
*/
|
|
115
|
+
.asma-hit-area {
|
|
116
|
+
position: relative;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.asma-hit-area::after {
|
|
120
|
+
content: '';
|
|
121
|
+
position: absolute;
|
|
122
|
+
left: 50%;
|
|
123
|
+
top: 50%;
|
|
124
|
+
width: 100%;
|
|
125
|
+
height: 100%;
|
|
126
|
+
min-width: var(--asma-touch-target-size, 44px);
|
|
127
|
+
min-height: var(--asma-touch-target-size, 44px);
|
|
128
|
+
transform: translate(-50%, -50%);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* 44px tall rows in a stack, where overlapping hit areas would fight and growing is what the
|
|
133
|
+
* design actually wants. Gated to the mobile breakpoint (`_breakpoints.scss` `$mobile-max`) so
|
|
134
|
+
* tablet and desktop — including every VRT baseline — stay byte-identical. `!important` is
|
|
135
|
+
* required to outrank Tailwind's own `min-h-*`, which `important: true` emits with `!`.
|
|
136
|
+
*
|
|
137
|
+
* This is a LATER-SOURCE OVERRIDE, not a CSS `max()` floor — it replaces whatever `min-height`
|
|
138
|
+
* the component declares, it cannot clamp against it. Two consequences (ASMA-8210 review B1):
|
|
139
|
+
* - Only apply it where the EFFECTIVE min-height is below 44px — effective, not the class you
|
|
140
|
+
* can see: stacked utilities resolve by emitted order, so verify with computed style. On a
|
|
141
|
+
* component resting at `min-h-12` (48px) this rule would SHRINK the target — that is why
|
|
142
|
+
* StyledTab takes only `asma-pressable` (both tab variants effectively rest at 48px; even
|
|
143
|
+
* the small one, whose `min-h-10` loses to the base `min-h-12` by source order).
|
|
144
|
+
* - It deliberately sets no `min-width`: a width override would replace designed minimums
|
|
145
|
+
* like the tab's `min-w-[90px]` with 44px. Width floors stay per-component.
|
|
146
|
+
*/
|
|
147
|
+
@media (max-width: 743px) {
|
|
148
|
+
.asma-touch-target {
|
|
149
|
+
min-height: var(--asma-touch-target-size, 44px) !important;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
50
153
|
@layer utilities {
|
|
51
154
|
.flip-180 {
|
|
52
155
|
transform: rotate(180deg);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { afterEach, describe, it } from 'vitest'
|
|
2
|
+
import { expect } from 'storybook/test'
|
|
3
|
+
import { StyledButton } from 'src/components/inputs/button/StyledButton'
|
|
4
|
+
import { StyledMenuItem } from 'src/components/navigation/menu/StyledMenuItem'
|
|
5
|
+
import { StyledTab } from 'src/components/navigation/tabs/StyledTab'
|
|
6
|
+
import { StyledTabs } from 'src/components/navigation/tabs/StyledTabs'
|
|
7
|
+
import { cleanup, mount } from 'src/test-utils/renderInteraction'
|
|
8
|
+
import { page } from 'src/test-utils/interaction-api'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ASMA-8210 (TB-02) — the touch-readiness primitives in `src/styles/index.css`.
|
|
12
|
+
*
|
|
13
|
+
* Real Chromium rather than the unit project, because every claim here is a computed-style or a
|
|
14
|
+
* hit-testing question: `::after` geometry, `touch-action` and "is this point inside the target"
|
|
15
|
+
* are exactly what a DOM emulator invents. The suite mounts components directly (no stories), so
|
|
16
|
+
* it adds no VRT baseline — see `docs/a11y-keyboard-contract.md`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The `min-height` the mobile-gated `.asma-touch-target` rule declares, or null if it is missing. */
|
|
20
|
+
const touchTargetMinHeightUnderMobileMedia = (): string | null => {
|
|
21
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
22
|
+
let rules: CSSRule[]
|
|
23
|
+
try {
|
|
24
|
+
rules = Array.from(sheet.cssRules)
|
|
25
|
+
} catch {
|
|
26
|
+
continue // cross-origin sheet; ours are inline <style> tags, so nothing of interest here
|
|
27
|
+
}
|
|
28
|
+
for (const rule of rules) {
|
|
29
|
+
if (!(rule instanceof CSSMediaRule) || !rule.conditionText.includes('743px')) continue
|
|
30
|
+
for (const inner of Array.from(rule.cssRules)) {
|
|
31
|
+
if (inner instanceof CSSStyleRule && inner.selectorText.includes('.asma-touch-target')) {
|
|
32
|
+
return inner.style.getPropertyValue('min-height')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const alphaOf = (color: string): number => {
|
|
41
|
+
const channels = /rgba?\(([^)]+)\)/.exec(color)?.[1]?.split(',') ?? []
|
|
42
|
+
// rgb() has no alpha channel, so an unparsed or 3-channel colour is fully opaque.
|
|
43
|
+
return channels.length < 4 ? 1 : Number(channels[3])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('touch-readiness primitives', () => {
|
|
47
|
+
afterEach(cleanup)
|
|
48
|
+
|
|
49
|
+
it('gives .asma-hit-area a 44px pointer target without changing the drawn size', async () => {
|
|
50
|
+
// Fixed positioning pins the probe away from the body edge so the hit tests below are
|
|
51
|
+
// arithmetic, not a guess about layout. A fixed box is still the containing block its
|
|
52
|
+
// ::after resolves against.
|
|
53
|
+
const { container } = mount(
|
|
54
|
+
<button className='asma-hit-area' style={{ position: 'fixed', left: 100, top: 100, width: 24, height: 24 }}>
|
|
55
|
+
probe
|
|
56
|
+
</button>,
|
|
57
|
+
)
|
|
58
|
+
const probe = container.querySelector('button')!
|
|
59
|
+
|
|
60
|
+
// The class supplies `position: relative` WITHOUT `!important` — the rule lives outside
|
|
61
|
+
// `@layer utilities` precisely so Tailwind's `important: true` cannot importantify it
|
|
62
|
+
// (ASMA-8210 review). If this assertion fails, the rule regressed back into the layer and
|
|
63
|
+
// the inline `fixed` (and any Tailwind position utility) would silently lose again.
|
|
64
|
+
await expect(getComputedStyle(probe).position).toBe('fixed')
|
|
65
|
+
|
|
66
|
+
// The visual box is untouched — this is hit-area padding, not a size change (ASMA-8210).
|
|
67
|
+
const box = probe.getBoundingClientRect()
|
|
68
|
+
await expect(box.width).toBe(24)
|
|
69
|
+
await expect(box.height).toBe(24)
|
|
70
|
+
|
|
71
|
+
const overlay = getComputedStyle(probe, '::after')
|
|
72
|
+
await expect(overlay.minWidth).toBe('44px')
|
|
73
|
+
await expect(overlay.minHeight).toBe('44px')
|
|
74
|
+
|
|
75
|
+
// 92px is outside the 24px box (which starts at 100) but inside the 44px overlay centred on
|
|
76
|
+
// it (90 → 134). A press there must still land on the control.
|
|
77
|
+
await expect(document.elementFromPoint(92, 112)).toBe(probe)
|
|
78
|
+
// …and the target stays bounded: 85px is outside the overlay entirely.
|
|
79
|
+
await expect(document.elementFromPoint(85, 112)).not.toBe(probe)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('suppresses the tap flash and the double-tap-zoom delay on a pressable control', async () => {
|
|
83
|
+
const { container } = mount(<StyledMenuItem>Item</StyledMenuItem>)
|
|
84
|
+
const item = container.querySelector('li')!
|
|
85
|
+
|
|
86
|
+
await expect(item.classList.contains('asma-pressable')).toBe(true)
|
|
87
|
+
await expect(item.classList.contains('asma-touch-target')).toBe(true)
|
|
88
|
+
|
|
89
|
+
const style = getComputedStyle(item)
|
|
90
|
+
await expect(style.touchAction).toBe('manipulation')
|
|
91
|
+
await expect(alphaOf(style.getPropertyValue('-webkit-tap-highlight-color'))).toBe(0)
|
|
92
|
+
// Resting opacity is untouched — the pressed feedback lives entirely under `:active`, which
|
|
93
|
+
// is why no VRT baseline moves.
|
|
94
|
+
await expect(style.opacity).toBe('1')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('keeps the 44px row growth on phones only, so tablet and desktop are unchanged', async () => {
|
|
98
|
+
await expect(touchTargetMinHeightUnderMobileMedia()).toBe('var(--asma-touch-target-size, 44px)')
|
|
99
|
+
|
|
100
|
+
// The interaction project runs at 1280x720 (vite.config.ts), i.e. the VRT viewport. The rule
|
|
101
|
+
// above must not apply here, or every desktop baseline would shift.
|
|
102
|
+
await expect(window.matchMedia('(max-width: 743px)').matches).toBe(false)
|
|
103
|
+
const { container } = mount(<StyledMenuItem>Item</StyledMenuItem>)
|
|
104
|
+
await expect(getComputedStyle(container.querySelector('li')!).minHeight).toBe('40px')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('on a phone viewport, grows sub-44px rows and never shrinks a designed minimum', async () => {
|
|
108
|
+
// 390x700 puts the tester iframe under the 743px gate — the cascade CI otherwise never
|
|
109
|
+
// sees (interaction and VRT both run at 1280x720). Guards ASMA-8210 review B1: the
|
|
110
|
+
// touch-target utility is a later-source override, not a floor, so a mis-scoped opt-in
|
|
111
|
+
// would silently SHRINK a tab's effective 48x90 minimum to 44px. Both tab variants rest
|
|
112
|
+
// at 48px — the small variant's `min-h-10` loses to the base `min-h-12` by emitted
|
|
113
|
+
// order (pre-existing quirk, `cn` is plain clsx) — so NEITHER may carry the override.
|
|
114
|
+
await page.viewport(390, 700)
|
|
115
|
+
try {
|
|
116
|
+
await expect(window.matchMedia('(max-width: 743px)').matches).toBe(true)
|
|
117
|
+
const { container } = mount(
|
|
118
|
+
<>
|
|
119
|
+
<StyledTabs value={0}>
|
|
120
|
+
<StyledTab label='Default' />
|
|
121
|
+
</StyledTabs>
|
|
122
|
+
<StyledTabs size='small' value={0}>
|
|
123
|
+
<StyledTab label='Small' />
|
|
124
|
+
</StyledTabs>
|
|
125
|
+
<StyledMenuItem>Item</StyledMenuItem>
|
|
126
|
+
</>,
|
|
127
|
+
)
|
|
128
|
+
const tabs = Array.from(container.querySelectorAll('[role="tab"]'))
|
|
129
|
+
await expect(tabs.length).toBe(2)
|
|
130
|
+
|
|
131
|
+
// Tabs carry no touch-target class: the effective 48px/90px minimums survive on
|
|
132
|
+
// both variants, and neither shrinks.
|
|
133
|
+
for (const tab of tabs) {
|
|
134
|
+
const tabStyle = getComputedStyle(tab)
|
|
135
|
+
await expect(tabStyle.minHeight).toBe('48px')
|
|
136
|
+
await expect(tabStyle.minWidth).toBe('90px')
|
|
137
|
+
await expect(tab.classList.contains('asma-touch-target')).toBe(false)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Menu item: 40px → 44px, and it must not gain a min-width it never declared
|
|
141
|
+
// (Chromium resolves the initial `min-width: auto` to 0px on a block-level li).
|
|
142
|
+
const itemStyle = getComputedStyle(container.querySelector('li')!)
|
|
143
|
+
await expect(itemStyle.minHeight).toBe('44px')
|
|
144
|
+
await expect(itemStyle.minWidth).toBe('0px')
|
|
145
|
+
} finally {
|
|
146
|
+
// The desktop viewport every other test in this project assumes (vite.config.ts).
|
|
147
|
+
await page.viewport(1280, 720)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('opts the button into touch readiness without dimming its designed pressed state', async () => {
|
|
152
|
+
const { container } = mount(<StyledButton dataTest='touch-ready-probe'>Save</StyledButton>)
|
|
153
|
+
const button = container.querySelector('button')!
|
|
154
|
+
|
|
155
|
+
await expect(button.classList.contains('asma-touch-ready')).toBe(true)
|
|
156
|
+
// StyledButton.module.scss already paints `:active` from the button colour tokens. Taking
|
|
157
|
+
// `.asma-pressable` as well would layer a 0.7 opacity over those designed colours.
|
|
158
|
+
await expect(button.classList.contains('asma-pressable')).toBe(false)
|
|
159
|
+
await expect(getComputedStyle(button).touchAction).toBe('manipulation')
|
|
160
|
+
})
|
|
161
|
+
})
|