@astronox/core 0.2.0 → 0.2.2

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 (81) hide show
  1. package/README.md +9 -30
  2. package/package.json +7 -1
  3. package/src/admin/contracts.ts +28 -0
  4. package/src/admin/index.ts +2 -0
  5. package/src/admin/module.ts +28 -0
  6. package/src/auth/contracts.ts +53 -0
  7. package/src/auth/firebase/client.ts +53 -0
  8. package/src/auth/firebase/contracts.ts +8 -0
  9. package/src/auth/firebase/index.ts +1 -0
  10. package/src/auth/index.ts +2 -0
  11. package/src/auth/module.ts +99 -0
  12. package/src/client/index.ts +3 -0
  13. package/src/client/preferences.ts +400 -0
  14. package/src/components/actions/Button/Button.astro +42 -6
  15. package/src/components/actions/Button/Button.css +19 -0
  16. package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
  17. package/src/components/actions/MediaTrigger/MediaTrigger.astro +12 -4
  18. package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
  19. package/src/components/content/CardBody/CardBody.astro +3 -0
  20. package/src/components/content/CardBody/CardBody.css +10 -0
  21. package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
  22. package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
  23. package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
  24. package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
  25. package/src/components/forms/Checkbox/Checkbox.astro +66 -0
  26. package/src/components/forms/Checkbox/Checkbox.css +126 -0
  27. package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
  28. package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
  29. package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
  30. package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
  31. package/src/components/forms/FieldGroup/FieldGroup.astro +34 -0
  32. package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
  33. package/src/components/forms/TextInput/TextInput.astro +7 -1
  34. package/src/components/forms/Textarea/Textarea.astro +32 -0
  35. package/src/components/forms/Textarea/Textarea.css +54 -0
  36. package/src/components/icons/BackIcon.astro +26 -0
  37. package/src/components/icons/CheckIcon.astro +22 -0
  38. package/src/components/icons/DeleteIcon.astro +22 -0
  39. package/src/components/icons/StatusIndicatorIcon.astro +16 -0
  40. package/src/components/index.ts +12 -0
  41. package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
  42. package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
  43. package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
  44. package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
  45. package/src/composition/Grid/Grid.astro +9 -0
  46. package/src/composition/Grid/Grid.css +52 -0
  47. package/src/composition/GridItem/GridItem.astro +9 -0
  48. package/src/composition/GridItem/GridItem.css +12 -0
  49. package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
  50. package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
  51. package/src/data/firestore/client/contracts.ts +55 -0
  52. package/src/data/firestore/client/firebase.ts +12 -0
  53. package/src/data/firestore/client/index.ts +3 -0
  54. package/src/data/firestore/client/repository.ts +111 -0
  55. package/src/display/contracts.ts +41 -0
  56. package/src/display/index.ts +1 -0
  57. package/src/display/tableProgressive.ts +62 -0
  58. package/src/patterns/Combobox/Combobox.astro +23 -0
  59. package/src/patterns/Combobox/comboboxController.ts +72 -0
  60. package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
  61. package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
  62. package/src/patterns/Modal/Modal.astro +1 -1
  63. package/src/patterns/Modal/Modal.css +38 -1
  64. package/src/patterns/Table/Table.astro +122 -7
  65. package/src/patterns/Table/Table.css +2 -1
  66. package/src/patterns/Table/TableBody.astro +11 -1
  67. package/src/patterns/Table/TableBody.css +6 -0
  68. package/src/patterns/Table/TableCell.css +6 -0
  69. package/src/patterns/Table/TableCellValue.astro +31 -0
  70. package/src/patterns/Table/TableFooter.astro +3 -1
  71. package/src/patterns/Table/TableHeader.astro +1 -0
  72. package/src/patterns/Table/TablePagination.astro +63 -47
  73. package/src/patterns/Table/TableSkeleton.astro +18 -22
  74. package/src/patterns/Table/TableSkeleton.css +30 -5
  75. package/src/patterns/Table/tableClientController.ts +253 -0
  76. package/src/patterns/Table/tableFilterController.ts +83 -33
  77. package/src/patterns/Table/tableLayout.ts +10 -3
  78. package/src/patterns/Table/tableProgressiveController.ts +615 -227
  79. package/src/patterns/Table/tableSortController.ts +25 -7
  80. package/src/styles/interaction.css +16 -0
  81. package/src/styles/style.css +4 -0
@@ -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');
@@ -6,8 +6,6 @@ import EnterFullscreenIcon from '../../components/icons/EnterFullscreenIcon.astr
6
6
  import ExitFullscreenIcon from '../../components/icons/ExitFullscreenIcon.astro';
7
7
  import ExternalLinkIcon from '../../components/icons/ExternalLinkIcon.astro';
8
8
 
9
- type FullscreenControlPlacement = 'inline' | 'overlay';
10
- type FullscreenOverlayOffset = 'default' | 'native-controls';
11
9
  type FullscreenUnsupportedFallback = {
12
10
  href: string;
13
11
  label: string;
@@ -17,8 +15,6 @@ type FullscreenUnsupportedFallback = {
17
15
  type Props = Omit<HTMLAttributes<'button'>, 'aria-controls' | 'type'> & {
18
16
  enterLabel: string;
19
17
  exitLabel: string;
20
- overlayOffset?: FullscreenOverlayOffset;
21
- placement?: FullscreenControlPlacement;
22
18
  targetId: string;
23
19
  tone?: Tone;
24
20
  unsupportedFallback?: FullscreenUnsupportedFallback;
@@ -28,8 +24,6 @@ const {
28
24
  class: className,
29
25
  enterLabel,
30
26
  exitLabel,
31
- overlayOffset = 'default',
32
- placement = 'inline',
33
27
  targetId,
34
28
  tone,
35
29
  unsupportedFallback,
@@ -44,10 +38,9 @@ const resolvedClass = ['fullscreen-control', className]
44
38
  {...attrs}
45
39
  class={resolvedClass}
46
40
  type="button"
47
- borderless={placement === 'inline'}
48
- full={placement === 'inline'}
41
+ borderless
42
+ full
49
43
  iconOnly
50
- size={placement === 'overlay' ? 'md' : undefined}
51
44
  tone={tone}
52
45
  aria-controls={targetId}
53
46
  aria-label={enterLabel}
@@ -56,8 +49,6 @@ const resolvedClass = ['fullscreen-control', className]
56
49
  data-fullscreen-control
57
50
  data-fullscreen-enter-label={enterLabel}
58
51
  data-fullscreen-exit-label={exitLabel}
59
- data-fullscreen-overlay-offset={overlayOffset}
60
- data-fullscreen-placement={placement}
61
52
  data-fullscreen-target={targetId}
62
53
  >
63
54
  <span class="fullscreen-control__icon" data-fullscreen-enter-icon>
@@ -73,10 +64,9 @@ const resolvedClass = ['fullscreen-control', className]
73
64
  <Button
74
65
  href={unsupportedFallback.href}
75
66
  target={unsupportedFallback.target ?? '_blank'}
76
- borderless={placement === 'inline'}
77
- full={placement === 'inline'}
67
+ borderless
68
+ full
78
69
  iconOnly
79
- size={placement === 'overlay' ? 'md' : undefined}
80
70
  tone={tone}
81
71
  aria-label={unsupportedFallback.label}
82
72
  title={unsupportedFallback.label}
@@ -15,32 +15,6 @@
15
15
  display: grid;
16
16
  }
17
17
 
18
- .fullscreen-control[data-fullscreen-placement='overlay'].nox-button {
19
- position: absolute;
20
- z-index: 2;
21
- display: none;
22
- inset-inline-end: var(--nox-space-3);
23
- inset-block-end: var(--nox-space-3);
24
- inline-size: var(--nox-section-responsive-block-size);
25
- block-size: var(--nox-section-responsive-block-size);
26
- min-block-size: 0;
27
- border-radius: var(--nox-radius-md);
28
- box-shadow: var(--nox-shadow-surface);
29
- }
30
-
31
- [data-fullscreen-region]:fullscreen
32
- .fullscreen-control[data-fullscreen-placement='overlay'].nox-button {
33
- display: inline-flex;
34
- }
35
-
36
- [data-fullscreen-region]:fullscreen
37
- .fullscreen-control[data-fullscreen-placement='overlay'][data-fullscreen-overlay-offset='native-controls'].nox-button {
38
- inset-block-end: calc(
39
- var(--nox-fullscreen-native-controls-block-size, 2.5rem) +
40
- var(--nox-space-3)
41
- );
42
- }
43
-
44
18
  [data-fullscreen-region]:fullscreen {
45
19
  inline-size: 100vw;
46
20
  block-size: 100vh;
@@ -4,7 +4,7 @@ import type { Tone } from '../../theme/contracts';
4
4
  import ModalClose from './ModalClose.astro';
5
5
 
6
6
  type ModalCloseControl = 'default' | 'delegated';
7
- type ModalSizing = 'content' | 'viewport';
7
+ type ModalSizing = 'adaptive' | 'confirmation' | 'content' | 'viewport';
8
8
 
9
9
  type Props = HTMLAttributes<'dialog'> & {
10
10
  id: string;
@@ -48,6 +48,33 @@ html:has(.modal[open]) [data-site-viewport] {
48
48
  overflow: auto;
49
49
  }
50
50
 
51
+ .modal[data-sizing='confirmation'] .modal__content {
52
+ inline-size: min(42rem, 100%);
53
+ block-size: auto;
54
+ max-block-size: 100%;
55
+ align-self: center;
56
+ overflow: auto;
57
+ }
58
+
59
+ .modal[data-sizing='adaptive'] .modal__content {
60
+ display: flex;
61
+ block-size: 100%;
62
+ min-block-size: 0;
63
+ flex-direction: column;
64
+ justify-content: center;
65
+ overflow: hidden;
66
+ }
67
+
68
+ .modal[data-sizing='adaptive']
69
+ .modal__content
70
+ > [data-composition='surface-group'] {
71
+ inline-size: 100%;
72
+ min-block-size: 0;
73
+ max-block-size: 100%;
74
+ flex: 0 1 auto;
75
+ overflow: hidden;
76
+ }
77
+
51
78
  .modal__default-close {
52
79
  position: absolute;
53
80
  z-index: 1;
@@ -81,7 +108,17 @@ html:has(.modal[open]) [data-site-viewport] {
81
108
  padding-inline: 0;
82
109
  }
83
110
 
84
- .modal__content [data-surface] {
111
+ .modal:not(
112
+ [data-sizing='adaptive'],
113
+ [data-sizing='confirmation']
114
+ )
115
+ .modal__content
116
+ [data-surface] {
85
117
  border-radius: 0;
86
118
  }
119
+
120
+ .modal:is([data-sizing='adaptive'], [data-sizing='confirmation'])
121
+ .modal__content {
122
+ padding: var(--nox-space-2);
123
+ }
87
124
  }
@@ -3,14 +3,17 @@ import type { HTMLAttributes } from 'astro/types';
3
3
  import type {
4
4
  TableColumn,
5
5
  TablePaginationModel,
6
+ TableProgressiveModel,
6
7
  TableRow,
7
8
  } from '../../display/contracts';
9
+ import { tableProgressiveBatchSize } from '../../display/tableProgressive';
8
10
  import type { Tone } from '../../theme/contracts';
9
11
  import SurfaceGroup from '../../composition/SurfaceGroup/SurfaceGroup.astro';
10
12
  import TableBody from './TableBody.astro';
11
13
  import TableFooter from './TableFooter.astro';
12
14
  import TableHeader from './TableHeader.astro';
13
15
  import TablePagination from './TablePagination.astro';
16
+ import TableSkeleton from './TableSkeleton.astro';
14
17
  import {
15
18
  getTableColumnDensity,
16
19
  getTableMinimumInlineSize,
@@ -33,6 +36,7 @@ type TablePaginationMessages = {
33
36
 
34
37
  type Props = Omit<HTMLAttributes<'div'>, 'style'> & {
35
38
  caption: string;
39
+ clientProgressive?: boolean;
36
40
  columns: readonly TableColumn[];
37
41
  density?: 'regular' | 'compact';
38
42
  emptyMessage: string;
@@ -44,6 +48,8 @@ type Props = Omit<HTMLAttributes<'div'>, 'style'> & {
44
48
  pagination?: TablePaginationModel;
45
49
  paginationKey?: string;
46
50
  paginationMessages?: TablePaginationMessages;
51
+ preparation?: 'automatic' | 'controlled';
52
+ progressive?: false | TableProgressiveModel;
47
53
  rowSizing?: 'content' | 'fill';
48
54
  rows: readonly TableRow[];
49
55
  skeletonRows?: number;
@@ -54,6 +60,7 @@ type Props = Omit<HTMLAttributes<'div'>, 'style'> & {
54
60
 
55
61
  const {
56
62
  caption,
63
+ clientProgressive = false,
57
64
  class: className,
58
65
  columns,
59
66
  'data-tone': dataTone,
@@ -67,6 +74,8 @@ const {
67
74
  pagination,
68
75
  paginationKey,
69
76
  paginationMessages,
77
+ preparation = 'automatic',
78
+ progressive,
70
79
  rowSizing = 'content',
71
80
  rows,
72
81
  skeletonRows,
@@ -95,6 +104,62 @@ const columnDensity = getTableColumnDensity(columns);
95
104
  const resolvedTone = tone ?? dataTone;
96
105
  const hasSortableColumns = columns.some((column) => column.sort);
97
106
  const hasFooter = Astro.slots.has('footer');
107
+ const configuredProgressive =
108
+ progressive && typeof progressive === 'object' ? progressive : undefined;
109
+ const batchSize = Math.max(
110
+ 1,
111
+ Math.floor(configuredProgressive?.batchSize ?? tableProgressiveBatchSize),
112
+ );
113
+ const resolvedRows = rows.map((row, index): TableRow => ({
114
+ ...row,
115
+ key: row.key ?? `${paginationKey ?? 'table'}-${index + 1}`,
116
+ }));
117
+ const inferredProgressive =
118
+ progressive === undefined && resolvedRows.length > batchSize
119
+ ? {
120
+ batchSize,
121
+ entries: resolvedRows.map((row) => ({
122
+ key: row.key!,
123
+ filters: row.filters,
124
+ sortValues: row.sortValues,
125
+ })),
126
+ }
127
+ : undefined;
128
+ const progressiveModel =
129
+ progressive === false
130
+ ? undefined
131
+ : configuredProgressive ?? inferredProgressive;
132
+ const hasProgressiveRows = Boolean(
133
+ progressiveModel && progressiveModel.entries.length > batchSize,
134
+ );
135
+ const hasProgressivePagination = hasProgressiveRows || clientProgressive;
136
+ const initialSkeletonRows = Math.max(
137
+ 1,
138
+ Math.floor(
139
+ skeletonRows ??
140
+ Math.min(
141
+ batchSize,
142
+ (progressiveModel?.entries.length ?? resolvedRows.length) || 1,
143
+ ),
144
+ ),
145
+ );
146
+ const preparesRows =
147
+ (resolvedRows.length > 0 || clientProgressive) && !errorMessage;
148
+ const initialLoadingState = errorMessage
149
+ ? 'error'
150
+ : resolvedRows.length === 0 && !clientProgressive
151
+ ? 'empty'
152
+ : 'pending';
153
+ const progressiveManifest = hasProgressiveRows
154
+ ? JSON.stringify({
155
+ ...progressiveModel,
156
+ batchSize,
157
+ }).replaceAll('<', '\\u003c')
158
+ : undefined;
159
+ const rowKeys = new Set(resolvedRows.map((row) => row.key));
160
+ const progressiveKeys = new Set(
161
+ progressiveModel?.entries.map((entry) => entry.key) ?? [],
162
+ );
98
163
 
99
164
  if (header === 'visible' && hasSortableColumns && !sortMessages) {
100
165
  throw new Error('Sortable table columns require localized sort messages');
@@ -102,6 +167,21 @@ if (header === 'visible' && hasSortableColumns && !sortMessages) {
102
167
  if (pagination?.nextHref && !paginationMessages) {
103
168
  throw new Error('Paginated tables require localized pagination messages');
104
169
  }
170
+ if (clientProgressive && !paginationMessages) {
171
+ throw new Error('Client-progressive tables require localized messages');
172
+ }
173
+ if (progressiveModel?.sources && !paginationKey) {
174
+ throw new Error('External progressive tables require a stable pagination key');
175
+ }
176
+ if (rowKeys.size !== resolvedRows.length) {
177
+ throw new Error('Table rows require unique keys');
178
+ }
179
+ if (
180
+ progressiveModel &&
181
+ progressiveKeys.size !== progressiveModel.entries.length
182
+ ) {
183
+ throw new Error('Progressive table entries require unique keys');
184
+ }
105
185
  ---
106
186
 
107
187
  <div
@@ -110,18 +190,32 @@ if (pagination?.nextHref && !paginationMessages) {
110
190
  data-widget="table"
111
191
  data-composition="table"
112
192
  data-table-column-density={columnDensity}
193
+ data-table-client-progressive={clientProgressive ? true : undefined}
113
194
  data-table-density={density}
114
195
  data-table-inline-overflow={inlineOverflow}
115
196
  data-table-index-minimum-rem={
116
197
  minimumInlineSize ? Number.parseFloat(minimumInlineSize) : undefined
117
198
  }
118
199
  data-table-layout={layout}
200
+ data-table-loading-state={initialLoadingState}
201
+ data-table-preparation={preparation}
119
202
  data-table-key={paginationKey}
120
203
  data-table-scroll-root
121
204
  data-table-sticky-header={stickyHeader ? true : undefined}
122
205
  data-tone={resolvedTone}
123
206
  style={tableStyle}
124
207
  >
208
+ {
209
+ preparesRows ? (
210
+ <script is:inline>
211
+ document.documentElement.setAttribute('data-nox-table-enhanced', '');
212
+ document.currentScript?.parentElement?.setAttribute(
213
+ 'aria-busy',
214
+ 'true',
215
+ );
216
+ </script>
217
+ ) : null
218
+ }
125
219
  <SurfaceGroup>
126
220
  <slot name="heading" />
127
221
  {
@@ -137,17 +231,17 @@ if (pagination?.nextHref && !paginationMessages) {
137
231
  filterEmptyMessage={filterEmptyMessage}
138
232
  layout={layout}
139
233
  rowSizing={rowSizing}
140
- rows={rows}
234
+ rows={resolvedRows}
235
+ skeletonRows={preparesRows ? initialSkeletonRows : 0}
141
236
  />
142
237
  {
143
- pagination && paginationMessages ? (
238
+ hasProgressivePagination || (pagination && paginationMessages) ? (
144
239
  <TablePagination
145
- columns={columns}
146
- errorLabel={paginationMessages.error}
147
- loadingLabel={paginationMessages.loading}
148
- nextLabel={paginationMessages.nextPage}
240
+ errorLabel={paginationMessages?.error ?? ''}
241
+ loadingLabel={paginationMessages?.loading ?? ''}
242
+ nextLabel={paginationMessages?.nextPage ?? ''}
149
243
  pagination={pagination}
150
- skeletonRows={skeletonRows}
244
+ progressive={hasProgressivePagination}
151
245
  />
152
246
  ) : null
153
247
  }
@@ -159,6 +253,27 @@ if (pagination?.nextHref && !paginationMessages) {
159
253
  ) : null
160
254
  }
161
255
  </SurfaceGroup>
256
+ {
257
+ progressiveManifest ? (
258
+ <script
259
+ is:inline
260
+ type="application/json"
261
+ data-table-progressive-manifest
262
+ set:html={progressiveManifest}
263
+ />
264
+ ) : null
265
+ }
266
+ {
267
+ preparesRows ? (
268
+ <template data-table-skeleton-template>
269
+ <table>
270
+ <tbody>
271
+ <TableSkeleton columns={columns} rows={initialSkeletonRows} />
272
+ </tbody>
273
+ </table>
274
+ </template>
275
+ ) : null
276
+ }
162
277
  </div>
163
278
 
164
279
  <script>
@@ -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;
@@ -5,6 +5,7 @@ import type { Tone } from '../../theme/contracts';
5
5
  import TableSortIcon from '../../components/icons/TableSortIcon.astro';
6
6
  import TextBlock from '../../primitives/text/TextBlock/TextBlock.astro';
7
7
  import TableCellContent from './TableCell.astro';
8
+ import TableSkeleton from './TableSkeleton.astro';
8
9
  import {
9
10
  getTableColumnAlignment,
10
11
  type TableLayout,
@@ -20,6 +21,7 @@ type Props = HTMLAttributes<'div'> & {
20
21
  layout?: TableLayout;
21
22
  rowSizing?: 'content' | 'fill';
22
23
  rows: readonly TableRow[];
24
+ skeletonRows?: number;
23
25
  };
24
26
 
25
27
  const {
@@ -33,6 +35,7 @@ const {
33
35
  layout = 'default',
34
36
  rowSizing = 'content',
35
37
  rows,
38
+ skeletonRows = 0,
36
39
  ...attrs
37
40
  }: Props = Astro.props;
38
41
 
@@ -86,6 +89,11 @@ const stateMessage =
86
89
  ))
87
90
  }
88
91
  </tr>
92
+ {
93
+ skeletonRows > 0 ? (
94
+ <TableSkeleton columns={columns} rows={skeletonRows} />
95
+ ) : null
96
+ }
89
97
  {
90
98
  state && stateMessage ? (
91
99
  <tr class="table-body__row" data-table-state={state}>
@@ -102,12 +110,14 @@ const stateMessage =
102
110
  </tr>
103
111
  ) : (
104
112
  rows.map((row) => {
105
- const rowHref = row.href;
113
+ const rowHref = row.disabled ? undefined : row.href;
106
114
 
107
115
  return (
108
116
  <tr
109
117
  class="table-body__row"
110
118
  hidden={row.hidden}
119
+ aria-disabled={row.disabled ? 'true' : undefined}
120
+ data-disabled={row.disabled ? 'true' : undefined}
111
121
  data-table-row
112
122
  data-table-row-key={row.key}
113
123
  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
  }
@@ -41,6 +41,12 @@
41
41
  min-inline-size: max-content;
42
42
  }
43
43
 
44
+ .table-cell[data-table-cell-kind='actions'] {
45
+ inline-size: max-content;
46
+ min-inline-size: max-content;
47
+ overflow: visible;
48
+ }
49
+
44
50
  .table-cell[data-table-cell-kind='tags']:not([data-wrap])
45
51
  > [data-composition='inline']
46
52
  > [data-widget='tag'] {
@@ -6,6 +6,7 @@ import TextPair from '../../components/data-display/TextPair/TextPair.astro';
6
6
  import Media from '../../primitives/media/Media/Media.astro';
7
7
  import Inline from '../../composition/Inline/Inline.astro';
8
8
  import Tag from '../../components/data-display/Tag/Tag.astro';
9
+ import Button from '../../components/actions/Button/Button.astro';
9
10
 
10
11
  type Props = {
11
12
  cell: TableCell;
@@ -84,3 +85,33 @@ const formatValue = (value: DisplayTextValue): string => String(value);
84
85
  </Inline>
85
86
  ) : null
86
87
  }
88
+
89
+ {
90
+ cell.kind === 'actions' ? (
91
+ <Inline align="center" gap="xs" justify="end" wrap={false}>
92
+ {cell.actions.map((action) => {
93
+ const Icon = action.icon;
94
+
95
+ return (
96
+ <Button
97
+ aria-controls={action.modalTargetId}
98
+ aria-haspopup={action.modalTargetId ? 'dialog' : undefined}
99
+ aria-label={action.label}
100
+ borderless={action.borderless !== false}
101
+ href={action.href}
102
+ hoverTone={action.hoverTone}
103
+ iconOnly
104
+ size="sm"
105
+ title={action.label}
106
+ tone={action.tone}
107
+ data-modal-open={action.modalTargetId}
108
+ >
109
+ <span data-tone={action.iconTone}>
110
+ <Icon />
111
+ </span>
112
+ </Button>
113
+ );
114
+ })}
115
+ </Inline>
116
+ ) : null
117
+ }
@@ -6,6 +6,7 @@ type TableFooterPadding = 'none' | 'sm' | 'md' | 'lg';
6
6
 
7
7
  type Props = HTMLAttributes<'div'> & {
8
8
  padding?: TableFooterPadding;
9
+ surface?: boolean;
9
10
  tone?: Tone;
10
11
  };
11
12
 
@@ -13,6 +14,7 @@ const {
13
14
  class: className,
14
15
  'data-tone': dataTone,
15
16
  padding = 'md',
17
+ surface = true,
16
18
  tone,
17
19
  ...attrs
18
20
  }: Props = Astro.props;
@@ -26,7 +28,7 @@ const resolvedTone = tone ?? dataTone;
26
28
  data-padding={padding}
27
29
  data-table-footer
28
30
  data-tone={resolvedTone}
29
- data-surface
31
+ data-surface={surface ? true : undefined}
30
32
  >
31
33
  <slot />
32
34
  </div>
@@ -53,6 +53,7 @@ const hasSortableColumns = columns.some((column) => column.sort);
53
53
  type="button"
54
54
  data-table-sort-column={column.id}
55
55
  data-table-sort-direction="none"
56
+ data-table-sort-first-direction={column.sort.firstDirection}
56
57
  data-table-sort-label={column.label}
57
58
  data-table-sort-type={column.sort.type}
58
59
  data-table-sort-tie-breaker={column.sort.tieBreaker}