@astronox/core 0.1.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 +15 -27
- package/package.json +3 -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/components/navigation/Breadcrumbs/Breadcrumbs.astro +2 -1
- package/src/components/navigation/SubNavigation/SubNavigation.astro +2 -1
- 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/display/index.ts +1 -0
- package/src/display/responsiveMedia.ts +38 -0
- package/src/i18n/LocalePreferenceBootstrap.astro +40 -0
- package/src/i18n/catalog.ts +203 -0
- package/src/i18n/config.ts +112 -0
- package/src/i18n/coreMessages.ts +37 -0
- package/src/i18n/formatters.ts +34 -0
- package/src/i18n/index.ts +6 -0
- package/src/i18n/preference.ts +58 -0
- package/src/index.ts +2 -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/routing/index.ts +1 -0
- package/src/routing/localized.ts +155 -0
- package/src/seo/index.ts +1 -0
- package/src/seo/localizedMeta.ts +58 -0
- package/src/styles/style.css +2 -0
|
@@ -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 {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './localized';
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { LocaleConfig, LocaleDefinition } from '../i18n/config';
|
|
2
|
+
import { normalizePathname } from '../seo/url';
|
|
3
|
+
|
|
4
|
+
export type LocalizedRoutePolicy = 'localized' | 'technical';
|
|
5
|
+
|
|
6
|
+
export type LocalizedRouter<TLocale extends string> = {
|
|
7
|
+
localizePath(path: string, locale: TLocale): string;
|
|
8
|
+
stripLocalePrefix(path: string): { locale: TLocale; path: string };
|
|
9
|
+
resolveLocale(path: string): TLocale;
|
|
10
|
+
getEquivalentPath(path: string, locale: TLocale): string;
|
|
11
|
+
getRoutePolicy(path: string): LocalizedRoutePolicy;
|
|
12
|
+
normalizePath(path: string): string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createLocalizedRouter<
|
|
16
|
+
TDefinition extends LocaleDefinition,
|
|
17
|
+
>({
|
|
18
|
+
localeConfig,
|
|
19
|
+
localizedRoots = ['/'],
|
|
20
|
+
technicalRoots = [],
|
|
21
|
+
}: {
|
|
22
|
+
localeConfig: LocaleConfig<TDefinition>;
|
|
23
|
+
localizedRoots?: readonly string[];
|
|
24
|
+
technicalRoots?: readonly string[];
|
|
25
|
+
}): LocalizedRouter<TDefinition['id']> {
|
|
26
|
+
type Locale = TDefinition['id'];
|
|
27
|
+
|
|
28
|
+
const normalizedLocalizedRoots = localizedRoots.map(normalizePathname);
|
|
29
|
+
const normalizedTechnicalRoots = technicalRoots.map(normalizePathname);
|
|
30
|
+
|
|
31
|
+
const localizePath = (path: string, locale: Locale): string => {
|
|
32
|
+
const { pathname, suffix } = splitPathReference(path);
|
|
33
|
+
const normalizedPath = normalizePathname(pathname);
|
|
34
|
+
if (
|
|
35
|
+
!localeConfig.prefixDefaultLocale &&
|
|
36
|
+
locale === localeConfig.defaultLocale
|
|
37
|
+
) {
|
|
38
|
+
return `${normalizedPath}${suffix}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const prefix = `/${localeConfig.getDefinition(locale).urlCode}`;
|
|
42
|
+
const localizedPath = normalizedPath === '/'
|
|
43
|
+
? prefix
|
|
44
|
+
: normalizePathname(`${prefix}${normalizedPath}`);
|
|
45
|
+
return `${localizedPath}${suffix}`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const stripLocalePrefix = (
|
|
49
|
+
path: string,
|
|
50
|
+
): { locale: Locale; path: string } => {
|
|
51
|
+
const { pathname, suffix } = splitPathReference(path);
|
|
52
|
+
const normalizedPath = normalizePathname(pathname);
|
|
53
|
+
const [firstSegment = ''] = normalizedPath.slice(1).split('/');
|
|
54
|
+
const locale = localeConfig.getLocaleFromUrlCode(firstSegment);
|
|
55
|
+
|
|
56
|
+
if (!locale) {
|
|
57
|
+
return {
|
|
58
|
+
locale: localeConfig.defaultLocale,
|
|
59
|
+
path: `${normalizedPath}${suffix}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const prefix = `/${firstSegment}`;
|
|
64
|
+
const unprefixedPath = normalizedPath.slice(prefix.length);
|
|
65
|
+
return {
|
|
66
|
+
locale,
|
|
67
|
+
path: `${normalizePathname(unprefixedPath || '/')}${suffix}`,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const matchesRoot = (path: string, root: string): boolean =>
|
|
72
|
+
path === root || path.startsWith(`${root}/`);
|
|
73
|
+
|
|
74
|
+
const getRoutePolicy = (path: string): LocalizedRoutePolicy => {
|
|
75
|
+
const { path: unprefixedPath } = stripLocalePrefix(path);
|
|
76
|
+
const normalizedPath = normalizePathname(unprefixedPath);
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
normalizedTechnicalRoots.some((root) =>
|
|
80
|
+
matchesRoot(normalizedPath, root),
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
return 'technical';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalizedLocalizedRoots.some(
|
|
87
|
+
(root) => root === '/' || matchesRoot(normalizedPath, root),
|
|
88
|
+
)
|
|
89
|
+
? 'localized'
|
|
90
|
+
: 'technical';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
localizePath,
|
|
95
|
+
stripLocalePrefix,
|
|
96
|
+
resolveLocale(path: string): Locale {
|
|
97
|
+
return stripLocalePrefix(path).locale;
|
|
98
|
+
},
|
|
99
|
+
getEquivalentPath(path: string, locale: Locale): string {
|
|
100
|
+
const { path: unprefixedPath } = stripLocalePrefix(path);
|
|
101
|
+
return getRoutePolicy(unprefixedPath) === 'localized'
|
|
102
|
+
? localizePath(unprefixedPath, locale)
|
|
103
|
+
: unprefixedPath;
|
|
104
|
+
},
|
|
105
|
+
getRoutePolicy,
|
|
106
|
+
normalizePath(path: string): string {
|
|
107
|
+
const { pathname, suffix } = splitPathReference(path);
|
|
108
|
+
return `${normalizePathname(pathname)}${suffix}`;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type LocalizedRouteParams = {
|
|
114
|
+
locale: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export function createLocalizedRoutePaths<
|
|
118
|
+
TDefinition extends LocaleDefinition,
|
|
119
|
+
>(localeConfig: LocaleConfig<TDefinition>): readonly {
|
|
120
|
+
params: LocalizedRouteParams;
|
|
121
|
+
props: { locale: TDefinition['id'] };
|
|
122
|
+
}[] {
|
|
123
|
+
return localeConfig.localizedLocales.map((locale) => ({
|
|
124
|
+
params: {
|
|
125
|
+
locale: localeConfig.getDefinition(locale).urlCode,
|
|
126
|
+
},
|
|
127
|
+
props: { locale },
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function splitPathReference(value: string): {
|
|
132
|
+
pathname: string;
|
|
133
|
+
suffix: string;
|
|
134
|
+
} {
|
|
135
|
+
const cleanValue = value.trim();
|
|
136
|
+
|
|
137
|
+
if (/^https?:\/\//i.test(cleanValue)) {
|
|
138
|
+
const url = new URL(cleanValue);
|
|
139
|
+
return {
|
|
140
|
+
pathname: url.pathname,
|
|
141
|
+
suffix: `${url.search}${url.hash}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const searchIndex = cleanValue.indexOf('?');
|
|
146
|
+
const hashIndex = cleanValue.indexOf('#');
|
|
147
|
+
const suffixIndex = [searchIndex, hashIndex]
|
|
148
|
+
.filter((index) => index >= 0)
|
|
149
|
+
.reduce((lowest, index) => Math.min(lowest, index), cleanValue.length);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
pathname: cleanValue.slice(0, suffixIndex),
|
|
153
|
+
suffix: cleanValue.slice(suffixIndex),
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/seo/index.ts
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PageMeta } from '../config/pageMeta';
|
|
2
|
+
import type { LocaleConfig, LocaleDefinition } from '../i18n/config';
|
|
3
|
+
import type { LocalizedRouter } from '../routing/localized';
|
|
4
|
+
import { normalizePathname } from './url';
|
|
5
|
+
|
|
6
|
+
export type LocalizedMetaInput = Omit<
|
|
7
|
+
PageMeta,
|
|
8
|
+
| 'alternates'
|
|
9
|
+
| 'canonical'
|
|
10
|
+
| 'lang'
|
|
11
|
+
| 'openGraphLocale'
|
|
12
|
+
| 'openGraphLocaleAlternates'
|
|
13
|
+
> & {
|
|
14
|
+
canonical: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createLocalizedPageMeta<
|
|
18
|
+
TDefinition extends LocaleDefinition,
|
|
19
|
+
>({
|
|
20
|
+
input,
|
|
21
|
+
locale,
|
|
22
|
+
localeConfig,
|
|
23
|
+
router,
|
|
24
|
+
socialImageAlt,
|
|
25
|
+
}: {
|
|
26
|
+
input: LocalizedMetaInput;
|
|
27
|
+
locale: TDefinition['id'];
|
|
28
|
+
localeConfig: LocaleConfig<TDefinition>;
|
|
29
|
+
router: LocalizedRouter<TDefinition['id']>;
|
|
30
|
+
socialImageAlt?: string;
|
|
31
|
+
}): PageMeta {
|
|
32
|
+
const definition = localeConfig.getDefinition(locale);
|
|
33
|
+
const { path } = router.stripLocalePrefix(
|
|
34
|
+
normalizePathname(input.canonical),
|
|
35
|
+
);
|
|
36
|
+
const alternates = localeConfig.definitions.map((alternateLocale) => ({
|
|
37
|
+
lang: alternateLocale.languageTag,
|
|
38
|
+
href: router.localizePath(path, alternateLocale.id),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...input,
|
|
43
|
+
canonical: router.localizePath(path, locale),
|
|
44
|
+
lang: definition.languageTag,
|
|
45
|
+
socialImageAlt: input.socialImageAlt ?? socialImageAlt,
|
|
46
|
+
openGraphLocale: definition.openGraphLocale,
|
|
47
|
+
openGraphLocaleAlternates: localeConfig.definitions
|
|
48
|
+
.filter((alternateLocale) => alternateLocale.id !== locale)
|
|
49
|
+
.map((alternateLocale) => alternateLocale.openGraphLocale),
|
|
50
|
+
alternates: [
|
|
51
|
+
...alternates,
|
|
52
|
+
{
|
|
53
|
+
lang: 'x-default',
|
|
54
|
+
href: router.localizePath(path, localeConfig.defaultLocale),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
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);
|