@astronox/core 0.2.0 → 0.2.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.
- package/README.md +9 -30
- package/package.json +1 -1
- package/src/components/actions/MediaTrigger/MediaTrigger.astro +12 -4
- package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
- package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
- package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
- package/src/components/forms/Checkbox/Checkbox.astro +60 -0
- package/src/components/forms/Checkbox/Checkbox.css +126 -0
- package/src/components/forms/FieldGroup/FieldGroup.astro +34 -0
- package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
- package/src/components/icons/CheckIcon.astro +22 -0
- package/src/components/index.ts +2 -0
- package/src/composition/Grid/Grid.astro +9 -0
- package/src/composition/Grid/Grid.css +52 -0
- package/src/composition/GridItem/GridItem.astro +9 -0
- package/src/composition/GridItem/GridItem.css +12 -0
- package/src/display/contracts.ts +1 -0
- package/src/patterns/Combobox/comboboxController.ts +72 -0
- package/src/patterns/Table/Table.css +2 -1
- package/src/patterns/Table/TableBody.astro +3 -1
- package/src/patterns/Table/TableBody.css +6 -0
- package/src/patterns/Table/tableFilterController.ts +60 -2
- package/src/patterns/Table/tableLayout.ts +10 -3
- package/src/styles/style.css +2 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# AstroNox Core
|
|
2
2
|
|
|
3
|
-
AstroNox Core is
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
AstroNox Core is a reusable foundation for Astro applications. It provides
|
|
4
|
+
components, composition primitives, interaction patterns, display contracts,
|
|
5
|
+
internationalization and SEO infrastructure, and a complete shared stylesheet.
|
|
6
6
|
|
|
7
7
|
Product data, product routes, branding, integrations, and business rules stay
|
|
8
8
|
in the consuming site.
|
|
@@ -33,11 +33,13 @@ import { Stack, SurfaceGroup } from '@astronox/core/composition';
|
|
|
33
33
|
</Stack>
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Load the shared visual system once from the consuming application's
|
|
36
|
+
Load the shared visual system once from the consuming application's root
|
|
37
|
+
layout:
|
|
37
38
|
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
```astro
|
|
40
|
+
---
|
|
41
|
+
import '@astronox/core/styles.css';
|
|
42
|
+
---
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Use the remaining entry points for layouts, patterns, primitives, display
|
|
@@ -50,26 +52,3 @@ and responsive media presentation are available through the `i18n`, `routing`,
|
|
|
50
52
|
definitions, translations, route names, product metadata, and assets. Generic
|
|
51
53
|
component labels can extend `defaultCoreMessages` through
|
|
52
54
|
`resolveCoreMessages()` while retaining the English fallback.
|
|
53
|
-
|
|
54
|
-
## Local Development
|
|
55
|
-
|
|
56
|
-
`astronox-core` is the only source of truth. A consuming project may link this
|
|
57
|
-
package locally while developing a shared change, but committed dependencies
|
|
58
|
-
must always reference a released package version.
|
|
59
|
-
|
|
60
|
-
The expected workflow is:
|
|
61
|
-
|
|
62
|
-
1. Keep an exact published version in the consumer manifest and lockfile.
|
|
63
|
-
2. Link the sibling package locally without touching either file with
|
|
64
|
-
`npm install --no-save --package-lock=false ../astronox-core`.
|
|
65
|
-
3. Implement and validate the generic change through the linked consumer.
|
|
66
|
-
4. Publish a prerelease or stable package version.
|
|
67
|
-
5. Update the exact consumer dependency before deployment.
|
|
68
|
-
|
|
69
|
-
Do not edit installed package files or copy `src` folders between projects.
|
|
70
|
-
|
|
71
|
-
## Releases
|
|
72
|
-
|
|
73
|
-
AstroNox Core follows semantic versioning. Public entry points are compatibility
|
|
74
|
-
boundaries; internal files may change without notice. The initial `0.x` series
|
|
75
|
-
is reserved for stabilizing the API with the first production consumers.
|
package/package.json
CHANGED
|
@@ -4,29 +4,37 @@ import type { DisplayMedia } from '../../../display/contracts';
|
|
|
4
4
|
import type { Tone } from '../../../theme/contracts';
|
|
5
5
|
import Media from '../../../primitives/media/Media/Media.astro';
|
|
6
6
|
|
|
7
|
-
type
|
|
7
|
+
type SharedProps = {
|
|
8
8
|
accentTone?: Tone;
|
|
9
9
|
label: string;
|
|
10
10
|
media: DisplayMedia;
|
|
11
11
|
tone?: Tone;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
type Props = SharedProps &
|
|
15
|
+
Omit<HTMLAttributes<'a'>, 'href' | 'type'> & {
|
|
16
|
+
href?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
const {
|
|
15
20
|
accentTone,
|
|
16
21
|
class: className,
|
|
17
22
|
'data-tone': dataTone,
|
|
23
|
+
href,
|
|
18
24
|
label,
|
|
19
25
|
media,
|
|
20
26
|
tone,
|
|
21
27
|
...attrs
|
|
22
28
|
}: Props = Astro.props;
|
|
23
29
|
const resolvedTone = tone ?? dataTone;
|
|
30
|
+
const Root = href ? 'a' : 'button';
|
|
24
31
|
---
|
|
25
32
|
|
|
26
|
-
<
|
|
33
|
+
<Root
|
|
27
34
|
{...attrs}
|
|
28
35
|
class:list={['media-trigger', className]}
|
|
29
|
-
|
|
36
|
+
href={href}
|
|
37
|
+
type={href ? undefined : 'button'}
|
|
30
38
|
aria-label={label}
|
|
31
39
|
data-tone={resolvedTone}
|
|
32
40
|
data-widget="media-trigger"
|
|
@@ -52,4 +60,4 @@ const resolvedTone = tone ?? dataTone;
|
|
|
52
60
|
</span>
|
|
53
61
|
) : null
|
|
54
62
|
}
|
|
55
|
-
</
|
|
63
|
+
</Root>
|
|
@@ -112,6 +112,7 @@ const listItems =
|
|
|
112
112
|
<div class="markdown-content__change">
|
|
113
113
|
<span class="markdown-content__change-label">
|
|
114
114
|
{node.label.map((child) => <MarkdownInline node={child} />)}
|
|
115
|
+
<span>:</span>
|
|
115
116
|
</span>
|
|
116
117
|
{node.presentation === 'statement' ? (
|
|
117
118
|
<span class="markdown-content__change-values">
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
import type { Tone } from '../../../theme/contracts';
|
|
4
|
+
import CheckIcon from '../../icons/CheckIcon.astro';
|
|
5
|
+
import CloseIcon from '../../icons/CloseIcon.astro';
|
|
6
|
+
|
|
7
|
+
type Props = Omit<HTMLAttributes<'input'>, 'class' | 'id' | 'type'> & {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
invalid?: boolean;
|
|
12
|
+
tone?: Tone;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
'aria-invalid': ariaInvalid,
|
|
17
|
+
class: className,
|
|
18
|
+
'data-tone': dataTone,
|
|
19
|
+
disabled = false,
|
|
20
|
+
id,
|
|
21
|
+
invalid = false,
|
|
22
|
+
label,
|
|
23
|
+
tone,
|
|
24
|
+
...attrs
|
|
25
|
+
}: Props = Astro.props;
|
|
26
|
+
const resolvedInvalid =
|
|
27
|
+
invalid || ariaInvalid === true || ariaInvalid === 'true';
|
|
28
|
+
const resolvedTone = tone ?? dataTone;
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<label
|
|
32
|
+
class:list={['checkbox-field', className]}
|
|
33
|
+
for={id}
|
|
34
|
+
data-disabled={disabled ? true : undefined}
|
|
35
|
+
data-invalid={resolvedInvalid ? true : undefined}
|
|
36
|
+
data-tone={resolvedTone}
|
|
37
|
+
data-widget="checkbox"
|
|
38
|
+
>
|
|
39
|
+
<input
|
|
40
|
+
{...attrs}
|
|
41
|
+
id={id}
|
|
42
|
+
class="checkbox"
|
|
43
|
+
type="checkbox"
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
aria-invalid={resolvedInvalid ? 'true' : ariaInvalid}
|
|
46
|
+
/>
|
|
47
|
+
<span class="checkbox__label">
|
|
48
|
+
<span class="checkbox__label-text">{label}</span>
|
|
49
|
+
<span class="checkbox__state" aria-hidden="true">
|
|
50
|
+
<CheckIcon
|
|
51
|
+
class="checkbox__icon checkbox__icon--checked"
|
|
52
|
+
data-tone="success"
|
|
53
|
+
/>
|
|
54
|
+
<CloseIcon
|
|
55
|
+
class="checkbox__icon checkbox__icon--unchecked"
|
|
56
|
+
data-tone="danger"
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
</span>
|
|
60
|
+
</label>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
.checkbox-field {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: grid;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
min-inline-size: 0;
|
|
6
|
+
inline-size: 100%;
|
|
7
|
+
block-size: var(--nox-field-control-block-size);
|
|
8
|
+
place-items: center;
|
|
9
|
+
padding: var(--nox-space-2);
|
|
10
|
+
overflow: clip;
|
|
11
|
+
border: var(--nox-border-width) solid var(--nox-field-border);
|
|
12
|
+
border-radius: var(--nox-radius-sm);
|
|
13
|
+
background: var(--nox-field-background-disabled);
|
|
14
|
+
color: var(--nox-text-disabled);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
transition:
|
|
17
|
+
border-color var(--nox-motion-duration-sm) var(--nox-motion-easing-standard),
|
|
18
|
+
box-shadow var(--nox-motion-duration-sm) var(--nox-motion-easing-standard),
|
|
19
|
+
background-color var(--nox-motion-duration-sm)
|
|
20
|
+
var(--nox-motion-easing-standard),
|
|
21
|
+
color var(--nox-motion-duration-sm) var(--nox-motion-easing-standard);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.checkbox {
|
|
25
|
+
position: absolute;
|
|
26
|
+
z-index: 1;
|
|
27
|
+
inset: 0;
|
|
28
|
+
inline-size: 100%;
|
|
29
|
+
block-size: 100%;
|
|
30
|
+
margin: 0;
|
|
31
|
+
opacity: 0;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.checkbox__label {
|
|
36
|
+
display: inline-grid;
|
|
37
|
+
min-inline-size: 0;
|
|
38
|
+
max-inline-size: 100%;
|
|
39
|
+
grid-template-columns: 0.75rem minmax(0, auto) 0.75rem;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: var(--nox-space-1);
|
|
42
|
+
color: inherit;
|
|
43
|
+
font-size: var(--nox-font-size-xs);
|
|
44
|
+
font-weight: var(--nox-font-weight-strong);
|
|
45
|
+
letter-spacing: var(--nox-letter-spacing-wide);
|
|
46
|
+
line-height: 1;
|
|
47
|
+
overflow-wrap: anywhere;
|
|
48
|
+
text-align: center;
|
|
49
|
+
text-transform: uppercase;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.checkbox__label::before {
|
|
53
|
+
content: '';
|
|
54
|
+
inline-size: 0.75rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.checkbox__label-text {
|
|
58
|
+
min-inline-size: 0;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.checkbox__state {
|
|
65
|
+
display: grid;
|
|
66
|
+
inline-size: 0.75rem;
|
|
67
|
+
block-size: 0.75rem;
|
|
68
|
+
place-items: center;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.checkbox__icon {
|
|
72
|
+
grid-area: 1 / 1;
|
|
73
|
+
inline-size: 100%;
|
|
74
|
+
block-size: 100%;
|
|
75
|
+
transition: opacity var(--nox-motion-duration-sm)
|
|
76
|
+
var(--nox-motion-easing-standard);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.checkbox__icon--checked {
|
|
80
|
+
color: var(--nox-tone-color);
|
|
81
|
+
opacity: 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.checkbox__icon--unchecked {
|
|
85
|
+
color: var(--nox-text-disabled);
|
|
86
|
+
opacity: 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.checkbox-field:has(.checkbox:checked) {
|
|
90
|
+
background: var(--nox-field-background);
|
|
91
|
+
color: var(--nox-tone-text);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.checkbox:checked ~ .checkbox__label .checkbox__icon--checked {
|
|
95
|
+
opacity: 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.checkbox:checked ~ .checkbox__label .checkbox__icon--unchecked {
|
|
99
|
+
opacity: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.checkbox-field:has(.checkbox:focus-visible) {
|
|
103
|
+
border-color: var(--nox-field-border-active);
|
|
104
|
+
box-shadow: var(--nox-field-focus-shadow);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.checkbox-field[data-invalid] {
|
|
108
|
+
border-color: var(--nox-field-border-invalid);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.checkbox-field[data-disabled] {
|
|
112
|
+
border-color: var(--nox-field-border);
|
|
113
|
+
background: var(--nox-field-background-disabled);
|
|
114
|
+
color: var(--nox-text-disabled);
|
|
115
|
+
cursor: not-allowed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.checkbox-field[data-disabled] .checkbox {
|
|
119
|
+
cursor: not-allowed;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@media (hover: hover) and (pointer: fine) {
|
|
123
|
+
.checkbox-field:not([data-disabled]):hover {
|
|
124
|
+
border-color: var(--nox-field-border-hover);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
import type { Tone } from '../../../theme/contracts';
|
|
4
|
+
|
|
5
|
+
type Props = Omit<HTMLAttributes<'fieldset'>, 'disabled'> & {
|
|
6
|
+
label: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
class: className,
|
|
13
|
+
'data-tone': dataTone,
|
|
14
|
+
disabled = false,
|
|
15
|
+
label,
|
|
16
|
+
tone,
|
|
17
|
+
...attrs
|
|
18
|
+
}: Props = Astro.props;
|
|
19
|
+
const resolvedTone = tone ?? dataTone;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<fieldset
|
|
23
|
+
{...attrs}
|
|
24
|
+
class:list={['field-group', className]}
|
|
25
|
+
disabled={disabled}
|
|
26
|
+
data-disabled={disabled ? true : undefined}
|
|
27
|
+
data-tone={resolvedTone}
|
|
28
|
+
data-widget="field-group"
|
|
29
|
+
>
|
|
30
|
+
<legend class="field-group__label">{label}</legend>
|
|
31
|
+
<div class="field-group__content">
|
|
32
|
+
<slot />
|
|
33
|
+
</div>
|
|
34
|
+
</fieldset>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
.field-group {
|
|
2
|
+
min-inline-size: 0;
|
|
3
|
+
margin: 0;
|
|
4
|
+
padding: 0;
|
|
5
|
+
border: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.field-group__label {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
min-inline-size: 0;
|
|
11
|
+
margin: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
align-items: center;
|
|
14
|
+
color: var(--nox-tone-text);
|
|
15
|
+
font-size: var(--nox-font-size-sm);
|
|
16
|
+
font-weight: var(--nox-font-weight-strong);
|
|
17
|
+
letter-spacing: var(--nox-letter-spacing-wide);
|
|
18
|
+
line-height: 1.25;
|
|
19
|
+
overflow-wrap: anywhere;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.field-group__content {
|
|
24
|
+
min-inline-size: 0;
|
|
25
|
+
margin-block-start: var(--nox-space-2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.field-group[data-disabled] .field-group__label {
|
|
29
|
+
color: var(--nox-text-disabled);
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
|
|
6
|
+
const attrs: Props = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<svg
|
|
10
|
+
{...attrs}
|
|
11
|
+
viewBox="0 0 32 32"
|
|
12
|
+
fill="none"
|
|
13
|
+
aria-hidden="true"
|
|
14
|
+
focusable="false"
|
|
15
|
+
>
|
|
16
|
+
<path
|
|
17
|
+
d="M7 16.5L13 22.5L25 9.5"
|
|
18
|
+
stroke="currentColor"
|
|
19
|
+
stroke-width="2.5"
|
|
20
|
+
stroke-linecap="round"
|
|
21
|
+
stroke-linejoin="round"></path>
|
|
22
|
+
</svg>
|
package/src/components/index.ts
CHANGED
|
@@ -15,7 +15,9 @@ export { default as IconList } from './data-display/IconList/IconList.astro';
|
|
|
15
15
|
export { default as MetricTile } from './data-display/MetricTile/MetricTile.astro';
|
|
16
16
|
export { default as Tag } from './data-display/Tag/Tag.astro';
|
|
17
17
|
export { default as TextPair } from './data-display/TextPair/TextPair.astro';
|
|
18
|
+
export { default as Checkbox } from './forms/Checkbox/Checkbox.astro';
|
|
18
19
|
export { default as FieldError } from './forms/FieldError/FieldError.astro';
|
|
20
|
+
export { default as FieldGroup } from './forms/FieldGroup/FieldGroup.astro';
|
|
19
21
|
export { default as FieldLabel } from './forms/FieldLabel/FieldLabel.astro';
|
|
20
22
|
export { default as TextInput } from './forms/TextInput/TextInput.astro';
|
|
21
23
|
export { default as Spinner } from './feedback/Spinner/Spinner.astro';
|
|
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'astro/types';
|
|
|
3
3
|
|
|
4
4
|
type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
5
5
|
type GridColumns = 1 | 2 | 3 | 4 | 'content-sidebar';
|
|
6
|
+
type GridFlow = 'row' | 'column';
|
|
6
7
|
type GridRows = 1 | 2 | 3 | 4;
|
|
7
8
|
type GridItemAlignment = 'start' | 'center' | 'end' | 'stretch';
|
|
8
9
|
type GridCollapseAt = 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
@@ -14,8 +15,10 @@ type Props = Omit<HTMLAttributes<'div'>, 'class' | 'style'> & {
|
|
|
14
15
|
collapseAt?: GridCollapseAt;
|
|
15
16
|
class?: string;
|
|
16
17
|
compactColumns?: 1 | 2 | 3 | 4;
|
|
18
|
+
compactFlow?: GridFlow;
|
|
17
19
|
gap?: GridGap;
|
|
18
20
|
columns?: GridColumns;
|
|
21
|
+
flow?: GridFlow;
|
|
19
22
|
justifyItems?: GridItemAlignment;
|
|
20
23
|
lastRow?: GridLastRow;
|
|
21
24
|
minTrackWidth?: string;
|
|
@@ -28,8 +31,10 @@ const {
|
|
|
28
31
|
collapseAt = 'md',
|
|
29
32
|
class: className,
|
|
30
33
|
compactColumns,
|
|
34
|
+
compactFlow,
|
|
31
35
|
gap = 'md',
|
|
32
36
|
columns,
|
|
37
|
+
flow = 'row',
|
|
33
38
|
justifyItems = 'stretch',
|
|
34
39
|
lastRow = 'preserve',
|
|
35
40
|
minTrackWidth = '16rem',
|
|
@@ -52,6 +57,8 @@ const gridVariables = {
|
|
|
52
57
|
...(compactColumns
|
|
53
58
|
? { '--grid-compact-columns': String(compactColumns) }
|
|
54
59
|
: {}),
|
|
60
|
+
'--grid-compact-flow': compactFlow ?? flow,
|
|
61
|
+
'--grid-flow': flow,
|
|
55
62
|
...(rows ? { '--grid-rows': String(rows) } : {}),
|
|
56
63
|
'--grid-align-items': alignItems,
|
|
57
64
|
'--grid-justify-items': justifyItems,
|
|
@@ -69,7 +76,9 @@ const gridStyle = Object.entries(gridVariables)
|
|
|
69
76
|
data-align-items={alignItems}
|
|
70
77
|
data-collapse-at={collapseMode}
|
|
71
78
|
data-compact-columns={compactColumns}
|
|
79
|
+
data-compact-flow={compactFlow}
|
|
72
80
|
data-columns={typeof columns === 'number' ? columns : undefined}
|
|
81
|
+
data-flow={flow}
|
|
73
82
|
data-gap={gap}
|
|
74
83
|
data-justify-items={justifyItems}
|
|
75
84
|
data-layout={layout}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
min-width: 0;
|
|
9
9
|
align-items: var(--grid-align-items);
|
|
10
10
|
gap: var(--grid-gap);
|
|
11
|
+
grid-auto-flow: var(--grid-flow);
|
|
11
12
|
justify-items: var(--grid-justify-items);
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -300,7 +301,58 @@
|
|
|
300
301
|
|
|
301
302
|
@container (width < 24rem) {
|
|
302
303
|
.grid[data-compact-columns] {
|
|
304
|
+
grid-auto-flow: var(--grid-compact-flow);
|
|
303
305
|
grid-template-columns: repeat(var(--grid-compact-columns), minmax(0, 1fr));
|
|
306
|
+
grid-template-rows: none;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.grid[data-compact-columns]
|
|
310
|
+
> [data-composition='grid-item'][data-compact-span='1'] {
|
|
311
|
+
grid-column: span 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.grid[data-compact-columns]
|
|
315
|
+
> [data-composition='grid-item'][data-compact-span='2'] {
|
|
316
|
+
grid-column: span 2;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.grid[data-compact-columns]
|
|
320
|
+
> [data-composition='grid-item'][data-compact-span='3'] {
|
|
321
|
+
grid-column: span 3;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.grid[data-compact-columns]
|
|
325
|
+
> [data-composition='grid-item'][data-compact-span='4'] {
|
|
326
|
+
grid-column: span 4;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.grid[data-compact-columns]
|
|
330
|
+
> [data-composition='grid-item'][data-compact-row-span='1'] {
|
|
331
|
+
grid-row: span 1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.grid[data-compact-columns]
|
|
335
|
+
> [data-composition='grid-item'][data-compact-row-span='2'] {
|
|
336
|
+
grid-row: span 2;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.grid[data-compact-columns]
|
|
340
|
+
> [data-composition='grid-item'][data-compact-row-span='3'] {
|
|
341
|
+
grid-row: span 3;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.grid[data-compact-columns]
|
|
345
|
+
> [data-composition='grid-item'][data-compact-row-span='4'] {
|
|
346
|
+
grid-row: span 4;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.grid[data-compact-columns]
|
|
350
|
+
> [data-composition='grid-item'][data-sizing='fill-track'] {
|
|
351
|
+
--grid-item-fill-track-contain: none;
|
|
352
|
+
--grid-item-fill-track-content-block-size: auto;
|
|
353
|
+
--grid-item-fill-track-content-inset: auto;
|
|
354
|
+
--grid-item-fill-track-content-position: static;
|
|
355
|
+
--grid-item-fill-track-overflow: visible;
|
|
304
356
|
}
|
|
305
357
|
|
|
306
358
|
.grid[data-last-row='fill'][data-compact-columns] > * {
|
|
@@ -14,8 +14,11 @@ type Props = Omit<GridItemAttributes, 'class' | 'style'> & {
|
|
|
14
14
|
alignSelf?: GridItemAlignment;
|
|
15
15
|
class?: string;
|
|
16
16
|
collapseOrder?: GridItemCollapseOrder;
|
|
17
|
+
compactRowSpan?: GridItemSpan;
|
|
18
|
+
compactSpan?: GridItemSpan;
|
|
17
19
|
justifySelf?: GridItemAlignment;
|
|
18
20
|
position?: GridItemPosition;
|
|
21
|
+
rowSpan?: GridItemSpan;
|
|
19
22
|
sizing?: GridItemSizing;
|
|
20
23
|
span?: GridItemSpan;
|
|
21
24
|
};
|
|
@@ -25,8 +28,11 @@ const {
|
|
|
25
28
|
alignSelf = 'auto',
|
|
26
29
|
class: className,
|
|
27
30
|
collapseOrder = 'preserve',
|
|
31
|
+
compactRowSpan,
|
|
32
|
+
compactSpan,
|
|
28
33
|
justifySelf = 'auto',
|
|
29
34
|
position = 'static',
|
|
35
|
+
rowSpan = 1,
|
|
30
36
|
sizing = 'auto',
|
|
31
37
|
span = 1,
|
|
32
38
|
...attrs
|
|
@@ -39,8 +45,11 @@ const {
|
|
|
39
45
|
data-composition="grid-item"
|
|
40
46
|
data-align-self={alignSelf}
|
|
41
47
|
data-collapse-order={collapseOrder}
|
|
48
|
+
data-compact-row-span={compactRowSpan}
|
|
49
|
+
data-compact-span={compactSpan}
|
|
42
50
|
data-justify-self={justifySelf}
|
|
43
51
|
data-position={position}
|
|
52
|
+
data-row-span={rowSpan}
|
|
44
53
|
data-sizing={sizing}
|
|
45
54
|
data-span={span}
|
|
46
55
|
>
|
|
@@ -69,6 +69,18 @@
|
|
|
69
69
|
grid-column: span 4;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
.grid-item[data-row-span='2'] {
|
|
73
|
+
grid-row: span 2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.grid-item[data-row-span='3'] {
|
|
77
|
+
grid-row: span 3;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.grid-item[data-row-span='4'] {
|
|
81
|
+
grid-row: span 4;
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
.grid-item[data-sizing='fill-track'] {
|
|
73
85
|
--grid-item-fill-track-contain: size layout;
|
|
74
86
|
--grid-item-fill-track-content-block-size: 100%;
|
package/src/display/contracts.ts
CHANGED
|
@@ -12,6 +12,11 @@ type SetComboboxValueOptions = {
|
|
|
12
12
|
emit?: boolean;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export type ComboboxNumberBounds = {
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
type NumberBounds = {
|
|
16
21
|
min: number;
|
|
17
22
|
max: number;
|
|
@@ -93,6 +98,17 @@ function normalizeNumber(value: number, bounds: NumberBounds): number {
|
|
|
93
98
|
return Math.min(bounds.max, Math.max(bounds.min, stepped));
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
function syncNumberOptionBounds(
|
|
102
|
+
root: HTMLElement,
|
|
103
|
+
bounds: NumberBounds,
|
|
104
|
+
): void {
|
|
105
|
+
for (const option of getOptions(root)) {
|
|
106
|
+
const value = Number(option.dataset.comboboxOption);
|
|
107
|
+
option.hidden =
|
|
108
|
+
!Number.isFinite(value) || value < bounds.min || value > bounds.max;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
export function setComboboxValues(
|
|
97
113
|
root: HTMLElement,
|
|
98
114
|
requestedValues: readonly string[],
|
|
@@ -186,6 +202,43 @@ export function setComboboxNumberValue(
|
|
|
186
202
|
}
|
|
187
203
|
}
|
|
188
204
|
|
|
205
|
+
export function setComboboxNumberBounds(
|
|
206
|
+
root: HTMLElement,
|
|
207
|
+
{ min, max }: ComboboxNumberBounds,
|
|
208
|
+
): void {
|
|
209
|
+
if (root.dataset.comboboxMode !== 'number') return;
|
|
210
|
+
|
|
211
|
+
const currentBounds = getNumberBounds(root);
|
|
212
|
+
const optionValues = getOptions(root)
|
|
213
|
+
.map((option) => Number(option.dataset.comboboxOption))
|
|
214
|
+
.filter(Number.isFinite);
|
|
215
|
+
const renderedValues = new Set(optionValues);
|
|
216
|
+
if (
|
|
217
|
+
!currentBounds ||
|
|
218
|
+
!Number.isInteger(min) ||
|
|
219
|
+
!Number.isInteger(max) ||
|
|
220
|
+
min > max ||
|
|
221
|
+
!renderedValues.has(min) ||
|
|
222
|
+
!renderedValues.has(max)
|
|
223
|
+
) {
|
|
224
|
+
throw new RangeError(
|
|
225
|
+
'Combobox number bounds must be ordered rendered values aligned to the configured step',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
root.dataset.comboboxNumberMin = String(min);
|
|
230
|
+
root.dataset.comboboxNumberMax = String(max);
|
|
231
|
+
const bounds = getNumberBounds(root);
|
|
232
|
+
const input = root.querySelector<HTMLInputElement>(
|
|
233
|
+
'[data-combobox-number-input]',
|
|
234
|
+
);
|
|
235
|
+
if (!bounds || !input) return;
|
|
236
|
+
|
|
237
|
+
syncNumberOptionBounds(root, bounds);
|
|
238
|
+
setComboboxNumberValue(root, Number(input.value));
|
|
239
|
+
root.dispatchEvent(new Event('combobox:number-bounds-change'));
|
|
240
|
+
}
|
|
241
|
+
|
|
189
242
|
export function initCombobox(root: HTMLElement): () => void {
|
|
190
243
|
if (root.getAttribute(initializedAttribute) === 'true') return noop;
|
|
191
244
|
|
|
@@ -343,6 +396,12 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
343
396
|
: null;
|
|
344
397
|
|
|
345
398
|
const resetFilter = (): void => {
|
|
399
|
+
if (mode === 'number') {
|
|
400
|
+
const bounds = getNumberBounds(root);
|
|
401
|
+
if (bounds) syncNumberOptionBounds(root, bounds);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
346
405
|
if (search) search.value = '';
|
|
347
406
|
for (const option of getOptions(root)) option.hidden = false;
|
|
348
407
|
if (empty) empty.hidden = getOptions(root).length > 0;
|
|
@@ -736,6 +795,11 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
736
795
|
close();
|
|
737
796
|
};
|
|
738
797
|
|
|
798
|
+
const handleNumberBoundsChange = (): void => {
|
|
799
|
+
setActiveOption(null);
|
|
800
|
+
schedulePanelPosition();
|
|
801
|
+
};
|
|
802
|
+
|
|
739
803
|
if (mode === 'options' && trigger && search) {
|
|
740
804
|
control.addEventListener('click', handleOptionsControlClick);
|
|
741
805
|
trigger.addEventListener('click', handleOptionsTriggerClick);
|
|
@@ -759,6 +823,10 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
759
823
|
}
|
|
760
824
|
optionsRoot.addEventListener('click', handleOptionClick);
|
|
761
825
|
root.addEventListener('combobox:clear', handleClear);
|
|
826
|
+
root.addEventListener(
|
|
827
|
+
'combobox:number-bounds-change',
|
|
828
|
+
handleNumberBoundsChange,
|
|
829
|
+
);
|
|
762
830
|
|
|
763
831
|
return () => {
|
|
764
832
|
close();
|
|
@@ -778,6 +846,10 @@ export function initCombobox(root: HTMLElement): () => void {
|
|
|
778
846
|
}
|
|
779
847
|
optionsRoot.removeEventListener('click', handleOptionClick);
|
|
780
848
|
root.removeEventListener('combobox:clear', handleClear);
|
|
849
|
+
root.removeEventListener(
|
|
850
|
+
'combobox:number-bounds-change',
|
|
851
|
+
handleNumberBoundsChange,
|
|
852
|
+
);
|
|
781
853
|
panel.style.removeProperty('--combobox-panel-inline-start');
|
|
782
854
|
panel.style.removeProperty('--combobox-panel-block-start');
|
|
783
855
|
panel.style.removeProperty('--combobox-panel-inline-size');
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
min-inline-size: 0;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
.table[data-table-column-density='compact']
|
|
20
|
+
.table[data-table-column-density='compact'],
|
|
21
|
+
.table[data-table-layout='index'] {
|
|
21
22
|
--table-cell-padding-inline: var(--nox-space-1);
|
|
22
23
|
--table-edge-padding-inline: var(--nox-space-3);
|
|
23
24
|
--table-header-letter-spacing: 0;
|
|
@@ -102,12 +102,14 @@ const stateMessage =
|
|
|
102
102
|
</tr>
|
|
103
103
|
) : (
|
|
104
104
|
rows.map((row) => {
|
|
105
|
-
const rowHref = row.href;
|
|
105
|
+
const rowHref = row.disabled ? undefined : row.href;
|
|
106
106
|
|
|
107
107
|
return (
|
|
108
108
|
<tr
|
|
109
109
|
class="table-body__row"
|
|
110
110
|
hidden={row.hidden}
|
|
111
|
+
aria-disabled={row.disabled ? 'true' : undefined}
|
|
112
|
+
data-disabled={row.disabled ? 'true' : undefined}
|
|
111
113
|
data-table-row
|
|
112
114
|
data-table-row-key={row.key}
|
|
113
115
|
data-table-row-href={rowHref}
|
|
@@ -166,6 +166,12 @@
|
|
|
166
166
|
cursor: pointer;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
.table-body__row[data-disabled='true'] {
|
|
170
|
+
opacity: 0.4;
|
|
171
|
+
filter: grayscale(0.65);
|
|
172
|
+
cursor: not-allowed;
|
|
173
|
+
}
|
|
174
|
+
|
|
169
175
|
.table-body__row[data-table-row-href]:focus-within {
|
|
170
176
|
--table-primary-link-color: var(--nox-link-hover);
|
|
171
177
|
}
|
|
@@ -13,8 +13,57 @@ type CachedFilterValues = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
const initializedAttribute = 'data-table-filters-initialized';
|
|
16
|
+
const externalFiltersAttribute = 'data-table-filter-external';
|
|
17
|
+
const exclusionsAttribute = 'data-table-filter-exclusions';
|
|
18
|
+
const refreshEvent = 'table-filter:refresh';
|
|
16
19
|
const noop = (): void => undefined;
|
|
17
20
|
|
|
21
|
+
export function setTableExternalFilterActive(
|
|
22
|
+
root: HTMLElement,
|
|
23
|
+
key: string,
|
|
24
|
+
active: boolean,
|
|
25
|
+
): void {
|
|
26
|
+
setTokenAttribute(root, externalFiltersAttribute, key, active);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setTableRowFilterExclusion(
|
|
30
|
+
row: HTMLElement,
|
|
31
|
+
key: string,
|
|
32
|
+
excluded: boolean,
|
|
33
|
+
): void {
|
|
34
|
+
setTokenAttribute(row, exclusionsAttribute, key, excluded);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setTokenAttribute(
|
|
38
|
+
element: HTMLElement,
|
|
39
|
+
attribute: string,
|
|
40
|
+
key: string,
|
|
41
|
+
enabled: boolean,
|
|
42
|
+
): void {
|
|
43
|
+
const normalizedKey = key.trim();
|
|
44
|
+
if (!normalizedKey || /\s/.test(normalizedKey)) {
|
|
45
|
+
throw new Error('Table filter keys must be non-empty tokens');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tokens = new Set(
|
|
49
|
+
(element.getAttribute(attribute) ?? '')
|
|
50
|
+
.split(/\s+/)
|
|
51
|
+
.filter(Boolean),
|
|
52
|
+
);
|
|
53
|
+
if (enabled) tokens.add(normalizedKey);
|
|
54
|
+
else tokens.delete(normalizedKey);
|
|
55
|
+
|
|
56
|
+
if (tokens.size > 0) {
|
|
57
|
+
element.setAttribute(attribute, [...tokens].join(' '));
|
|
58
|
+
} else {
|
|
59
|
+
element.removeAttribute(attribute);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function refreshTableFilters(root: HTMLElement): void {
|
|
64
|
+
root.dispatchEvent(new Event(refreshEvent));
|
|
65
|
+
}
|
|
66
|
+
|
|
18
67
|
export function initTableFilters(root: HTMLElement): () => void {
|
|
19
68
|
if (root.getAttribute(initializedAttribute) === 'true') return noop;
|
|
20
69
|
|
|
@@ -79,7 +128,11 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
79
128
|
const activeSelections = [...selections.entries()].filter(
|
|
80
129
|
([, value]) => value.length > 0,
|
|
81
130
|
);
|
|
82
|
-
const
|
|
131
|
+
const hasExternalFilter =
|
|
132
|
+
root.hasAttribute(externalFiltersAttribute) ||
|
|
133
|
+
rows.some((row) => row.hasAttribute(exclusionsAttribute));
|
|
134
|
+
const active =
|
|
135
|
+
query.length > 0 || activeSelections.length > 0 || hasExternalFilter;
|
|
83
136
|
let visibleRows = 0;
|
|
84
137
|
|
|
85
138
|
for (const row of rows) {
|
|
@@ -92,7 +145,10 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
92
145
|
([field, selectedValue]) =>
|
|
93
146
|
(cached.values[field] ?? []).includes(selectedValue),
|
|
94
147
|
);
|
|
95
|
-
const visible =
|
|
148
|
+
const visible =
|
|
149
|
+
matchesSearch &&
|
|
150
|
+
matchesSelections &&
|
|
151
|
+
!row.hasAttribute(exclusionsAttribute);
|
|
96
152
|
|
|
97
153
|
if (row.hidden !== !visible) row.hidden = !visible;
|
|
98
154
|
if (visible) visibleRows += 1;
|
|
@@ -168,6 +224,7 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
168
224
|
applyFilters();
|
|
169
225
|
search?.addEventListener('input', handleSearch);
|
|
170
226
|
root.addEventListener('combobox:change', handleComboboxChange);
|
|
227
|
+
root.addEventListener(refreshEvent, scheduleFilters);
|
|
171
228
|
table.addEventListener('table:rows-appended', handleRowsAppended);
|
|
172
229
|
table.addEventListener(
|
|
173
230
|
'table:progressive-complete',
|
|
@@ -182,6 +239,7 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
182
239
|
}
|
|
183
240
|
search?.removeEventListener('input', handleSearch);
|
|
184
241
|
root.removeEventListener('combobox:change', handleComboboxChange);
|
|
242
|
+
root.removeEventListener(refreshEvent, scheduleFilters);
|
|
185
243
|
table.removeEventListener('table:rows-appended', handleRowsAppended);
|
|
186
244
|
table.removeEventListener(
|
|
187
245
|
'table:progressive-complete',
|
|
@@ -25,7 +25,7 @@ const compactIndexRoleMinimums = {
|
|
|
25
25
|
|
|
26
26
|
const regularIndexRoleMinimums = {
|
|
27
27
|
...compactIndexRoleMinimums,
|
|
28
|
-
value:
|
|
28
|
+
value: 8.5,
|
|
29
29
|
} satisfies Record<TableColumnRole, number>;
|
|
30
30
|
|
|
31
31
|
export function getTableColumnDensity(
|
|
@@ -39,6 +39,7 @@ export function getTableColumnTemplate(
|
|
|
39
39
|
layout: TableLayout = 'default',
|
|
40
40
|
indexSizing: TableIndexSizing = 'compact',
|
|
41
41
|
): string {
|
|
42
|
+
const columnDensity = getTableColumnDensity(columns);
|
|
42
43
|
const indexMinimums = getIndexRoleMinimums(columns);
|
|
43
44
|
|
|
44
45
|
return columns
|
|
@@ -46,7 +47,7 @@ export function getTableColumnTemplate(
|
|
|
46
47
|
layout === 'index'
|
|
47
48
|
? indexSizing === 'overflow'
|
|
48
49
|
? 'max-content'
|
|
49
|
-
: getIndexColumnTrack(column.role, indexMinimums)
|
|
50
|
+
: getIndexColumnTrack(column.role, indexMinimums, columnDensity)
|
|
50
51
|
: getDefaultColumnTrack(column),
|
|
51
52
|
)
|
|
52
53
|
.join(' ');
|
|
@@ -91,9 +92,15 @@ function getIndexRoleMinimums(
|
|
|
91
92
|
function getIndexColumnTrack(
|
|
92
93
|
role: TableColumnRole,
|
|
93
94
|
minimums: Record<TableColumnRole, number>,
|
|
95
|
+
density: 'compact' | 'regular',
|
|
94
96
|
): string {
|
|
95
97
|
const minimum = `${minimums[role]}rem`;
|
|
96
|
-
|
|
98
|
+
if (role === 'primary') {
|
|
99
|
+
const weight = density === 'compact' ? 2 : 1;
|
|
100
|
+
return `minmax(${minimum}, ${weight}fr)`;
|
|
101
|
+
}
|
|
102
|
+
if (role === 'value') return `minmax(${minimum}, 1fr)`;
|
|
103
|
+
return minimum;
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
function getDefaultColumnTrack(column: TableColumn): string {
|
package/src/styles/style.css
CHANGED
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
@import '../components/data-display/Tag/Tag.css' layer(components);
|
|
42
42
|
@import '../components/data-display/TextPair/TextPair.css'
|
|
43
43
|
layer(components);
|
|
44
|
+
@import '../components/forms/Checkbox/Checkbox.css' layer(components);
|
|
44
45
|
@import '../components/forms/FieldLabel/FieldLabel.css' layer(components);
|
|
45
46
|
@import '../components/forms/FieldError/FieldError.css' layer(components);
|
|
47
|
+
@import '../components/forms/FieldGroup/FieldGroup.css' layer(components);
|
|
46
48
|
@import '../components/forms/TextInput/TextInput.css' layer(components);
|
|
47
49
|
@import '../components/feedback/Tooltip/Tooltip.css' layer(components);
|
|
48
50
|
@import '../components/feedback/Spinner/Spinner.css' layer(components);
|