@happyvertical/smrt-ui 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +50 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/dist/actions/__tests__/ripple.test.js +28 -0
- package/dist/actions/permission.d.ts +34 -0
- package/dist/actions/permission.d.ts.map +1 -0
- package/dist/actions/permission.js +70 -0
- package/dist/actions/ripple.d.ts +7 -0
- package/dist/actions/ripple.d.ts.map +1 -0
- package/dist/actions/ripple.js +65 -0
- package/dist/components/calendar/Calendar.svelte +520 -0
- package/dist/components/calendar/Calendar.svelte.d.ts +17 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +389 -0
- package/dist/components/calendar/DayView.svelte.d.ts +13 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/index.d.ts +6 -0
- package/dist/components/calendar/index.d.ts.map +1 -0
- package/dist/components/calendar/index.js +5 -0
- package/dist/components/chat/MessageBubble.svelte +126 -0
- package/dist/components/chat/MessageBubble.svelte.d.ts +30 -0
- package/dist/components/chat/MessageBubble.svelte.d.ts.map +1 -0
- package/dist/components/chat/ReactionPicker.svelte +89 -0
- package/dist/components/chat/ReactionPicker.svelte.d.ts +19 -0
- package/dist/components/chat/ReactionPicker.svelte.d.ts.map +1 -0
- package/dist/components/chat/TypingIndicator.svelte +90 -0
- package/dist/components/chat/TypingIndicator.svelte.d.ts +17 -0
- package/dist/components/chat/TypingIndicator.svelte.d.ts.map +1 -0
- package/dist/components/chat/__tests__/chat-primitives.test.js +67 -0
- package/dist/components/chat/index.d.ts +10 -0
- package/dist/components/chat/index.d.ts.map +1 -0
- package/dist/components/chat/index.js +9 -0
- package/dist/components/data/DataTable.svelte +519 -0
- package/dist/components/data/DataTable.svelte.d.ts +49 -0
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/DataTable.test.js +48 -0
- package/dist/components/data/__tests__/data-table-helpers.test.js +36 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -0
- package/dist/components/data/index.js +5 -0
- package/dist/components/data/types.d.ts +104 -0
- package/dist/components/data/types.d.ts.map +1 -0
- package/dist/components/data/types.js +45 -0
- package/dist/components/display/ConfidenceBadge.svelte +142 -0
- package/dist/components/display/ConfidenceBadge.svelte.d.ts +25 -0
- package/dist/components/display/ConfidenceBadge.svelte.d.ts.map +1 -0
- package/dist/components/display/CurrencyDisplay.svelte +106 -0
- package/dist/components/display/CurrencyDisplay.svelte.d.ts +30 -0
- package/dist/components/display/CurrencyDisplay.svelte.d.ts.map +1 -0
- package/dist/components/display/DateDisplay.svelte +122 -0
- package/dist/components/display/DateDisplay.svelte.d.ts +24 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -0
- package/dist/components/display/Icon.svelte +77 -0
- package/dist/components/display/Icon.svelte.d.ts +28 -0
- package/dist/components/display/Icon.svelte.d.ts.map +1 -0
- package/dist/components/display/StatusBadge.svelte +256 -0
- package/dist/components/display/StatusBadge.svelte.d.ts +24 -0
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -0
- package/dist/components/display/__tests__/ConfidenceBadge.test.js +96 -0
- package/dist/components/display/__tests__/CurrencyDisplay.test.js +114 -0
- package/dist/components/display/__tests__/DateDisplay.test.js +114 -0
- package/dist/components/display/__tests__/Icon.test.js +93 -0
- package/dist/components/display/__tests__/StatusBadge.test.js +98 -0
- package/dist/components/display/index.d.ts +10 -0
- package/dist/components/display/index.d.ts.map +1 -0
- package/dist/components/display/index.js +9 -0
- package/dist/components/display/types.d.ts +5 -0
- package/dist/components/display/types.d.ts.map +1 -0
- package/dist/components/display/types.js +4 -0
- package/dist/components/feedback/ConfirmDialog.svelte +226 -0
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +25 -0
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -0
- package/dist/components/feedback/LoadingOverlay.svelte +281 -0
- package/dist/components/feedback/LoadingOverlay.svelte.d.ts +31 -0
- package/dist/components/feedback/LoadingOverlay.svelte.d.ts.map +1 -0
- package/dist/components/feedback/Modal.svelte +393 -0
- package/dist/components/feedback/Modal.svelte.d.ts +46 -0
- package/dist/components/feedback/Modal.svelte.d.ts.map +1 -0
- package/dist/components/feedback/ProgressBar.svelte +162 -0
- package/dist/components/feedback/ProgressBar.svelte.d.ts +21 -0
- package/dist/components/feedback/ProgressBar.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +111 -0
- package/dist/components/feedback/__tests__/LoadingOverlay.test.js +99 -0
- package/dist/components/feedback/__tests__/Modal.test.js +72 -0
- package/dist/components/feedback/__tests__/ProgressBar.test.js +89 -0
- package/dist/components/feedback/index.d.ts +8 -0
- package/dist/components/feedback/index.d.ts.map +1 -0
- package/dist/components/feedback/index.js +10 -0
- package/dist/components/layout/Container.svelte +53 -0
- package/dist/components/layout/Container.svelte.d.ts +11 -0
- package/dist/components/layout/Container.svelte.d.ts.map +1 -0
- package/dist/components/layout/EmptyState.svelte +187 -0
- package/dist/components/layout/EmptyState.svelte.d.ts +28 -0
- package/dist/components/layout/EmptyState.svelte.d.ts.map +1 -0
- package/dist/components/layout/Footer.svelte +63 -0
- package/dist/components/layout/Footer.svelte.d.ts +8 -0
- package/dist/components/layout/Footer.svelte.d.ts.map +1 -0
- package/dist/components/layout/Grid.svelte +241 -0
- package/dist/components/layout/Grid.svelte.d.ts +56 -0
- package/dist/components/layout/Grid.svelte.d.ts.map +1 -0
- package/dist/components/layout/Header.svelte +86 -0
- package/dist/components/layout/Header.svelte.d.ts +9 -0
- package/dist/components/layout/Header.svelte.d.ts.map +1 -0
- package/dist/components/layout/Masthead.svelte +219 -0
- package/dist/components/layout/Masthead.svelte.d.ts +13 -0
- package/dist/components/layout/Masthead.svelte.d.ts.map +1 -0
- package/dist/components/layout/PageHeader.svelte +131 -0
- package/dist/components/layout/PageHeader.svelte.d.ts +26 -0
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -0
- package/dist/components/layout/SummaryCard.svelte +203 -0
- package/dist/components/layout/SummaryCard.svelte.d.ts +20 -0
- package/dist/components/layout/SummaryCard.svelte.d.ts.map +1 -0
- package/dist/components/layout/__tests__/Container.test.js +62 -0
- package/dist/components/layout/__tests__/EmptyState.test.js +83 -0
- package/dist/components/layout/__tests__/Footer.test.js +50 -0
- package/dist/components/layout/__tests__/Grid.test.js +121 -0
- package/dist/components/layout/__tests__/Header.test.js +48 -0
- package/dist/components/layout/__tests__/Masthead.test.js +93 -0
- package/dist/components/layout/__tests__/PageHeader.test.js +80 -0
- package/dist/components/layout/__tests__/SummaryCard.test.js +82 -0
- package/dist/components/layout/index.d.ts +12 -0
- package/dist/components/layout/index.d.ts.map +1 -0
- package/dist/components/layout/index.js +11 -0
- package/dist/components/memberships/MembershipCard.svelte +163 -0
- package/dist/components/memberships/MembershipCard.svelte.d.ts +12 -0
- package/dist/components/memberships/MembershipCard.svelte.d.ts.map +1 -0
- package/dist/components/memberships/MembershipList.svelte +98 -0
- package/dist/components/memberships/MembershipList.svelte.d.ts +19 -0
- package/dist/components/memberships/MembershipList.svelte.d.ts.map +1 -0
- package/dist/components/nav/FilterChips.svelte +152 -0
- package/dist/components/nav/FilterChips.svelte.d.ts +19 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -0
- package/dist/components/nav/Tabs.svelte +252 -0
- package/dist/components/nav/Tabs.svelte.d.ts +34 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -0
- package/dist/components/nav/__tests__/FilterChips.test.js +94 -0
- package/dist/components/nav/__tests__/Tabs.test.js +128 -0
- package/dist/components/nav/index.d.ts +7 -0
- package/dist/components/nav/index.d.ts.map +1 -0
- package/dist/components/nav/index.js +6 -0
- package/dist/components/nav/types.d.ts +24 -0
- package/dist/components/nav/types.d.ts.map +1 -0
- package/dist/components/nav/types.js +4 -0
- package/dist/components/permissions/PermissionCheck.svelte +45 -0
- package/dist/components/permissions/PermissionCheck.svelte.d.ts +19 -0
- package/dist/components/permissions/PermissionCheck.svelte.d.ts.map +1 -0
- package/dist/components/roles/RoleBadge.svelte +84 -0
- package/dist/components/roles/RoleBadge.svelte.d.ts +13 -0
- package/dist/components/roles/RoleBadge.svelte.d.ts.map +1 -0
- package/dist/components/roles/RoleSelector.svelte +216 -0
- package/dist/components/roles/RoleSelector.svelte.d.ts +13 -0
- package/dist/components/roles/RoleSelector.svelte.d.ts.map +1 -0
- package/dist/components/theme/ThemeProvider.svelte +71 -0
- package/dist/components/theme/ThemeProvider.svelte.d.ts +10 -0
- package/dist/components/theme/ThemeProvider.svelte.d.ts.map +1 -0
- package/dist/components/theme/context.svelte.d.ts +15 -0
- package/dist/components/theme/context.svelte.d.ts.map +1 -0
- package/dist/components/theme/context.svelte.js +42 -0
- package/dist/components/theme/index.d.ts +3 -0
- package/dist/components/theme/index.d.ts.map +1 -0
- package/dist/components/theme/index.js +2 -0
- package/dist/components/ui/Avatar.svelte +167 -0
- package/dist/components/ui/Avatar.svelte.d.ts +26 -0
- package/dist/components/ui/Avatar.svelte.d.ts.map +1 -0
- package/dist/components/ui/Badge.svelte +70 -0
- package/dist/components/ui/Badge.svelte.d.ts +12 -0
- package/dist/components/ui/Badge.svelte.d.ts.map +1 -0
- package/dist/components/ui/Button.svelte +226 -0
- package/dist/components/ui/Button.svelte.d.ts +28 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -0
- package/dist/components/ui/Card.svelte +122 -0
- package/dist/components/ui/Card.svelte.d.ts +15 -0
- package/dist/components/ui/Card.svelte.d.ts.map +1 -0
- package/dist/components/ui/Chip.svelte +167 -0
- package/dist/components/ui/Chip.svelte.d.ts +33 -0
- package/dist/components/ui/Chip.svelte.d.ts.map +1 -0
- package/dist/components/ui/Dropdown.svelte +250 -0
- package/dist/components/ui/Dropdown.svelte.d.ts +20 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +294 -0
- package/dist/components/ui/Pagination.svelte.d.ts +21 -0
- package/dist/components/ui/Pagination.svelte.d.ts.map +1 -0
- package/dist/components/ui/Skeleton.svelte +113 -0
- package/dist/components/ui/Skeleton.svelte.d.ts +24 -0
- package/dist/components/ui/Skeleton.svelte.d.ts.map +1 -0
- package/dist/components/ui/Tooltip.svelte +120 -0
- package/dist/components/ui/Tooltip.svelte.d.ts +24 -0
- package/dist/components/ui/Tooltip.svelte.d.ts.map +1 -0
- package/dist/components/ui/Tree.svelte +209 -0
- package/dist/components/ui/Tree.svelte.d.ts +17 -0
- package/dist/components/ui/Tree.svelte.d.ts.map +1 -0
- package/dist/components/ui/__tests__/Badge.test.js +76 -0
- package/dist/components/ui/__tests__/Button.test.js +69 -0
- package/dist/components/ui/__tests__/Card.test.js +103 -0
- package/dist/components/ui/__tests__/Pagination.test.js +99 -0
- package/dist/components/ui/__tests__/gap-primitives-interactive.test.js +112 -0
- package/dist/components/ui/__tests__/gap-primitives.test.js +84 -0
- package/dist/components/ui/index.d.ts +14 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/index.js +18 -0
- package/dist/i18n/Trans.svelte +29 -0
- package/dist/i18n/Trans.svelte.d.ts +24 -0
- package/dist/i18n/Trans.svelte.d.ts.map +1 -0
- package/dist/i18n/__tests__/i18n.test.js +74 -0
- package/dist/i18n/__tests__/render-parity.spec.js +37 -0
- package/dist/i18n/context.svelte.d.ts +43 -0
- package/dist/i18n/context.svelte.d.ts.map +1 -0
- package/dist/i18n/context.svelte.js +69 -0
- package/dist/i18n/index.d.ts +17 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +24 -0
- package/dist/i18n/registry.d.ts +44 -0
- package/dist/i18n/registry.d.ts.map +1 -0
- package/dist/i18n/registry.js +60 -0
- package/dist/i18n/render.d.ts +22 -0
- package/dist/i18n/render.d.ts.map +1 -0
- package/dist/i18n/render.js +44 -0
- package/dist/i18n/strings.d.ts +7 -0
- package/dist/i18n/strings.d.ts.map +1 -0
- package/dist/i18n/strings.js +19 -0
- package/dist/i18n/strings.ui.d.ts +34 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -0
- package/dist/i18n/strings.ui.js +44 -0
- package/dist/i18n/use-i18n.d.ts +20 -0
- package/dist/i18n/use-i18n.d.ts.map +1 -0
- package/dist/i18n/use-i18n.js +21 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/registry/index.d.ts +6 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +4 -0
- package/dist/registry/module-registry.d.ts +58 -0
- package/dist/registry/module-registry.d.ts.map +1 -0
- package/dist/registry/module-registry.js +94 -0
- package/dist/styles/index.d.ts +4 -0
- package/dist/styles/index.d.ts.map +1 -0
- package/dist/styles/index.js +6 -0
- package/dist/styles/tokens.css +76 -0
- package/dist/test-support/a11y.d.ts +16 -0
- package/dist/test-support/a11y.d.ts.map +1 -0
- package/dist/test-support/a11y.js +32 -0
- package/dist/test-support/setup.d.ts +11 -0
- package/dist/test-support/setup.d.ts.map +1 -0
- package/dist/test-support/setup.js +33 -0
- package/dist/theme/ThemeProvider.svelte +207 -0
- package/dist/theme/ThemeProvider.svelte.d.ts +22 -0
- package/dist/theme/ThemeProvider.svelte.d.ts.map +1 -0
- package/dist/theme/context.d.ts +49 -0
- package/dist/theme/context.d.ts.map +1 -0
- package/dist/theme/context.js +32 -0
- package/dist/theme/index.d.ts +7 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +9 -0
- package/dist/theme/tokens.d.ts +309 -0
- package/dist/theme/tokens.d.ts.map +1 -0
- package/dist/theme/tokens.js +418 -0
- package/dist/themes/CUSTOM_THEME_GUIDE.md +341 -0
- package/dist/themes/README.md +675 -0
- package/dist/themes/ThemeProvider.svelte +275 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +24 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -0
- package/dist/themes/__tests__/css-generator.test.js +32 -0
- package/dist/themes/__tests__/registry.test.js +43 -0
- package/dist/themes/__tests__/token-aliases.test.js +176 -0
- package/dist/themes/components/ColorSchemeToggle.svelte +205 -0
- package/dist/themes/components/ColorSchemeToggle.svelte.d.ts +14 -0
- package/dist/themes/components/ColorSchemeToggle.svelte.d.ts.map +1 -0
- package/dist/themes/components/ThemeSwitcher.svelte +188 -0
- package/dist/themes/components/ThemeSwitcher.svelte.d.ts +14 -0
- package/dist/themes/components/ThemeSwitcher.svelte.d.ts.map +1 -0
- package/dist/themes/components/index.d.ts +8 -0
- package/dist/themes/components/index.d.ts.map +1 -0
- package/dist/themes/components/index.js +7 -0
- package/dist/themes/context.svelte.d.ts +30 -0
- package/dist/themes/context.svelte.d.ts.map +1 -0
- package/dist/themes/context.svelte.js +42 -0
- package/dist/themes/create-theme.d.ts +99 -0
- package/dist/themes/create-theme.d.ts.map +1 -0
- package/dist/themes/create-theme.js +389 -0
- package/dist/themes/css-generator.d.ts +44 -0
- package/dist/themes/css-generator.d.ts.map +1 -0
- package/dist/themes/css-generator.js +226 -0
- package/dist/themes/glass/index.d.ts +14 -0
- package/dist/themes/glass/index.d.ts.map +1 -0
- package/dist/themes/glass/index.js +286 -0
- package/dist/themes/index.d.ts +31 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +37 -0
- package/dist/themes/material/index.d.ts +13 -0
- package/dist/themes/material/index.d.ts.map +1 -0
- package/dist/themes/material/index.js +269 -0
- package/dist/themes/registry.d.ts +64 -0
- package/dist/themes/registry.d.ts.map +1 -0
- package/dist/themes/registry.js +122 -0
- package/dist/themes/shared.d.ts +78 -0
- package/dist/themes/shared.d.ts.map +1 -0
- package/dist/themes/shared.js +179 -0
- package/dist/themes/studio/index.d.ts +14 -0
- package/dist/themes/studio/index.d.ts.map +1 -0
- package/dist/themes/studio/index.js +270 -0
- package/dist/themes/styles/all.css +12 -0
- package/dist/themes/styles/glass.css +432 -0
- package/dist/themes/styles/index.d.ts +22 -0
- package/dist/themes/styles/index.d.ts.map +1 -0
- package/dist/themes/styles/index.js +23 -0
- package/dist/themes/styles/material.css +364 -0
- package/dist/themes/styles/studio.css +416 -0
- package/dist/themes/types.d.ts +273 -0
- package/dist/themes/types.d.ts.map +1 -0
- package/dist/themes/types.js +15 -0
- package/dist/types-generic.d.ts +75 -0
- package/dist/types-generic.d.ts.map +1 -0
- package/dist/types-generic.js +1 -0
- package/dist/utils/forms/__tests__/formatters.test.js +27 -0
- package/dist/utils/forms/formatters.d.ts +14 -0
- package/dist/utils/forms/formatters.d.ts.map +1 -0
- package/dist/utils/forms/formatters.js +77 -0
- package/dist/utils/import-optional.d.ts +5 -0
- package/dist/utils/import-optional.d.ts.map +1 -0
- package/dist/utils/import-optional.js +7 -0
- package/dist/utils/theme/__tests__/color.test.js +72 -0
- package/dist/utils/theme/__tests__/typography.test.js +11 -0
- package/dist/utils/theme/color.d.ts +70 -0
- package/dist/utils/theme/color.d.ts.map +1 -0
- package/dist/utils/theme/color.js +221 -0
- package/dist/utils/theme/typography.d.ts +27 -0
- package/dist/utils/theme/typography.d.ts.map +1 -0
- package/dist/utils/theme/typography.js +30 -0
- package/package.json +143 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component test for Pagination (Sweep S11, #1416).
|
|
3
|
+
*
|
|
4
|
+
* Pagination has two modes: link mode (anchors, default) and callback mode
|
|
5
|
+
* (buttons + `onPageChange`). This suite exercises the callback mode for
|
|
6
|
+
* interaction (clicking pages/prev/next fires the callback; bounds disable the
|
|
7
|
+
* edge controls) and verifies the navigation landmark, current-page marking,
|
|
8
|
+
* and a11y. aria-labels resolve to their registered English defaults outside a
|
|
9
|
+
* Provider (e.g. "Previous page", "Go to page 2").
|
|
10
|
+
*/
|
|
11
|
+
import { render, screen } from '@testing-library/svelte';
|
|
12
|
+
import userEvent from '@testing-library/user-event';
|
|
13
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
14
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
15
|
+
import Pagination from '../Pagination.svelte';
|
|
16
|
+
describe('Pagination', () => {
|
|
17
|
+
it('renders nothing when there is a single page', () => {
|
|
18
|
+
const { container } = render(Pagination, {
|
|
19
|
+
props: { currentPage: 1, totalPages: 1 },
|
|
20
|
+
});
|
|
21
|
+
expect(container.querySelector('.pagination')).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
it('renders a navigation landmark with the provided aria-label', () => {
|
|
24
|
+
render(Pagination, {
|
|
25
|
+
props: { currentPage: 1, totalPages: 5, 'aria-label': 'Article pages' },
|
|
26
|
+
});
|
|
27
|
+
expect(screen.getByRole('navigation', { name: 'Article pages' })).toBeInTheDocument();
|
|
28
|
+
});
|
|
29
|
+
it('marks the current page with aria-current and renders it as a non-link', () => {
|
|
30
|
+
render(Pagination, {
|
|
31
|
+
props: { currentPage: 3, totalPages: 5, onPageChange: vi.fn() },
|
|
32
|
+
});
|
|
33
|
+
const current = screen.getByText('3');
|
|
34
|
+
expect(current).toHaveAttribute('aria-current', 'page');
|
|
35
|
+
// current page is a span, not a button
|
|
36
|
+
expect(current.tagName).toBe('SPAN');
|
|
37
|
+
});
|
|
38
|
+
it('fires onPageChange with the clicked page number (callback mode)', async () => {
|
|
39
|
+
const onPageChange = vi.fn();
|
|
40
|
+
render(Pagination, {
|
|
41
|
+
props: { currentPage: 1, totalPages: 5, onPageChange },
|
|
42
|
+
});
|
|
43
|
+
await userEvent.click(screen.getByRole('button', { name: 'Go to page 3' }));
|
|
44
|
+
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
45
|
+
});
|
|
46
|
+
it('fires onPageChange for prev and next relative to the current page', async () => {
|
|
47
|
+
const onPageChange = vi.fn();
|
|
48
|
+
render(Pagination, {
|
|
49
|
+
props: { currentPage: 3, totalPages: 5, onPageChange },
|
|
50
|
+
});
|
|
51
|
+
await userEvent.click(screen.getByRole('button', { name: 'Previous page' }));
|
|
52
|
+
expect(onPageChange).toHaveBeenLastCalledWith(2);
|
|
53
|
+
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
54
|
+
expect(onPageChange).toHaveBeenLastCalledWith(4);
|
|
55
|
+
});
|
|
56
|
+
it('disables prev/first on the first page and blocks their callback', async () => {
|
|
57
|
+
const onPageChange = vi.fn();
|
|
58
|
+
render(Pagination, {
|
|
59
|
+
props: { currentPage: 1, totalPages: 5, onPageChange },
|
|
60
|
+
});
|
|
61
|
+
const prev = screen.getByRole('button', { name: 'Previous page' });
|
|
62
|
+
const first = screen.getByRole('button', { name: 'First page' });
|
|
63
|
+
expect(prev).toBeDisabled();
|
|
64
|
+
expect(first).toBeDisabled();
|
|
65
|
+
await userEvent.click(prev);
|
|
66
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
it('disables next/last on the last page and blocks their callback', async () => {
|
|
69
|
+
const onPageChange = vi.fn();
|
|
70
|
+
render(Pagination, {
|
|
71
|
+
props: { currentPage: 5, totalPages: 5, onPageChange },
|
|
72
|
+
});
|
|
73
|
+
const next = screen.getByRole('button', { name: 'Next page' });
|
|
74
|
+
const last = screen.getByRole('button', { name: /Last page/ });
|
|
75
|
+
expect(next).toBeDisabled();
|
|
76
|
+
expect(last).toBeDisabled();
|
|
77
|
+
await userEvent.click(next);
|
|
78
|
+
expect(onPageChange).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
it('renders numbered pages as links in link mode (no onPageChange)', () => {
|
|
81
|
+
render(Pagination, {
|
|
82
|
+
props: { currentPage: 1, totalPages: 3, baseUrl: '/articles' },
|
|
83
|
+
});
|
|
84
|
+
const page2 = screen.getByRole('link', { name: 'Go to page 2' });
|
|
85
|
+
expect(page2).toHaveAttribute('href', '/articles/page/2');
|
|
86
|
+
});
|
|
87
|
+
it('is axe-clean in callback mode on a middle page', async () => {
|
|
88
|
+
const { container } = render(Pagination, {
|
|
89
|
+
props: { currentPage: 3, totalPages: 10, onPageChange: vi.fn() },
|
|
90
|
+
});
|
|
91
|
+
await expectNoA11yViolations(container);
|
|
92
|
+
});
|
|
93
|
+
it('is axe-clean in link mode at the first-page boundary', async () => {
|
|
94
|
+
const { container } = render(Pagination, {
|
|
95
|
+
props: { currentPage: 1, totalPages: 4, baseUrl: '/articles' },
|
|
96
|
+
});
|
|
97
|
+
await expectNoA11yViolations(container);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden tests for the L3 gap primitives — batch 2 (Tooltip, Dropdown, Tree).
|
|
3
|
+
* Render + keyboard/pointer interaction + a11y, per the L4 harness (#1422).
|
|
4
|
+
*/
|
|
5
|
+
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { createRawSnippet } from 'svelte';
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
10
|
+
import Dropdown from '../Dropdown.svelte';
|
|
11
|
+
import Tooltip from '../Tooltip.svelte';
|
|
12
|
+
import Tree from '../Tree.svelte';
|
|
13
|
+
const triggerSnippet = (text) => createRawSnippet(() => ({ render: () => `<button>${text}</button>` }));
|
|
14
|
+
describe('Tooltip', () => {
|
|
15
|
+
it('wires the trigger to the bubble via aria-describedby', () => {
|
|
16
|
+
render(Tooltip, {
|
|
17
|
+
props: { text: 'More info', children: triggerSnippet('Help') },
|
|
18
|
+
});
|
|
19
|
+
const tip = screen.getByRole('tooltip');
|
|
20
|
+
expect(tip).toHaveTextContent('More info');
|
|
21
|
+
// The trigger wrapper (direct parent of the snippet content) is described
|
|
22
|
+
// by the bubble.
|
|
23
|
+
const trigger = screen.getByRole('button', { name: 'Help' }).parentElement;
|
|
24
|
+
expect(trigger).toHaveAttribute('aria-describedby', tip.id);
|
|
25
|
+
});
|
|
26
|
+
it('reveals on hover', async () => {
|
|
27
|
+
render(Tooltip, {
|
|
28
|
+
props: { text: 'More info', delay: 0, children: triggerSnippet('Help') },
|
|
29
|
+
});
|
|
30
|
+
await userEvent.hover(screen.getByRole('button', { name: 'Help' }));
|
|
31
|
+
await waitFor(() => expect(screen.getByRole('tooltip')).toHaveClass('open'));
|
|
32
|
+
});
|
|
33
|
+
it('is axe-clean', async () => {
|
|
34
|
+
const { container } = render(Tooltip, {
|
|
35
|
+
props: { text: 'More info', children: triggerSnippet('Help') },
|
|
36
|
+
});
|
|
37
|
+
await expectNoA11yViolations(container);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
const MENU = [
|
|
41
|
+
{ id: 'edit', label: 'Edit' },
|
|
42
|
+
{ id: 'dup', label: 'Duplicate' },
|
|
43
|
+
{ id: 'del', label: 'Delete' },
|
|
44
|
+
];
|
|
45
|
+
describe('Dropdown', () => {
|
|
46
|
+
it('trigger advertises a collapsed menu', () => {
|
|
47
|
+
render(Dropdown, { props: { label: 'Actions', items: MENU } });
|
|
48
|
+
const trigger = screen.getByRole('button', { name: 'Actions' });
|
|
49
|
+
expect(trigger).toHaveAttribute('aria-haspopup', 'menu');
|
|
50
|
+
expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
51
|
+
});
|
|
52
|
+
it('opens a menu and reports selection', async () => {
|
|
53
|
+
const onselect = vi.fn();
|
|
54
|
+
render(Dropdown, { props: { label: 'Actions', items: MENU, onselect } });
|
|
55
|
+
await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
|
|
56
|
+
expect(screen.getByRole('menu')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getAllByRole('menuitem')).toHaveLength(3);
|
|
58
|
+
await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
|
59
|
+
expect(onselect).toHaveBeenCalledWith('del');
|
|
60
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
it('closes on Escape', async () => {
|
|
63
|
+
render(Dropdown, { props: { label: 'Actions', items: MENU } });
|
|
64
|
+
await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
|
|
65
|
+
await userEvent.keyboard('{Escape}');
|
|
66
|
+
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
it('is axe-clean when open', async () => {
|
|
69
|
+
const { container } = render(Dropdown, {
|
|
70
|
+
props: { label: 'Actions', items: MENU },
|
|
71
|
+
});
|
|
72
|
+
await userEvent.click(screen.getByRole('button', { name: 'Actions' }));
|
|
73
|
+
await expectNoA11yViolations(container);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const NODES = [
|
|
77
|
+
{
|
|
78
|
+
id: 'src',
|
|
79
|
+
label: 'src',
|
|
80
|
+
children: [
|
|
81
|
+
{ id: 'a', label: 'a.ts' },
|
|
82
|
+
{ id: 'b', label: 'b.ts' },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
{ id: 'readme', label: 'README.md' },
|
|
86
|
+
];
|
|
87
|
+
describe('Tree', () => {
|
|
88
|
+
it('is a labelled tree with levelled items', () => {
|
|
89
|
+
render(Tree, { props: { nodes: NODES, 'aria-label': 'Files' } });
|
|
90
|
+
expect(screen.getByRole('tree', { name: 'Files' })).toBeInTheDocument();
|
|
91
|
+
const folder = screen.getByRole('treeitem', { name: /src/ });
|
|
92
|
+
expect(folder).toHaveAttribute('aria-level', '1');
|
|
93
|
+
expect(folder).toHaveAttribute('aria-expanded', 'false');
|
|
94
|
+
});
|
|
95
|
+
it('expands a node to reveal children', async () => {
|
|
96
|
+
render(Tree, { props: { nodes: NODES } });
|
|
97
|
+
await userEvent.click(screen.getByRole('treeitem', { name: /src/ }));
|
|
98
|
+
expect(screen.getByRole('treeitem', { name: 'a.ts' })).toHaveAttribute('aria-level', '2');
|
|
99
|
+
});
|
|
100
|
+
it('reports leaf selection', async () => {
|
|
101
|
+
const onselect = vi.fn();
|
|
102
|
+
render(Tree, { props: { nodes: NODES, onselect } });
|
|
103
|
+
await userEvent.click(screen.getByRole('treeitem', { name: 'README.md' }));
|
|
104
|
+
expect(onselect).toHaveBeenCalledWith('readme');
|
|
105
|
+
});
|
|
106
|
+
it('is axe-clean', async () => {
|
|
107
|
+
const { container } = render(Tree, {
|
|
108
|
+
props: { nodes: NODES, expanded: ['src'], 'aria-label': 'Files' },
|
|
109
|
+
});
|
|
110
|
+
await expectNoA11yViolations(container);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Golden tests for the L3 gap primitives — batch 1 (Avatar, Chip, Skeleton).
|
|
3
|
+
* Render + interaction + a11y, per the L4 harness pattern (#1422 / #1423).
|
|
4
|
+
*/
|
|
5
|
+
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
6
|
+
import userEvent from '@testing-library/user-event';
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
9
|
+
import Avatar from '../Avatar.svelte';
|
|
10
|
+
import Chip from '../Chip.svelte';
|
|
11
|
+
import Skeleton from '../Skeleton.svelte';
|
|
12
|
+
describe('Avatar', () => {
|
|
13
|
+
it('shows initials + accessible name when no src', () => {
|
|
14
|
+
render(Avatar, { props: { name: 'Ada Lovelace' } });
|
|
15
|
+
const img = screen.getByRole('img', { name: 'Ada Lovelace' });
|
|
16
|
+
expect(img).toHaveTextContent('AL');
|
|
17
|
+
});
|
|
18
|
+
it('renders an <img> with alt when src is set', () => {
|
|
19
|
+
render(Avatar, { props: { name: 'Ada', src: 'https://x/a.png' } });
|
|
20
|
+
expect(screen.getByRole('img', { name: 'Ada' }).tagName).toBe('IMG');
|
|
21
|
+
});
|
|
22
|
+
it('announces presence status', () => {
|
|
23
|
+
render(Avatar, { props: { name: 'Ada', status: 'online' } });
|
|
24
|
+
expect(screen.getByText('Online')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
it('falls back to initials when the image fails to load', async () => {
|
|
27
|
+
render(Avatar, {
|
|
28
|
+
props: { name: 'Ada Lovelace', src: 'https://x/broken.png' },
|
|
29
|
+
});
|
|
30
|
+
const img = screen.getByRole('img', { name: 'Ada Lovelace' });
|
|
31
|
+
expect(img.tagName).toBe('IMG');
|
|
32
|
+
await fireEvent.error(img);
|
|
33
|
+
expect(screen.getByRole('img', { name: 'Ada Lovelace' })).toHaveTextContent('AL');
|
|
34
|
+
});
|
|
35
|
+
it('is axe-clean', async () => {
|
|
36
|
+
const { container } = render(Avatar, {
|
|
37
|
+
props: { name: 'Ada Lovelace', status: 'away' },
|
|
38
|
+
});
|
|
39
|
+
await expectNoA11yViolations(container);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('Chip', () => {
|
|
43
|
+
it('renders its label', () => {
|
|
44
|
+
render(Chip, { props: { label: 'TypeScript' } });
|
|
45
|
+
expect(screen.getByText('TypeScript')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
it('selectable: toggle button exposes aria-pressed and fires onselect', async () => {
|
|
48
|
+
const onselect = vi.fn();
|
|
49
|
+
render(Chip, {
|
|
50
|
+
props: { label: 'Tag', selectable: true, selected: true, onselect },
|
|
51
|
+
});
|
|
52
|
+
const btn = screen.getByRole('button', { name: 'Tag', pressed: true });
|
|
53
|
+
await userEvent.click(btn);
|
|
54
|
+
expect(onselect).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
it('closeable: labelled remove button fires onclose', async () => {
|
|
57
|
+
const onclose = vi.fn();
|
|
58
|
+
render(Chip, { props: { label: 'Tag', closeable: true, onclose } });
|
|
59
|
+
await userEvent.click(screen.getByRole('button', { name: 'Remove Tag' }));
|
|
60
|
+
expect(onclose).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
it('is axe-clean (selectable + closeable)', async () => {
|
|
63
|
+
const { container } = render(Chip, {
|
|
64
|
+
props: { label: 'Tag', selectable: true, closeable: true },
|
|
65
|
+
});
|
|
66
|
+
await expectNoA11yViolations(container);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('Skeleton', () => {
|
|
70
|
+
it('is a labelled status region', () => {
|
|
71
|
+
render(Skeleton, { props: { label: 'Loading profile' } });
|
|
72
|
+
expect(screen.getByRole('status', { name: 'Loading profile' })).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
it('renders one placeholder per text line', () => {
|
|
75
|
+
const { container } = render(Skeleton, {
|
|
76
|
+
props: { variant: 'text', lines: 3 },
|
|
77
|
+
});
|
|
78
|
+
expect(container.querySelectorAll('.skeleton')).toHaveLength(3);
|
|
79
|
+
});
|
|
80
|
+
it('is axe-clean', async () => {
|
|
81
|
+
const { container } = render(Skeleton, { props: { variant: 'circle' } });
|
|
82
|
+
await expectNoA11yViolations(container);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI components - Buttons, cards, badges, and other UI elements
|
|
3
|
+
*/
|
|
4
|
+
export { default as Avatar } from './Avatar.svelte';
|
|
5
|
+
export { default as Badge } from './Badge.svelte';
|
|
6
|
+
export { default as Button } from './Button.svelte';
|
|
7
|
+
export { default as Card } from './Card.svelte';
|
|
8
|
+
export { default as Chip } from './Chip.svelte';
|
|
9
|
+
export { default as Dropdown } from './Dropdown.svelte';
|
|
10
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
11
|
+
export { default as Skeleton } from './Skeleton.svelte';
|
|
12
|
+
export { default as Tooltip } from './Tooltip.svelte';
|
|
13
|
+
export { default as Tree } from './Tree.svelte';
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/ui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI components - Buttons, cards, badges, and other UI elements
|
|
3
|
+
*/
|
|
4
|
+
// Export components
|
|
5
|
+
export { default as Avatar } from './Avatar.svelte';
|
|
6
|
+
export { default as Badge } from './Badge.svelte';
|
|
7
|
+
export { default as Button } from './Button.svelte';
|
|
8
|
+
export { default as Card } from './Card.svelte';
|
|
9
|
+
export { default as Chip } from './Chip.svelte';
|
|
10
|
+
export { default as Dropdown } from './Dropdown.svelte';
|
|
11
|
+
export { default as Pagination } from './Pagination.svelte';
|
|
12
|
+
export { default as Skeleton } from './Skeleton.svelte';
|
|
13
|
+
export { default as Tooltip } from './Tooltip.svelte';
|
|
14
|
+
export { default as Tree } from './Tree.svelte';
|
|
15
|
+
// Note: Component prop types (BadgeProps, ButtonProps, etc.) are available
|
|
16
|
+
// via svelte-package build output but cannot be re-exported here because
|
|
17
|
+
// tsc --noEmit cannot resolve type exports from .svelte files.
|
|
18
|
+
// Generic BadgeProps, ButtonProps, CardProps are re-exported from types-generic.ts.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* `<Trans>` — the component form of `$t` (S13 #1418), for translating strings in
|
|
4
|
+
* element bodies. Equal first-class partner to `useI18n().t` for attributes.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <p><Trans key="ui.pagination.range" vars={{ count, total }} /></p>
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* Phase 1 renders plain interpolated text. Rich interpolation (links, nested
|
|
12
|
+
* components inside placeholders) is a planned extension via snippet values —
|
|
13
|
+
* see the i18n ADR.
|
|
14
|
+
*/
|
|
15
|
+
import type { LanguageVariables } from './render.js';
|
|
16
|
+
import { useI18n } from './use-i18n.js';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
/** Message key, e.g. `ui.data_table.empty`. */
|
|
20
|
+
key: string;
|
|
21
|
+
/** Variables substituted into `{var}` placeholders. */
|
|
22
|
+
vars?: LanguageVariables;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { key, vars }: Props = $props();
|
|
26
|
+
const i18n = useI18n();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{i18n.t(key, vars)}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Trans>` — the component form of `$t` (S13 #1418), for translating strings in
|
|
3
|
+
* element bodies. Equal first-class partner to `useI18n().t` for attributes.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```svelte
|
|
7
|
+
* <p><Trans key="ui.pagination.range" vars={{ count, total }} /></p>
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Phase 1 renders plain interpolated text. Rich interpolation (links, nested
|
|
11
|
+
* components inside placeholders) is a planned extension via snippet values —
|
|
12
|
+
* see the i18n ADR.
|
|
13
|
+
*/
|
|
14
|
+
import type { LanguageVariables } from './render.js';
|
|
15
|
+
interface Props {
|
|
16
|
+
/** Message key, e.g. `ui.data_table.empty`. */
|
|
17
|
+
key: string;
|
|
18
|
+
/** Variables substituted into `{var}` placeholders. */
|
|
19
|
+
vars?: LanguageVariables;
|
|
20
|
+
}
|
|
21
|
+
declare const Trans: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type Trans = ReturnType<typeof Trans>;
|
|
23
|
+
export default Trans;
|
|
24
|
+
//# sourceMappingURL=Trans.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Trans.svelte.d.ts","sourceRoot":"","sources":["../../src/i18n/Trans.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIrD,UAAU,KAAK;IACb,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B;AAeD,QAAA,MAAM,KAAK,2CAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit coverage for the client i18n core (S13 #1418): the message-default
|
|
3
|
+
* registry, the reactive store's resolution order, and `{var}` interpolation.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { clearRegisteredMessages, createI18nContext, defineMessages, getRegisteredDefault, } from '../index.js';
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
clearRegisteredMessages();
|
|
9
|
+
});
|
|
10
|
+
describe('defineMessages', () => {
|
|
11
|
+
it('registers English defaults and returns a typed key map', () => {
|
|
12
|
+
const M = defineMessages({
|
|
13
|
+
'ui.demo.hello': 'Hello',
|
|
14
|
+
'ui.demo.bye': 'Goodbye',
|
|
15
|
+
});
|
|
16
|
+
expect(M['ui.demo.hello']).toBe('ui.demo.hello');
|
|
17
|
+
expect(getRegisteredDefault('ui.demo.hello')).toBe('Hello');
|
|
18
|
+
expect(getRegisteredDefault('ui.demo.bye')).toBe('Goodbye');
|
|
19
|
+
});
|
|
20
|
+
it('is idempotent for an identical default', () => {
|
|
21
|
+
defineMessages({ 'ui.demo.hello': 'Hello' });
|
|
22
|
+
expect(() => defineMessages({ 'ui.demo.hello': 'Hello' })).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
it('throws when a key is redefined with a different default', () => {
|
|
25
|
+
defineMessages({ 'ui.demo.hello': 'Hello' });
|
|
26
|
+
expect(() => defineMessages({ 'ui.demo.hello': 'Hi' })).toThrow(/already registered/);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('I18nStore.t resolution order', () => {
|
|
30
|
+
it('falls back to the registered default when no snapshot has the key', () => {
|
|
31
|
+
defineMessages({ 'ui.demo.empty': 'No data available' });
|
|
32
|
+
const store = createI18nContext();
|
|
33
|
+
expect(store.t('ui.demo.empty')).toBe('No data available');
|
|
34
|
+
expect(store.locale).toBe('en');
|
|
35
|
+
});
|
|
36
|
+
it('prefers the snapshot template over the registered default', () => {
|
|
37
|
+
defineMessages({ 'ui.demo.empty': 'No data available' });
|
|
38
|
+
const store = createI18nContext({
|
|
39
|
+
locale: 'fr',
|
|
40
|
+
messages: { 'ui.demo.empty': 'Aucune donnée' },
|
|
41
|
+
});
|
|
42
|
+
expect(store.t('ui.demo.empty')).toBe('Aucune donnée');
|
|
43
|
+
expect(store.locale).toBe('fr');
|
|
44
|
+
});
|
|
45
|
+
it('falls back to the key itself for an unknown message (never blank)', () => {
|
|
46
|
+
const store = createI18nContext();
|
|
47
|
+
expect(store.t('ui.demo.unknown')).toBe('ui.demo.unknown');
|
|
48
|
+
});
|
|
49
|
+
it('reflects a snapshot reassignment (locale switch)', () => {
|
|
50
|
+
defineMessages({ 'ui.demo.empty': 'No data available' });
|
|
51
|
+
const store = createI18nContext();
|
|
52
|
+
expect(store.t('ui.demo.empty')).toBe('No data available');
|
|
53
|
+
store.snapshot = {
|
|
54
|
+
locale: 'es',
|
|
55
|
+
messages: { 'ui.demo.empty': 'Sin datos' },
|
|
56
|
+
};
|
|
57
|
+
expect(store.locale).toBe('es');
|
|
58
|
+
expect(store.t('ui.demo.empty')).toBe('Sin datos');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('interpolation', () => {
|
|
62
|
+
it('substitutes {var} placeholders from the snapshot template', () => {
|
|
63
|
+
const store = createI18nContext({
|
|
64
|
+
locale: 'en',
|
|
65
|
+
messages: { 'ui.demo.range': 'Showing {count} of {total}' },
|
|
66
|
+
});
|
|
67
|
+
expect(store.t('ui.demo.range', { count: 3, total: 9 })).toBe('Showing 3 of 9');
|
|
68
|
+
});
|
|
69
|
+
it('collapses missing variables to empty strings', () => {
|
|
70
|
+
defineMessages({ 'ui.demo.greet': 'Hi {name}{title}' });
|
|
71
|
+
const store = createI18nContext();
|
|
72
|
+
expect(store.t('ui.demo.greet', { name: 'Ada' })).toBe('Hi Ada');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the client's inlined `renderTemplate` (src/i18n/render.ts) against the
|
|
3
|
+
* canonical `@happyvertical/smrt-languages` implementation so the two
|
|
4
|
+
* interpolation contracts never drift (S13 #1418). The client keeps its own
|
|
5
|
+
* copy to avoid bundling the heavy languages package; this test is the guard.
|
|
6
|
+
*
|
|
7
|
+
* Importing the languages root triggers its `__smrt-register__` manifest
|
|
8
|
+
* side-effect, which throws under vitest's source-alias (dev-server URL, not a
|
|
9
|
+
* file) — unrelated to the pure `renderTemplate`. Record-not-throw via
|
|
10
|
+
* SMRT_STRICT_REGISTRY=false set before a dynamic import.
|
|
11
|
+
*/
|
|
12
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
|
13
|
+
import { renderTemplate as clientRender } from '../render.js';
|
|
14
|
+
let languagesRender;
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
vi.stubEnv('SMRT_STRICT_REGISTRY', 'false');
|
|
17
|
+
({ renderTemplate: languagesRender } = await import('@happyvertical/smrt-languages'));
|
|
18
|
+
});
|
|
19
|
+
const NOW = new Date('2026-06-17T12:00:00.000Z');
|
|
20
|
+
const cases = [
|
|
21
|
+
['No interpolation here', undefined],
|
|
22
|
+
['No interpolation here', {}],
|
|
23
|
+
['Showing {count} of {total}', { count: 3, total: 9 }],
|
|
24
|
+
['Hi {name}{title}', { name: 'Ada' }], // missing → empty
|
|
25
|
+
['Null is {x} here', { x: null }],
|
|
26
|
+
['Date: {when}', { when: NOW }],
|
|
27
|
+
['List: {items}', { items: [1, 2, 3] }],
|
|
28
|
+
['Obj: {meta}', { meta: { a: 1 } }],
|
|
29
|
+
['Trim { spaced } key', { spaced: 'ok' }],
|
|
30
|
+
['Number {n}', { n: 0 }],
|
|
31
|
+
['Bool {b}', { b: false }],
|
|
32
|
+
];
|
|
33
|
+
describe('renderTemplate parity with @happyvertical/smrt-languages', () => {
|
|
34
|
+
it.each(cases)('renders %j identically', (template, vars) => {
|
|
35
|
+
expect(clientRender(template, vars)).toBe(languagesRender(template, vars));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n context + reactive store (Sweep S13 #1418).
|
|
3
|
+
*
|
|
4
|
+
* The `Provider` builds an `I18nStore` from a server snapshot and puts it on a
|
|
5
|
+
* Svelte context; `useI18n()` / `$t` / `<Trans>` read it synchronously. Browser
|
|
6
|
+
* safe — interpolation uses the client's own dependency-free `renderTemplate`
|
|
7
|
+
* (see ./render.ts) so the heavy languages package is never bundled client-side.
|
|
8
|
+
*/
|
|
9
|
+
import { type LanguageVariables } from './render.js';
|
|
10
|
+
/** A per-locale dictionary of message templates, as built on the server. */
|
|
11
|
+
export interface I18nSnapshot {
|
|
12
|
+
/** The active locale (normalized BCP-47-ish tag). */
|
|
13
|
+
locale: string;
|
|
14
|
+
/** `key → template` for the active locale. Variables are applied client-side. */
|
|
15
|
+
messages: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Reactive holder for the active locale + message templates. Locale switching
|
|
19
|
+
* (assigning `snapshot`) re-renders every `$t` / `<Trans>` reading this store.
|
|
20
|
+
*/
|
|
21
|
+
export declare class I18nStore {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(snapshot?: Partial<I18nSnapshot>);
|
|
24
|
+
get locale(): string;
|
|
25
|
+
/** Replace the active snapshot (e.g. on a locale switch). */
|
|
26
|
+
set snapshot(snapshot: I18nSnapshot);
|
|
27
|
+
/**
|
|
28
|
+
* Resolve and interpolate a message. Resolution order: server-snapshot
|
|
29
|
+
* template → registered English default → the key itself (a loud,
|
|
30
|
+
* never-blank fallback). Interpolation uses the same `renderTemplate` the
|
|
31
|
+
* server uses, so `{var}` placeholders render identically.
|
|
32
|
+
*/
|
|
33
|
+
t: (key: string, vars?: LanguageVariables) => string;
|
|
34
|
+
}
|
|
35
|
+
/** Create a store from a snapshot (used by `Provider`). */
|
|
36
|
+
export declare function createI18nContext(snapshot?: Partial<I18nSnapshot>): I18nStore;
|
|
37
|
+
/** Put an i18n store on context (used by `Provider`). */
|
|
38
|
+
export declare function setI18nContext(store: I18nStore): I18nStore;
|
|
39
|
+
/** Read the i18n store, or `null` when there is no `Provider` above. */
|
|
40
|
+
export declare function tryGetI18nContext(): I18nStore | null;
|
|
41
|
+
/** Read the i18n store, creating a default fallback if there is no Provider. */
|
|
42
|
+
export declare function getI18nContext(): I18nStore;
|
|
43
|
+
//# sourceMappingURL=context.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.svelte.d.ts","sourceRoot":"","sources":["../../src/i18n/context.svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,KAAK,iBAAiB,EAAkB,MAAM,aAAa,CAAC;AAErE,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;GAGG;AACH,qBAAa,SAAS;;gBAIR,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC;IAK5C,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,6DAA6D;IAC7D,IAAI,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAGlC;IAED;;;;;OAKG;IACH,CAAC,GAAI,KAAK,MAAM,EAAE,OAAO,iBAAiB,KAAG,MAAM,CAGjD;CACH;AAID,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAE7E;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAE1D;AAED,wEAAwE;AACxE,wBAAgB,iBAAiB,IAAI,SAAS,GAAG,IAAI,CAEpD;AAOD,gFAAgF;AAChF,wBAAgB,cAAc,IAAI,SAAS,CAK1C"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n context + reactive store (Sweep S13 #1418).
|
|
3
|
+
*
|
|
4
|
+
* The `Provider` builds an `I18nStore` from a server snapshot and puts it on a
|
|
5
|
+
* Svelte context; `useI18n()` / `$t` / `<Trans>` read it synchronously. Browser
|
|
6
|
+
* safe — interpolation uses the client's own dependency-free `renderTemplate`
|
|
7
|
+
* (see ./render.ts) so the heavy languages package is never bundled client-side.
|
|
8
|
+
*/
|
|
9
|
+
import { getContext, setContext } from 'svelte';
|
|
10
|
+
import { getRegisteredDefault } from './registry.js';
|
|
11
|
+
import { renderTemplate } from './render.js';
|
|
12
|
+
/**
|
|
13
|
+
* Reactive holder for the active locale + message templates. Locale switching
|
|
14
|
+
* (assigning `snapshot`) re-renders every `$t` / `<Trans>` reading this store.
|
|
15
|
+
*/
|
|
16
|
+
export class I18nStore {
|
|
17
|
+
#locale = $state('en');
|
|
18
|
+
#messages = $state({});
|
|
19
|
+
constructor(snapshot) {
|
|
20
|
+
if (snapshot?.locale)
|
|
21
|
+
this.#locale = snapshot.locale;
|
|
22
|
+
if (snapshot?.messages)
|
|
23
|
+
this.#messages = snapshot.messages;
|
|
24
|
+
}
|
|
25
|
+
get locale() {
|
|
26
|
+
return this.#locale;
|
|
27
|
+
}
|
|
28
|
+
/** Replace the active snapshot (e.g. on a locale switch). */
|
|
29
|
+
set snapshot(snapshot) {
|
|
30
|
+
this.#locale = snapshot.locale;
|
|
31
|
+
this.#messages = snapshot.messages;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve and interpolate a message. Resolution order: server-snapshot
|
|
35
|
+
* template → registered English default → the key itself (a loud,
|
|
36
|
+
* never-blank fallback). Interpolation uses the same `renderTemplate` the
|
|
37
|
+
* server uses, so `{var}` placeholders render identically.
|
|
38
|
+
*/
|
|
39
|
+
t = (key, vars) => {
|
|
40
|
+
const template = this.#messages[key] ?? getRegisteredDefault(key) ?? key;
|
|
41
|
+
return renderTemplate(template, vars);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const I18N_KEY = Symbol('smrt-i18n');
|
|
45
|
+
/** Create a store from a snapshot (used by `Provider`). */
|
|
46
|
+
export function createI18nContext(snapshot) {
|
|
47
|
+
return new I18nStore(snapshot);
|
|
48
|
+
}
|
|
49
|
+
/** Put an i18n store on context (used by `Provider`). */
|
|
50
|
+
export function setI18nContext(store) {
|
|
51
|
+
return setContext(I18N_KEY, store);
|
|
52
|
+
}
|
|
53
|
+
/** Read the i18n store, or `null` when there is no `Provider` above. */
|
|
54
|
+
export function tryGetI18nContext() {
|
|
55
|
+
return getContext(I18N_KEY) ?? null;
|
|
56
|
+
}
|
|
57
|
+
// A process-wide fallback store so `useI18n()` works outside a Provider
|
|
58
|
+
// (standalone primitives, tests) without throwing — it resolves through
|
|
59
|
+
// registered defaults. Created lazily to avoid running runes at module load.
|
|
60
|
+
let fallbackStore = null;
|
|
61
|
+
/** Read the i18n store, creating a default fallback if there is no Provider. */
|
|
62
|
+
export function getI18nContext() {
|
|
63
|
+
const ctx = tryGetI18nContext();
|
|
64
|
+
if (ctx)
|
|
65
|
+
return ctx;
|
|
66
|
+
if (!fallbackStore)
|
|
67
|
+
fallbackStore = new I18nStore();
|
|
68
|
+
return fallbackStore;
|
|
69
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-ui/i18n — client i18n layer (Sweep S13 #1418).
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe. Provides `defineMessages` (register a package's English code
|
|
5
|
+
* defaults), `useI18n()` / `<Trans>` (read + render translations), and the
|
|
6
|
+
* context plumbing the `Provider` uses. The server-side snapshot builder lives
|
|
7
|
+
* on the `./i18n/server` subpath (it pulls the full languages resolver).
|
|
8
|
+
*
|
|
9
|
+
* See `docs/content/architecture/i18n.md` for the design.
|
|
10
|
+
*/
|
|
11
|
+
export { createI18nContext, getI18nContext, type I18nSnapshot, I18nStore, setI18nContext, tryGetI18nContext, } from './context.svelte.js';
|
|
12
|
+
export { clearRegisteredMessages, defineMessages, getRegisteredDefault, getRegisteredDefaults, type MessageCatalog, } from './registry.js';
|
|
13
|
+
export { default as Trans } from './Trans.svelte';
|
|
14
|
+
import './strings.js';
|
|
15
|
+
export { M } from './strings.ui.js';
|
|
16
|
+
export { useI18n } from './use-i18n.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,KAAK,YAAY,EACjB,SAAS,EACT,cAAc,EACd,iBAAiB,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,uBAAuB,EACvB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAMlD,OAAO,cAAc,CAAC;AAKtB,OAAO,EAAE,CAAC,EAAE,MAAM,iBAAiB,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
|