@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.
Files changed (40) hide show
  1. package/README.md +15 -27
  2. package/package.json +3 -1
  3. package/src/components/actions/MediaTrigger/MediaTrigger.astro +12 -4
  4. package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
  5. package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
  6. package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
  7. package/src/components/forms/Checkbox/Checkbox.astro +60 -0
  8. package/src/components/forms/Checkbox/Checkbox.css +126 -0
  9. package/src/components/forms/FieldGroup/FieldGroup.astro +34 -0
  10. package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
  11. package/src/components/icons/CheckIcon.astro +22 -0
  12. package/src/components/index.ts +2 -0
  13. package/src/components/navigation/Breadcrumbs/Breadcrumbs.astro +2 -1
  14. package/src/components/navigation/SubNavigation/SubNavigation.astro +2 -1
  15. package/src/composition/Grid/Grid.astro +9 -0
  16. package/src/composition/Grid/Grid.css +52 -0
  17. package/src/composition/GridItem/GridItem.astro +9 -0
  18. package/src/composition/GridItem/GridItem.css +12 -0
  19. package/src/display/contracts.ts +1 -0
  20. package/src/display/index.ts +1 -0
  21. package/src/display/responsiveMedia.ts +38 -0
  22. package/src/i18n/LocalePreferenceBootstrap.astro +40 -0
  23. package/src/i18n/catalog.ts +203 -0
  24. package/src/i18n/config.ts +112 -0
  25. package/src/i18n/coreMessages.ts +37 -0
  26. package/src/i18n/formatters.ts +34 -0
  27. package/src/i18n/index.ts +6 -0
  28. package/src/i18n/preference.ts +58 -0
  29. package/src/index.ts +2 -0
  30. package/src/patterns/Combobox/comboboxController.ts +72 -0
  31. package/src/patterns/Table/Table.css +2 -1
  32. package/src/patterns/Table/TableBody.astro +3 -1
  33. package/src/patterns/Table/TableBody.css +6 -0
  34. package/src/patterns/Table/tableFilterController.ts +60 -2
  35. package/src/patterns/Table/tableLayout.ts +10 -3
  36. package/src/routing/index.ts +1 -0
  37. package/src/routing/localized.ts +155 -0
  38. package/src/seo/index.ts +1 -0
  39. package/src/seo/localizedMeta.ts +58 -0
  40. 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 active = query.length > 0 || activeSelections.length > 0;
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 = matchesSearch && matchesSelections;
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: 7,
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
- return role === 'primary' ? `minmax(${minimum}, 1fr)` : minimum;
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
@@ -1,5 +1,6 @@
1
1
  export { default as StaticRedirect } from './StaticRedirect.astro';
2
2
  export * from './pageSeo';
3
+ export * from './localizedMeta';
3
4
  export * from './robots';
4
5
  export * from './sitemap';
5
6
  export * from './structuredData';
@@ -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
+ }
@@ -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);