@hjmds/design-contracts 0.8.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.
- package/README.md +208 -0
- package/dist/alert-dialog.d.ts +102 -0
- package/dist/alert-dialog.d.ts.map +1 -0
- package/dist/alert-dialog.js +136 -0
- package/dist/alert-dialog.js.map +1 -0
- package/dist/base-recipes.d.ts +184 -0
- package/dist/base-recipes.d.ts.map +1 -0
- package/dist/base-recipes.js +129 -0
- package/dist/base-recipes.js.map +1 -0
- package/dist/behaviors.d.ts +1254 -0
- package/dist/behaviors.d.ts.map +1 -0
- package/dist/behaviors.js +972 -0
- package/dist/behaviors.js.map +1 -0
- package/dist/bottom-navigation-defaults.d.ts +13 -0
- package/dist/bottom-navigation-defaults.d.ts.map +1 -0
- package/dist/bottom-navigation-defaults.js +10 -0
- package/dist/bottom-navigation-defaults.js.map +1 -0
- package/dist/bottom-navigation.d.ts +114 -0
- package/dist/bottom-navigation.d.ts.map +1 -0
- package/dist/bottom-navigation.js +223 -0
- package/dist/bottom-navigation.js.map +1 -0
- package/dist/breadcrumb.d.ts +97 -0
- package/dist/breadcrumb.d.ts.map +1 -0
- package/dist/breadcrumb.js +99 -0
- package/dist/breadcrumb.js.map +1 -0
- package/dist/calendar.d.ts +285 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +297 -0
- package/dist/calendar.js.map +1 -0
- package/dist/card.d.ts +39 -0
- package/dist/card.d.ts.map +1 -0
- package/dist/card.js +50 -0
- package/dist/card.js.map +1 -0
- package/dist/carousel.d.ts +180 -0
- package/dist/carousel.d.ts.map +1 -0
- package/dist/carousel.js +172 -0
- package/dist/carousel.js.map +1 -0
- package/dist/catalog.d.ts +7158 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +220 -0
- package/dist/catalog.js.map +1 -0
- package/dist/collection.d.ts +123 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +211 -0
- package/dist/collection.js.map +1 -0
- package/dist/color-references.d.ts +46 -0
- package/dist/color-references.d.ts.map +1 -0
- package/dist/color-references.js +39 -0
- package/dist/color-references.js.map +1 -0
- package/dist/colors.d.ts +56 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +90 -0
- package/dist/colors.js.map +1 -0
- package/dist/command-palette.d.ts +240 -0
- package/dist/command-palette.d.ts.map +1 -0
- package/dist/command-palette.js +97 -0
- package/dist/command-palette.js.map +1 -0
- package/dist/component-contracts.d.ts +137 -0
- package/dist/component-contracts.d.ts.map +1 -0
- package/dist/component-contracts.js +53 -0
- package/dist/component-contracts.js.map +1 -0
- package/dist/component-definitions.d.ts +135 -0
- package/dist/component-definitions.d.ts.map +1 -0
- package/dist/component-definitions.js +142 -0
- package/dist/component-definitions.js.map +1 -0
- package/dist/component-recipes.d.ts +3583 -0
- package/dist/component-recipes.d.ts.map +1 -0
- package/dist/component-recipes.js +1503 -0
- package/dist/component-recipes.js.map +1 -0
- package/dist/component-references.d.ts +416 -0
- package/dist/component-references.d.ts.map +1 -0
- package/dist/component-references.js +140 -0
- package/dist/component-references.js.map +1 -0
- package/dist/content-state.d.ts +104 -0
- package/dist/content-state.d.ts.map +1 -0
- package/dist/content-state.js +116 -0
- package/dist/content-state.js.map +1 -0
- package/dist/counter-badge-recipe.d.ts +80 -0
- package/dist/counter-badge-recipe.d.ts.map +1 -0
- package/dist/counter-badge-recipe.js +44 -0
- package/dist/counter-badge-recipe.js.map +1 -0
- package/dist/counter-badge.d.ts +12 -0
- package/dist/counter-badge.d.ts.map +1 -0
- package/dist/counter-badge.js +21 -0
- package/dist/counter-badge.js.map +1 -0
- package/dist/data-table.d.ts +203 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/data-table.js +182 -0
- package/dist/data-table.js.map +1 -0
- package/dist/date-picker.d.ts +268 -0
- package/dist/date-picker.d.ts.map +1 -0
- package/dist/date-picker.js +168 -0
- package/dist/date-picker.js.map +1 -0
- package/dist/description-list.d.ts +68 -0
- package/dist/description-list.d.ts.map +1 -0
- package/dist/description-list.js +85 -0
- package/dist/description-list.js.map +1 -0
- package/dist/design-system-provider.d.ts +80 -0
- package/dist/design-system-provider.d.ts.map +1 -0
- package/dist/design-system-provider.js +147 -0
- package/dist/design-system-provider.js.map +1 -0
- package/dist/evidence.d.ts +49 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +133 -0
- package/dist/evidence.js.map +1 -0
- package/dist/file-picker.d.ts +183 -0
- package/dist/file-picker.d.ts.map +1 -0
- package/dist/file-picker.js +224 -0
- package/dist/file-picker.js.map +1 -0
- package/dist/floating-action-button.d.ts +143 -0
- package/dist/floating-action-button.d.ts.map +1 -0
- package/dist/floating-action-button.js +149 -0
- package/dist/floating-action-button.js.map +1 -0
- package/dist/form.d.ts +143 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +206 -0
- package/dist/form.js.map +1 -0
- package/dist/foundations.d.ts +300 -0
- package/dist/foundations.d.ts.map +1 -0
- package/dist/foundations.js +238 -0
- package/dist/foundations.js.map +1 -0
- package/dist/grid.d.ts +75 -0
- package/dist/grid.d.ts.map +1 -0
- package/dist/grid.js +133 -0
- package/dist/grid.js.map +1 -0
- package/dist/icon-button-recipe.d.ts +99 -0
- package/dist/icon-button-recipe.d.ts.map +1 -0
- package/dist/icon-button-recipe.js +53 -0
- package/dist/icon-button-recipe.js.map +1 -0
- package/dist/icon.d.ts +41 -0
- package/dist/icon.d.ts.map +1 -0
- package/dist/icon.js +147 -0
- package/dist/icon.js.map +1 -0
- package/dist/image.d.ts +82 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +100 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +118 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +118 -0
- package/dist/layout.js.map +1 -0
- package/dist/link.d.ts +57 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +142 -0
- package/dist/link.js.map +1 -0
- package/dist/load-more.d.ts +64 -0
- package/dist/load-more.d.ts.map +1 -0
- package/dist/load-more.js +124 -0
- package/dist/load-more.js.map +1 -0
- package/dist/mentions.d.ts +67 -0
- package/dist/mentions.d.ts.map +1 -0
- package/dist/mentions.js +107 -0
- package/dist/mentions.js.map +1 -0
- package/dist/number-field.d.ts +208 -0
- package/dist/number-field.d.ts.map +1 -0
- package/dist/number-field.js +247 -0
- package/dist/number-field.js.map +1 -0
- package/dist/otp-field.d.ts +152 -0
- package/dist/otp-field.d.ts.map +1 -0
- package/dist/otp-field.js +117 -0
- package/dist/otp-field.js.map +1 -0
- package/dist/pagination.d.ts +181 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +226 -0
- package/dist/pagination.js.map +1 -0
- package/dist/password-field.d.ts +187 -0
- package/dist/password-field.d.ts.map +1 -0
- package/dist/password-field.js +120 -0
- package/dist/password-field.js.map +1 -0
- package/dist/popover.d.ts +152 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +137 -0
- package/dist/popover.js.map +1 -0
- package/dist/progress-recipe.d.ts +43 -0
- package/dist/progress-recipe.d.ts.map +1 -0
- package/dist/progress-recipe.js +16 -0
- package/dist/progress-recipe.js.map +1 -0
- package/dist/recipes.d.ts +31 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +46 -0
- package/dist/recipes.js.map +1 -0
- package/dist/responsive.d.ts +27 -0
- package/dist/responsive.d.ts.map +1 -0
- package/dist/responsive.js +66 -0
- package/dist/responsive.js.map +1 -0
- package/dist/result.d.ts +111 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +97 -0
- package/dist/result.js.map +1 -0
- package/dist/selection-helpers.d.ts +16 -0
- package/dist/selection-helpers.d.ts.map +1 -0
- package/dist/selection-helpers.js +52 -0
- package/dist/selection-helpers.js.map +1 -0
- package/dist/semantic-colors.d.ts +275 -0
- package/dist/semantic-colors.d.ts.map +1 -0
- package/dist/semantic-colors.js +84 -0
- package/dist/semantic-colors.js.map +1 -0
- package/dist/sheet.d.ts +51 -0
- package/dist/sheet.d.ts.map +1 -0
- package/dist/sheet.js +69 -0
- package/dist/sheet.js.map +1 -0
- package/dist/showcase.d.ts +153 -0
- package/dist/showcase.d.ts.map +1 -0
- package/dist/showcase.js +210 -0
- package/dist/showcase.js.map +1 -0
- package/dist/side-panel.d.ts +199 -0
- package/dist/side-panel.d.ts.map +1 -0
- package/dist/side-panel.js +111 -0
- package/dist/side-panel.js.map +1 -0
- package/dist/slider.d.ts +138 -0
- package/dist/slider.d.ts.map +1 -0
- package/dist/slider.js +150 -0
- package/dist/slider.js.map +1 -0
- package/dist/splitter.d.ts +113 -0
- package/dist/splitter.d.ts.map +1 -0
- package/dist/splitter.js +99 -0
- package/dist/splitter.js.map +1 -0
- package/dist/statistic.d.ts +41 -0
- package/dist/statistic.d.ts.map +1 -0
- package/dist/statistic.js +76 -0
- package/dist/statistic.js.map +1 -0
- package/dist/steps.d.ts +196 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +160 -0
- package/dist/steps.js.map +1 -0
- package/dist/tag.d.ts +128 -0
- package/dist/tag.d.ts.map +1 -0
- package/dist/tag.js +93 -0
- package/dist/tag.js.map +1 -0
- package/dist/timeline.d.ts +147 -0
- package/dist/timeline.d.ts.map +1 -0
- package/dist/timeline.js +127 -0
- package/dist/timeline.js.map +1 -0
- package/dist/toast.d.ts +164 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +529 -0
- package/dist/toast.js.map +1 -0
- package/dist/tokens.d.ts +9 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +9 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tooltip.d.ts +44 -0
- package/dist/tooltip.d.ts.map +1 -0
- package/dist/tooltip.js +88 -0
- package/dist/tooltip.js.map +1 -0
- package/dist/tour.d.ts +218 -0
- package/dist/tour.d.ts.map +1 -0
- package/dist/tour.js +211 -0
- package/dist/tour.js.map +1 -0
- package/dist/transfer-list.d.ts +207 -0
- package/dist/transfer-list.d.ts.map +1 -0
- package/dist/transfer-list.js +193 -0
- package/dist/transfer-list.js.map +1 -0
- package/dist/tree-select.d.ts +78 -0
- package/dist/tree-select.d.ts.map +1 -0
- package/dist/tree-select.js +132 -0
- package/dist/tree-select.js.map +1 -0
- package/dist/tree.d.ts +206 -0
- package/dist/tree.d.ts.map +1 -0
- package/dist/tree.js +223 -0
- package/dist/tree.js.map +1 -0
- package/dist/upload-item.d.ts +173 -0
- package/dist/upload-item.d.ts.map +1 -0
- package/dist/upload-item.js +156 -0
- package/dist/upload-item.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/docs/affix.md +63 -0
- package/docs/anchor.md +59 -0
- package/docs/ant-design-coverage.md +119 -0
- package/docs/app-provider.md +50 -0
- package/docs/app-rn-adoption.md +227 -0
- package/docs/architecture.md +320 -0
- package/docs/authoring-brief.md +87 -0
- package/docs/border-beam.md +66 -0
- package/docs/bottom-navigation.md +127 -0
- package/docs/breadcrumb.md +82 -0
- package/docs/calendar.md +154 -0
- package/docs/carousel.md +130 -0
- package/docs/cascader.md +93 -0
- package/docs/catalog-decision-status.md +306 -0
- package/docs/color-picker.md +72 -0
- package/docs/command-palette.md +116 -0
- package/docs/confirm-popover.md +93 -0
- package/docs/consistency-audit.md +383 -0
- package/docs/consumer-release-gate.md +90 -0
- package/docs/content-state.md +156 -0
- package/docs/context-panel.md +88 -0
- package/docs/cross-platform-core-normalization.md +118 -0
- package/docs/data-table.md +79 -0
- package/docs/date-picker.md +84 -0
- package/docs/description-list.md +75 -0
- package/docs/design-system-provider.md +139 -0
- package/docs/dropdown.md +78 -0
- package/docs/expansion-roadmap.md +285 -0
- package/docs/file-picker.md +59 -0
- package/docs/floating-action-button.md +111 -0
- package/docs/form.md +134 -0
- package/docs/generated/component-maturity.md +103 -0
- package/docs/generated/renderer-evidence.json +5597 -0
- package/docs/generated/renderer-evidence.md +132 -0
- package/docs/generated/showcase-manifest.json +3732 -0
- package/docs/icon.md +22 -0
- package/docs/identity.md +121 -0
- package/docs/image.md +84 -0
- package/docs/implementation-0.5.md +85 -0
- package/docs/layout-primitives.md +107 -0
- package/docs/layout.md +83 -0
- package/docs/library-reference-decisions.md +123 -0
- package/docs/link.md +67 -0
- package/docs/load-more.md +31 -0
- package/docs/mentions.md +82 -0
- package/docs/migration-0.2.md +108 -0
- package/docs/migration-0.3.md +72 -0
- package/docs/migration-0.5.md +82 -0
- package/docs/migration-0.6.md +197 -0
- package/docs/notification.md +54 -0
- package/docs/number-field.md +82 -0
- package/docs/otp-field.md +103 -0
- package/docs/pagination.md +139 -0
- package/docs/password-field.md +98 -0
- package/docs/popover.md +124 -0
- package/docs/promotion-candidates.md +169 -0
- package/docs/qr-code.md +69 -0
- package/docs/rating.md +58 -0
- package/docs/responsive-grid.md +90 -0
- package/docs/result.md +96 -0
- package/docs/showcase.md +97 -0
- package/docs/side-panel.md +71 -0
- package/docs/slider.md +79 -0
- package/docs/splitter.md +55 -0
- package/docs/statistic.md +16 -0
- package/docs/steps.md +115 -0
- package/docs/tag.md +57 -0
- package/docs/time-picker.md +88 -0
- package/docs/timeline.md +146 -0
- package/docs/toast.md +127 -0
- package/docs/tooltip.md +58 -0
- package/docs/tour.md +136 -0
- package/docs/transfer-list.md +94 -0
- package/docs/tree-select.md +101 -0
- package/docs/tree.md +123 -0
- package/docs/upload-item.md +68 -0
- package/docs/utility.md +53 -0
- package/docs/virtual-list.md +70 -0
- package/docs/watermark.md +58 -0
- package/package.json +402 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { designSystemVersion } from "./version.js";
|
|
2
|
+
import { createShowcaseCoverage, showcaseManifest, showcaseScenarios, } from "./showcase.js";
|
|
3
|
+
export const designSystemEvidenceSchemaVersion = 1;
|
|
4
|
+
const scenarioIds = new Set(showcaseScenarios.map(({ id }) => id));
|
|
5
|
+
const evidenceSourceKinds = new Set([
|
|
6
|
+
"storybook",
|
|
7
|
+
"interaction-test",
|
|
8
|
+
"accessibility-audit",
|
|
9
|
+
"visual-regression",
|
|
10
|
+
"device-test",
|
|
11
|
+
"contract-test",
|
|
12
|
+
]);
|
|
13
|
+
const surfaces = new Set(["contract", "web", "native"]);
|
|
14
|
+
function requireNonEmptyString(value, field) {
|
|
15
|
+
if (value.trim().length === 0)
|
|
16
|
+
throw new Error(`${field} must not be empty`);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns the canonical component story ids that currently require evidence on
|
|
20
|
+
* one surface. Consumers use this instead of maintaining hard-coded lists.
|
|
21
|
+
*/
|
|
22
|
+
export function getShowcaseStoryIdsForSurface(surface, manifest = showcaseManifest) {
|
|
23
|
+
return manifest
|
|
24
|
+
.filter(({ requirements }) => requirements.some((requirement) => requirement.surface === surface))
|
|
25
|
+
.map(({ storyId }) => storyId);
|
|
26
|
+
}
|
|
27
|
+
/** Compares a product Storybook inventory with the pinned contract package. */
|
|
28
|
+
export function compareShowcaseStoryIds(surface, storyIds, manifest = showcaseManifest) {
|
|
29
|
+
const expected = getShowcaseStoryIdsForSurface(surface, manifest);
|
|
30
|
+
const expectedSet = new Set(expected);
|
|
31
|
+
const actual = [...new Set(storyIds)];
|
|
32
|
+
const counts = new Map();
|
|
33
|
+
for (const storyId of storyIds)
|
|
34
|
+
counts.set(storyId, (counts.get(storyId) ?? 0) + 1);
|
|
35
|
+
const missing = expected.filter((storyId) => !actual.includes(storyId));
|
|
36
|
+
const unexpected = actual.filter((storyId) => !expectedSet.has(storyId));
|
|
37
|
+
const duplicates = [...counts]
|
|
38
|
+
.filter(([, count]) => count > 1)
|
|
39
|
+
.map(([storyId]) => storyId)
|
|
40
|
+
.sort();
|
|
41
|
+
return {
|
|
42
|
+
surface,
|
|
43
|
+
expected,
|
|
44
|
+
actual,
|
|
45
|
+
missing,
|
|
46
|
+
unexpected,
|
|
47
|
+
duplicates,
|
|
48
|
+
complete: missing.length === 0 && unexpected.length === 0 && duplicates.length === 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function assertShowcaseStoryIds(surface, storyIds, manifest = showcaseManifest) {
|
|
52
|
+
const result = compareShowcaseStoryIds(surface, storyIds, manifest);
|
|
53
|
+
if (result.complete)
|
|
54
|
+
return;
|
|
55
|
+
throw new Error([
|
|
56
|
+
`${surface} Storybook inventory does not match the design contract manifest.`,
|
|
57
|
+
`Missing: ${result.missing.join(", ") || "none"}`,
|
|
58
|
+
`Unexpected: ${result.unexpected.join(", ") || "none"}`,
|
|
59
|
+
`Duplicates: ${result.duplicates.join(", ") || "none"}`,
|
|
60
|
+
].join("\n"));
|
|
61
|
+
}
|
|
62
|
+
/** Runtime validation for evidence artifacts submitted by product repositories. */
|
|
63
|
+
export function defineDesignSystemEvidence(input, manifest = showcaseManifest) {
|
|
64
|
+
if (input.schemaVersion !== designSystemEvidenceSchemaVersion) {
|
|
65
|
+
throw new Error(`Unsupported evidence schema version: ${input.schemaVersion}`);
|
|
66
|
+
}
|
|
67
|
+
requireNonEmptyString(input.designSystemVersion, "designSystemVersion");
|
|
68
|
+
if (input.designSystemVersion !== designSystemVersion) {
|
|
69
|
+
throw new Error(`Evidence designSystemVersion ${input.designSystemVersion} does not match current ${designSystemVersion}`);
|
|
70
|
+
}
|
|
71
|
+
requireNonEmptyString(input.source.id, "source.id");
|
|
72
|
+
requireNonEmptyString(input.source.product, "source.product");
|
|
73
|
+
if (!surfaces.has(input.source.surface)) {
|
|
74
|
+
throw new Error(`Unknown evidence surface: ${input.source.surface}`);
|
|
75
|
+
}
|
|
76
|
+
if (!evidenceSourceKinds.has(input.source.kind)) {
|
|
77
|
+
throw new Error(`Unknown evidence source kind: ${input.source.kind}`);
|
|
78
|
+
}
|
|
79
|
+
if (input.source.revision !== undefined)
|
|
80
|
+
requireNonEmptyString(input.source.revision, "source.revision");
|
|
81
|
+
const requirementsForSurface = new Map();
|
|
82
|
+
for (const item of manifest) {
|
|
83
|
+
const requirement = item.requirements.find(({ surface }) => surface === input.source.surface);
|
|
84
|
+
if (requirement) {
|
|
85
|
+
requirementsForSurface.set(item.storyId, new Set(requirement.scenarios));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const entry of input.entries) {
|
|
90
|
+
requireNonEmptyString(entry.storyId, "entries[].storyId");
|
|
91
|
+
const requiredScenarios = requirementsForSurface.get(entry.storyId);
|
|
92
|
+
if (!requiredScenarios) {
|
|
93
|
+
throw new Error(`Evidence ${input.source.id} contains unsupported ${input.source.surface} story: ${entry.storyId}`);
|
|
94
|
+
}
|
|
95
|
+
if (seen.has(entry.storyId)) {
|
|
96
|
+
throw new Error(`Evidence ${input.source.id} contains duplicate story: ${entry.storyId}`);
|
|
97
|
+
}
|
|
98
|
+
seen.add(entry.storyId);
|
|
99
|
+
if (entry.scenarios.length === 0) {
|
|
100
|
+
throw new Error(`Evidence ${input.source.id}/${entry.storyId} must include at least one scenario`);
|
|
101
|
+
}
|
|
102
|
+
const entryScenarios = new Set();
|
|
103
|
+
for (const scenario of entry.scenarios) {
|
|
104
|
+
if (!scenarioIds.has(scenario))
|
|
105
|
+
throw new Error(`Unknown evidence scenario: ${scenario}`);
|
|
106
|
+
if (!requiredScenarios.has(scenario)) {
|
|
107
|
+
throw new Error(`Evidence ${input.source.id}/${entry.storyId} scenario ${scenario} is not required for ${input.source.surface}`);
|
|
108
|
+
}
|
|
109
|
+
if (entryScenarios.has(scenario)) {
|
|
110
|
+
throw new Error(`Evidence ${input.source.id}/${entry.storyId} repeats scenario: ${scenario}`);
|
|
111
|
+
}
|
|
112
|
+
entryScenarios.add(scenario);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return input;
|
|
116
|
+
}
|
|
117
|
+
export function createDesignSystemEvidence(source, entries) {
|
|
118
|
+
return defineDesignSystemEvidence({
|
|
119
|
+
schemaVersion: designSystemEvidenceSchemaVersion,
|
|
120
|
+
designSystemVersion,
|
|
121
|
+
source,
|
|
122
|
+
entries,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function toShowcaseEvidenceEntries(evidence) {
|
|
126
|
+
return evidence.flatMap(({ source, entries }) => entries.map(({ storyId, scenarios }) => ({ storyId, surface: source.surface, scenarios })));
|
|
127
|
+
}
|
|
128
|
+
export function createDesignSystemEvidenceCoverage(evidence, manifest = showcaseManifest) {
|
|
129
|
+
for (const item of evidence)
|
|
130
|
+
defineDesignSystemEvidence(item, manifest);
|
|
131
|
+
return createShowcaseCoverage(toShowcaseEvidenceEntries(evidence), manifest);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evidence.js","sourceRoot":"","sources":["../src/evidence.ts"],"names":[],"mappings":"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","sourcesContent":["import { designSystemVersion } from \"./version.js\";\nimport {\n createShowcaseCoverage,\n showcaseManifest,\n showcaseScenarios,\n type ShowcaseComponentEntry,\n type ShowcaseCoverageEntry,\n type ShowcaseEvidenceEntry,\n type ShowcaseScenarioId,\n type ShowcaseSurface,\n} from \"./showcase.js\";\n\nexport const designSystemEvidenceSchemaVersion = 1 as const;\n\nexport type DesignSystemEvidenceSourceKind =\n | \"storybook\"\n | \"interaction-test\"\n | \"accessibility-audit\"\n | \"visual-regression\"\n | \"device-test\"\n | \"contract-test\";\n\nexport type DesignSystemEvidenceSource = Readonly<{\n /** Stable repository or product identifier, for example `burntok-web`. */\n id: string;\n /** Human-readable owner shown in generated reports. */\n product: string;\n surface: ShowcaseSurface;\n kind: DesignSystemEvidenceSourceKind;\n /** Optional immutable Git revision or release identifier. */\n revision?: string;\n}>;\n\nexport type DesignSystemEvidenceItem = Readonly<{\n /** Canonical `category/component-id` from `showcaseManifest`. */\n storyId: string;\n scenarios: readonly ShowcaseScenarioId[];\n /** Product Storybook id when it differs from the canonical HJM id. */\n storybookStoryId?: string;\n}>;\n\nexport type DesignSystemEvidenceManifest = Readonly<{\n schemaVersion: typeof designSystemEvidenceSchemaVersion;\n designSystemVersion: string;\n source: DesignSystemEvidenceSource;\n entries: readonly DesignSystemEvidenceItem[];\n}>;\n\nexport type ShowcaseStoryIdComparison = Readonly<{\n surface: ShowcaseSurface;\n expected: readonly string[];\n actual: readonly string[];\n missing: readonly string[];\n unexpected: readonly string[];\n duplicates: readonly string[];\n complete: boolean;\n}>;\n\nconst scenarioIds = new Set<ShowcaseScenarioId>(showcaseScenarios.map(({ id }) => id));\nconst evidenceSourceKinds = new Set<DesignSystemEvidenceSourceKind>([\n \"storybook\",\n \"interaction-test\",\n \"accessibility-audit\",\n \"visual-regression\",\n \"device-test\",\n \"contract-test\",\n]);\nconst surfaces = new Set<ShowcaseSurface>([\"contract\", \"web\", \"native\"]);\n\nfunction requireNonEmptyString(value: string, field: string): void {\n if (value.trim().length === 0) throw new Error(`${field} must not be empty`);\n}\n\n/**\n * Returns the canonical component story ids that currently require evidence on\n * one surface. Consumers use this instead of maintaining hard-coded lists.\n */\nexport function getShowcaseStoryIdsForSurface(\n surface: ShowcaseSurface,\n manifest: readonly ShowcaseComponentEntry[] = showcaseManifest,\n): readonly string[] {\n return manifest\n .filter(({ requirements }) => requirements.some((requirement) => requirement.surface === surface))\n .map(({ storyId }) => storyId);\n}\n\n/** Compares a product Storybook inventory with the pinned contract package. */\nexport function compareShowcaseStoryIds(\n surface: ShowcaseSurface,\n storyIds: readonly string[],\n manifest: readonly ShowcaseComponentEntry[] = showcaseManifest,\n): ShowcaseStoryIdComparison {\n const expected = getShowcaseStoryIdsForSurface(surface, manifest);\n const expectedSet = new Set(expected);\n const actual = [...new Set(storyIds)];\n const counts = new Map<string, number>();\n for (const storyId of storyIds) counts.set(storyId, (counts.get(storyId) ?? 0) + 1);\n const missing = expected.filter((storyId) => !actual.includes(storyId));\n const unexpected = actual.filter((storyId) => !expectedSet.has(storyId));\n const duplicates = [...counts]\n .filter(([, count]) => count > 1)\n .map(([storyId]) => storyId)\n .sort();\n return {\n surface,\n expected,\n actual,\n missing,\n unexpected,\n duplicates,\n complete: missing.length === 0 && unexpected.length === 0 && duplicates.length === 0,\n };\n}\n\nexport function assertShowcaseStoryIds(\n surface: ShowcaseSurface,\n storyIds: readonly string[],\n manifest: readonly ShowcaseComponentEntry[] = showcaseManifest,\n): void {\n const result = compareShowcaseStoryIds(surface, storyIds, manifest);\n if (result.complete) return;\n throw new Error(\n [\n `${surface} Storybook inventory does not match the design contract manifest.`,\n `Missing: ${result.missing.join(\", \") || \"none\"}`,\n `Unexpected: ${result.unexpected.join(\", \") || \"none\"}`,\n `Duplicates: ${result.duplicates.join(\", \") || \"none\"}`,\n ].join(\"\\n\"),\n );\n}\n\n/** Runtime validation for evidence artifacts submitted by product repositories. */\nexport function defineDesignSystemEvidence(\n input: DesignSystemEvidenceManifest,\n manifest: readonly ShowcaseComponentEntry[] = showcaseManifest,\n): DesignSystemEvidenceManifest {\n if (input.schemaVersion !== designSystemEvidenceSchemaVersion) {\n throw new Error(`Unsupported evidence schema version: ${input.schemaVersion}`);\n }\n requireNonEmptyString(input.designSystemVersion, \"designSystemVersion\");\n if (input.designSystemVersion !== designSystemVersion) {\n throw new Error(\n `Evidence designSystemVersion ${input.designSystemVersion} does not match current ${designSystemVersion}`,\n );\n }\n requireNonEmptyString(input.source.id, \"source.id\");\n requireNonEmptyString(input.source.product, \"source.product\");\n if (!surfaces.has(input.source.surface)) {\n throw new Error(`Unknown evidence surface: ${input.source.surface}`);\n }\n if (!evidenceSourceKinds.has(input.source.kind)) {\n throw new Error(`Unknown evidence source kind: ${input.source.kind}`);\n }\n if (input.source.revision !== undefined) requireNonEmptyString(input.source.revision, \"source.revision\");\n\n const requirementsForSurface = new Map<string, ReadonlySet<ShowcaseScenarioId>>();\n for (const item of manifest) {\n const requirement = item.requirements.find(({ surface }) => surface === input.source.surface);\n if (requirement) {\n requirementsForSurface.set(item.storyId, new Set(requirement.scenarios));\n }\n }\n const seen = new Set<string>();\n for (const entry of input.entries) {\n requireNonEmptyString(entry.storyId, \"entries[].storyId\");\n const requiredScenarios = requirementsForSurface.get(entry.storyId);\n if (!requiredScenarios) {\n throw new Error(\n `Evidence ${input.source.id} contains unsupported ${input.source.surface} story: ${entry.storyId}`,\n );\n }\n if (seen.has(entry.storyId)) {\n throw new Error(`Evidence ${input.source.id} contains duplicate story: ${entry.storyId}`);\n }\n seen.add(entry.storyId);\n if (entry.scenarios.length === 0) {\n throw new Error(`Evidence ${input.source.id}/${entry.storyId} must include at least one scenario`);\n }\n const entryScenarios = new Set<ShowcaseScenarioId>();\n for (const scenario of entry.scenarios) {\n if (!scenarioIds.has(scenario)) throw new Error(`Unknown evidence scenario: ${scenario}`);\n if (!requiredScenarios.has(scenario)) {\n throw new Error(\n `Evidence ${input.source.id}/${entry.storyId} scenario ${scenario} is not required for ${input.source.surface}`,\n );\n }\n if (entryScenarios.has(scenario)) {\n throw new Error(`Evidence ${input.source.id}/${entry.storyId} repeats scenario: ${scenario}`);\n }\n entryScenarios.add(scenario);\n }\n }\n return input;\n}\n\nexport function createDesignSystemEvidence(\n source: DesignSystemEvidenceSource,\n entries: readonly DesignSystemEvidenceItem[],\n): DesignSystemEvidenceManifest {\n return defineDesignSystemEvidence({\n schemaVersion: designSystemEvidenceSchemaVersion,\n designSystemVersion,\n source,\n entries,\n });\n}\n\nexport function toShowcaseEvidenceEntries(\n evidence: readonly DesignSystemEvidenceManifest[],\n): readonly ShowcaseEvidenceEntry[] {\n return evidence.flatMap(({ source, entries }) =>\n entries.map(({ storyId, scenarios }) => ({ storyId, surface: source.surface, scenarios })),\n );\n}\n\nexport function createDesignSystemEvidenceCoverage(\n evidence: readonly DesignSystemEvidenceManifest[],\n manifest: readonly ShowcaseComponentEntry[] = showcaseManifest,\n): readonly ShowcaseCoverageEntry[] {\n for (const item of evidence) defineDesignSystemEvidence(item, manifest);\n return createShowcaseCoverage(toShowcaseEvidenceEntries(evidence), manifest);\n}\n"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
export type FilePickerMode = "single" | "multiple";
|
|
2
|
+
/**
|
|
3
|
+
* MIME pattern ("image/png", "image" + wildcard subtype, or wildcard/wildcard)
|
|
4
|
+
* or extension (".pdf"). Extensions exist because native pickers do not
|
|
5
|
+
* always resolve a MIME type.
|
|
6
|
+
*/
|
|
7
|
+
export type FilePickerAcceptPattern = string;
|
|
8
|
+
export type FilePickerDescriptor = Readonly<{
|
|
9
|
+
mode?: FilePickerMode;
|
|
10
|
+
accept?: readonly FilePickerAcceptPattern[];
|
|
11
|
+
maxSizeBytes?: number;
|
|
12
|
+
/** Only meaningful when `mode` is `"multiple"` — single mode is always capped at one. */
|
|
13
|
+
maxCount?: number;
|
|
14
|
+
}>;
|
|
15
|
+
export type ResolvedFilePickerDescriptor = Readonly<{
|
|
16
|
+
mode: FilePickerMode;
|
|
17
|
+
accept?: readonly FilePickerAcceptPattern[];
|
|
18
|
+
maxSizeBytes?: number;
|
|
19
|
+
maxCount?: number;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const filePickerDefaults: {
|
|
22
|
+
readonly mode: "single";
|
|
23
|
+
};
|
|
24
|
+
/** A file the platform picker or drop target produced, before FilePicker judges it. */
|
|
25
|
+
export type FilePickerCandidate = Readonly<{
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
/** `""` when the platform could not resolve a MIME type. */
|
|
29
|
+
mimeType: string;
|
|
30
|
+
sizeBytes: number;
|
|
31
|
+
}>;
|
|
32
|
+
export type FilePickerRejectionReason = "unsupported-type" | "too-large" | "count-exceeded";
|
|
33
|
+
export type FilePickerRejection = Readonly<{
|
|
34
|
+
file: FilePickerCandidate;
|
|
35
|
+
reason: "unsupported-type";
|
|
36
|
+
accept: readonly FilePickerAcceptPattern[];
|
|
37
|
+
}> | Readonly<{
|
|
38
|
+
file: FilePickerCandidate;
|
|
39
|
+
reason: "too-large";
|
|
40
|
+
maxSizeBytes: number;
|
|
41
|
+
}> | Readonly<{
|
|
42
|
+
file: FilePickerCandidate;
|
|
43
|
+
reason: "count-exceeded";
|
|
44
|
+
maxCount: number;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* `accepted` and `rejected` are reported together on purpose: a rejection
|
|
48
|
+
* never has to be inferred by diffing two lists, and the rest of the batch
|
|
49
|
+
* staying selected is visible in the same result (identity.md's "what broke
|
|
50
|
+
* + what stayed fine").
|
|
51
|
+
*/
|
|
52
|
+
export type FilePickerSelectionResult = Readonly<{
|
|
53
|
+
accepted: readonly FilePickerCandidate[];
|
|
54
|
+
rejected: readonly FilePickerRejection[];
|
|
55
|
+
}>;
|
|
56
|
+
export declare function validateFilePickerDescriptor(descriptor: FilePickerDescriptor): void;
|
|
57
|
+
export declare function resolveFilePickerDescriptor(descriptor: FilePickerDescriptor): ResolvedFilePickerDescriptor;
|
|
58
|
+
export declare function validateFilePickerCandidate(candidate: FilePickerCandidate): void;
|
|
59
|
+
/**
|
|
60
|
+
* `undefined`/empty `accept` means unrestricted. Exported so a Web renderer's
|
|
61
|
+
* live drag-over preview and the post-drop judgment share one rule.
|
|
62
|
+
*/
|
|
63
|
+
export declare function matchesFilePickerAccept(candidate: FilePickerCandidate, accept: readonly FilePickerAcceptPattern[] | undefined): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Judges one selection batch against the descriptor. `existingCount` is a
|
|
66
|
+
* read-only input — like Select's `selectedItem` — for products that let
|
|
67
|
+
* users add files across repeated picks; FilePicker never accumulates a
|
|
68
|
+
* selection list itself.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resolveFilePickerSelection(candidates: readonly FilePickerCandidate[], descriptor: FilePickerDescriptor, existingCount?: number): FilePickerSelectionResult;
|
|
71
|
+
export type FilePickerTriggerPlatform = "web" | "native";
|
|
72
|
+
export type FilePickerTrigger = "button" | "dropzone";
|
|
73
|
+
/**
|
|
74
|
+
* Drag-and-drop is a Web-only enhancement. `validateFilePickerTriggers`
|
|
75
|
+
* enforces the roadmap's adaptive gate: Native never gets a dropzone, and no
|
|
76
|
+
* platform may ship a dropzone without an always-reachable button — the same
|
|
77
|
+
* "automatic mode keeps a manual fallback" principle as LoadMore
|
|
78
|
+
* (`src/load-more.ts`).
|
|
79
|
+
*/
|
|
80
|
+
export declare const filePickerTriggerDefaults: {
|
|
81
|
+
readonly web: readonly ["button", "dropzone"];
|
|
82
|
+
readonly native: readonly ["button"];
|
|
83
|
+
};
|
|
84
|
+
export declare function validateFilePickerTriggers(platform: FilePickerTriggerPlatform, triggers: readonly FilePickerTrigger[]): void;
|
|
85
|
+
export type FilePickerDensity = "compact" | "regular";
|
|
86
|
+
export declare const filePickerRecipe: {
|
|
87
|
+
readonly slots: readonly ["root", "trigger", "dropzone", "hint", "error"];
|
|
88
|
+
readonly defaults: {
|
|
89
|
+
readonly density: "regular";
|
|
90
|
+
};
|
|
91
|
+
readonly trigger: {
|
|
92
|
+
readonly minHeight: 44;
|
|
93
|
+
readonly paddingHorizontal: 16;
|
|
94
|
+
readonly radius: "md";
|
|
95
|
+
readonly color: Readonly<{
|
|
96
|
+
source: "theme";
|
|
97
|
+
key: "contentBrand";
|
|
98
|
+
alpha?: number;
|
|
99
|
+
}>;
|
|
100
|
+
readonly textVariant: "label";
|
|
101
|
+
readonly fontWeight: "700";
|
|
102
|
+
};
|
|
103
|
+
readonly dropzone: {
|
|
104
|
+
readonly borderStyle: "dashed";
|
|
105
|
+
readonly borderWidth: 1;
|
|
106
|
+
readonly borderColor: Readonly<{
|
|
107
|
+
source: "theme";
|
|
108
|
+
key: "border";
|
|
109
|
+
alpha?: number;
|
|
110
|
+
}>;
|
|
111
|
+
readonly activeBorderColor: Readonly<{
|
|
112
|
+
source: "theme";
|
|
113
|
+
key: "contentBrand";
|
|
114
|
+
alpha?: number;
|
|
115
|
+
}>;
|
|
116
|
+
readonly activeBackground: Readonly<{
|
|
117
|
+
source: "theme";
|
|
118
|
+
key: "contentBrand";
|
|
119
|
+
alpha?: number;
|
|
120
|
+
}>;
|
|
121
|
+
readonly radius: "lg";
|
|
122
|
+
readonly padding: 24;
|
|
123
|
+
};
|
|
124
|
+
readonly hint: {
|
|
125
|
+
readonly color: Readonly<{
|
|
126
|
+
source: "theme";
|
|
127
|
+
key: "textMuted";
|
|
128
|
+
alpha?: number;
|
|
129
|
+
}>;
|
|
130
|
+
readonly textVariant: "label";
|
|
131
|
+
};
|
|
132
|
+
readonly error: {
|
|
133
|
+
readonly color: Readonly<{
|
|
134
|
+
source: "theme";
|
|
135
|
+
key: "danger";
|
|
136
|
+
alpha?: number;
|
|
137
|
+
}>;
|
|
138
|
+
readonly textVariant: "label";
|
|
139
|
+
};
|
|
140
|
+
readonly states: {
|
|
141
|
+
readonly focus: {
|
|
142
|
+
readonly color: Readonly<{
|
|
143
|
+
source: "theme";
|
|
144
|
+
key: "contentBrand";
|
|
145
|
+
alpha?: number;
|
|
146
|
+
}>;
|
|
147
|
+
readonly width: 2;
|
|
148
|
+
readonly offset: 2;
|
|
149
|
+
};
|
|
150
|
+
readonly disabledOpacity: 0.5;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* No `open`/dismiss axis: the platform picker sheet is transient OS chrome
|
|
155
|
+
* FilePicker does not model, unlike Select/Sheet which own their surface.
|
|
156
|
+
*/
|
|
157
|
+
export declare const filePickerBehavior: {
|
|
158
|
+
readonly controlled: readonly [];
|
|
159
|
+
readonly inputs: readonly ["descriptor", "existingCount"];
|
|
160
|
+
readonly events: readonly ["onSelect"];
|
|
161
|
+
readonly configuration: {
|
|
162
|
+
readonly mode: readonly ["single", "multiple"];
|
|
163
|
+
};
|
|
164
|
+
readonly defaults: {
|
|
165
|
+
readonly mode: "single";
|
|
166
|
+
};
|
|
167
|
+
readonly stateAxes: {
|
|
168
|
+
readonly availability: readonly ["enabled", "disabled"];
|
|
169
|
+
readonly interaction: readonly ["idle", "hover", "focusVisible", "pressed", "dragged"];
|
|
170
|
+
};
|
|
171
|
+
readonly web: {
|
|
172
|
+
readonly roles: readonly ["button"];
|
|
173
|
+
readonly keyboard: readonly ["Tab", "Enter", "Space"];
|
|
174
|
+
readonly focus: "native";
|
|
175
|
+
};
|
|
176
|
+
readonly native: {
|
|
177
|
+
readonly roles: readonly ["button"];
|
|
178
|
+
readonly states: readonly ["disabled"];
|
|
179
|
+
readonly actions: readonly ["activate"];
|
|
180
|
+
};
|
|
181
|
+
readonly scenarios: readonly ["selecting-files-never-mutates-already-accepted-items", "rejected-files-report-a-reason-without-blocking-the-rest-of-the-batch", "type-and-size-are-judged-at-selection-time-not-upload-time", "count-cap-accounts-for-already-selected-items-across-repeated-picks", "single-mode-behaves-as-an-implicit-one-file-count-cap", "web-dropzone-is-always-paired-with-a-reachable-button-trigger", "native-never-exposes-a-dropzone-trigger", "rejection-is-announced-without-relying-on-color-alone"];
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=file-picker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-picker.d.ts","sourceRoot":"","sources":["../src/file-picker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { focusIndicatorContract } from "./component-contracts.js";
|
|
2
|
+
import { control, fontWeight, opacity, radius, spacing, stroke, } from "./foundations.js";
|
|
3
|
+
import { semanticColors } from "./semantic-colors.js";
|
|
4
|
+
export const filePickerDefaults = {
|
|
5
|
+
mode: "single",
|
|
6
|
+
};
|
|
7
|
+
const ACCEPT_PATTERN = /^(\*\/\*|[a-z0-9.+-]+\/(?:\*|[a-z0-9.+-]+)|\.[a-z0-9]+)$/i;
|
|
8
|
+
function assertNonEmpty(value, field) {
|
|
9
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
10
|
+
throw new TypeError(`FilePicker ${field} must not be empty`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function validateFilePickerDescriptor(descriptor) {
|
|
14
|
+
const mode = descriptor.mode ?? filePickerDefaults.mode;
|
|
15
|
+
if (mode !== "single" && mode !== "multiple") {
|
|
16
|
+
throw new TypeError(`Unsupported FilePicker mode: ${String(mode)}`);
|
|
17
|
+
}
|
|
18
|
+
if (descriptor.accept !== undefined) {
|
|
19
|
+
if (descriptor.accept.length === 0) {
|
|
20
|
+
throw new TypeError("FilePicker accept must not be empty when provided");
|
|
21
|
+
}
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const pattern of descriptor.accept) {
|
|
24
|
+
if (!ACCEPT_PATTERN.test(pattern)) {
|
|
25
|
+
throw new TypeError(`Unsupported FilePicker accept pattern: ${pattern}`);
|
|
26
|
+
}
|
|
27
|
+
const normalized = pattern.toLowerCase();
|
|
28
|
+
if (seen.has(normalized)) {
|
|
29
|
+
throw new TypeError(`Duplicate FilePicker accept pattern: ${pattern}`);
|
|
30
|
+
}
|
|
31
|
+
seen.add(normalized);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (descriptor.maxSizeBytes !== undefined) {
|
|
35
|
+
if (!Number.isFinite(descriptor.maxSizeBytes) || descriptor.maxSizeBytes <= 0) {
|
|
36
|
+
throw new RangeError("FilePicker maxSizeBytes must be a positive finite number");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (descriptor.maxCount !== undefined) {
|
|
40
|
+
if (mode !== "multiple") {
|
|
41
|
+
throw new TypeError("FilePicker maxCount only applies when mode is \"multiple\"");
|
|
42
|
+
}
|
|
43
|
+
if (!Number.isInteger(descriptor.maxCount) || descriptor.maxCount < 1) {
|
|
44
|
+
throw new RangeError("FilePicker maxCount must be a positive integer");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function resolveFilePickerDescriptor(descriptor) {
|
|
49
|
+
validateFilePickerDescriptor(descriptor);
|
|
50
|
+
return { ...descriptor, mode: descriptor.mode ?? filePickerDefaults.mode };
|
|
51
|
+
}
|
|
52
|
+
export function validateFilePickerCandidate(candidate) {
|
|
53
|
+
assertNonEmpty(candidate.id, "candidate id");
|
|
54
|
+
assertNonEmpty(candidate.name, "candidate name");
|
|
55
|
+
if (typeof candidate.mimeType !== "string") {
|
|
56
|
+
throw new TypeError('FilePicker candidate mimeType must be a string (use "" when unknown)');
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isFinite(candidate.sizeBytes) || candidate.sizeBytes < 0) {
|
|
59
|
+
throw new RangeError("FilePicker candidate sizeBytes must be a non-negative finite number");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Splits `"type/subtype"` into a fixed-length tuple so both sides are always `string`. */
|
|
63
|
+
function splitOnSlash(value) {
|
|
64
|
+
const slash = value.indexOf("/");
|
|
65
|
+
if (slash === -1)
|
|
66
|
+
return [value, ""];
|
|
67
|
+
return [value.slice(0, slash), value.slice(slash + 1)];
|
|
68
|
+
}
|
|
69
|
+
function fileExtension(name) {
|
|
70
|
+
const dot = name.lastIndexOf(".");
|
|
71
|
+
return dot === -1 ? "" : name.slice(dot).toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
function matchesPattern(candidate, pattern) {
|
|
74
|
+
if (pattern.startsWith(".")) {
|
|
75
|
+
return fileExtension(candidate.name) === pattern.toLowerCase();
|
|
76
|
+
}
|
|
77
|
+
if (pattern === "*/*")
|
|
78
|
+
return true;
|
|
79
|
+
if (!candidate.mimeType)
|
|
80
|
+
return false;
|
|
81
|
+
const [patternType, patternSubtype] = splitOnSlash(pattern.toLowerCase());
|
|
82
|
+
const [fileType, fileSubtype] = splitOnSlash(candidate.mimeType.toLowerCase());
|
|
83
|
+
if (patternType !== "*" && patternType !== fileType)
|
|
84
|
+
return false;
|
|
85
|
+
return patternSubtype === "*" || patternSubtype === fileSubtype;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `undefined`/empty `accept` means unrestricted. Exported so a Web renderer's
|
|
89
|
+
* live drag-over preview and the post-drop judgment share one rule.
|
|
90
|
+
*/
|
|
91
|
+
export function matchesFilePickerAccept(candidate, accept) {
|
|
92
|
+
if (accept === undefined || accept.length === 0)
|
|
93
|
+
return true;
|
|
94
|
+
return accept.some((pattern) => matchesPattern(candidate, pattern));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Judges one selection batch against the descriptor. `existingCount` is a
|
|
98
|
+
* read-only input — like Select's `selectedItem` — for products that let
|
|
99
|
+
* users add files across repeated picks; FilePicker never accumulates a
|
|
100
|
+
* selection list itself.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveFilePickerSelection(candidates, descriptor, existingCount = 0) {
|
|
103
|
+
const resolved = resolveFilePickerDescriptor(descriptor);
|
|
104
|
+
for (const candidate of candidates)
|
|
105
|
+
validateFilePickerCandidate(candidate);
|
|
106
|
+
if (!Number.isInteger(existingCount) || existingCount < 0) {
|
|
107
|
+
throw new RangeError("FilePicker existingCount must be a non-negative integer");
|
|
108
|
+
}
|
|
109
|
+
const accepted = [];
|
|
110
|
+
const rejected = [];
|
|
111
|
+
for (const candidate of candidates) {
|
|
112
|
+
if (!matchesFilePickerAccept(candidate, resolved.accept)) {
|
|
113
|
+
rejected.push({ file: candidate, reason: "unsupported-type", accept: resolved.accept ?? [] });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (resolved.maxSizeBytes !== undefined && candidate.sizeBytes > resolved.maxSizeBytes) {
|
|
117
|
+
rejected.push({ file: candidate, reason: "too-large", maxSizeBytes: resolved.maxSizeBytes });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
accepted.push(candidate);
|
|
121
|
+
}
|
|
122
|
+
const effectiveMaxCount = resolved.mode === "single" ? 1 : resolved.maxCount;
|
|
123
|
+
if (effectiveMaxCount !== undefined) {
|
|
124
|
+
const room = Math.max(0, effectiveMaxCount - existingCount);
|
|
125
|
+
if (accepted.length > room) {
|
|
126
|
+
const overflow = accepted.splice(room);
|
|
127
|
+
for (const candidate of overflow) {
|
|
128
|
+
rejected.push({ file: candidate, reason: "count-exceeded", maxCount: effectiveMaxCount });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { accepted, rejected };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Drag-and-drop is a Web-only enhancement. `validateFilePickerTriggers`
|
|
136
|
+
* enforces the roadmap's adaptive gate: Native never gets a dropzone, and no
|
|
137
|
+
* platform may ship a dropzone without an always-reachable button — the same
|
|
138
|
+
* "automatic mode keeps a manual fallback" principle as LoadMore
|
|
139
|
+
* (`src/load-more.ts`).
|
|
140
|
+
*/
|
|
141
|
+
export const filePickerTriggerDefaults = {
|
|
142
|
+
web: ["button", "dropzone"],
|
|
143
|
+
native: ["button"],
|
|
144
|
+
};
|
|
145
|
+
export function validateFilePickerTriggers(platform, triggers) {
|
|
146
|
+
if (triggers.length === 0) {
|
|
147
|
+
throw new TypeError("FilePicker triggers must not be empty");
|
|
148
|
+
}
|
|
149
|
+
if (!triggers.includes("button")) {
|
|
150
|
+
throw new TypeError("FilePicker must always expose a button trigger, even with a dropzone");
|
|
151
|
+
}
|
|
152
|
+
if (platform === "native" && triggers.includes("dropzone")) {
|
|
153
|
+
throw new TypeError("FilePicker dropzone trigger is a Web-only enhancement");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export const filePickerRecipe = {
|
|
157
|
+
slots: ["root", "trigger", "dropzone", "hint", "error"],
|
|
158
|
+
defaults: { density: "regular" },
|
|
159
|
+
trigger: {
|
|
160
|
+
minHeight: control.minTouchTarget,
|
|
161
|
+
paddingHorizontal: spacing.md,
|
|
162
|
+
radius: "md",
|
|
163
|
+
color: semanticColors.content.brand,
|
|
164
|
+
textVariant: "label",
|
|
165
|
+
fontWeight: fontWeight.bold,
|
|
166
|
+
},
|
|
167
|
+
dropzone: {
|
|
168
|
+
borderStyle: "dashed",
|
|
169
|
+
borderWidth: stroke.default,
|
|
170
|
+
borderColor: semanticColors.border.default,
|
|
171
|
+
activeBorderColor: semanticColors.border.focus,
|
|
172
|
+
activeBackground: semanticColors.interaction.focus,
|
|
173
|
+
radius: "lg",
|
|
174
|
+
padding: spacing.xl,
|
|
175
|
+
},
|
|
176
|
+
hint: {
|
|
177
|
+
color: semanticColors.content.secondary,
|
|
178
|
+
textVariant: "label",
|
|
179
|
+
},
|
|
180
|
+
error: {
|
|
181
|
+
color: semanticColors.content.danger,
|
|
182
|
+
textVariant: "label",
|
|
183
|
+
},
|
|
184
|
+
states: {
|
|
185
|
+
focus: focusIndicatorContract,
|
|
186
|
+
disabledOpacity: opacity.disabled,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* No `open`/dismiss axis: the platform picker sheet is transient OS chrome
|
|
191
|
+
* FilePicker does not model, unlike Select/Sheet which own their surface.
|
|
192
|
+
*/
|
|
193
|
+
export const filePickerBehavior = {
|
|
194
|
+
controlled: [],
|
|
195
|
+
inputs: ["descriptor", "existingCount"],
|
|
196
|
+
events: ["onSelect"],
|
|
197
|
+
configuration: { mode: ["single", "multiple"] },
|
|
198
|
+
defaults: filePickerDefaults,
|
|
199
|
+
stateAxes: {
|
|
200
|
+
availability: ["enabled", "disabled"],
|
|
201
|
+
interaction: ["idle", "hover", "focusVisible", "pressed", "dragged"],
|
|
202
|
+
},
|
|
203
|
+
web: {
|
|
204
|
+
roles: ["button"],
|
|
205
|
+
keyboard: ["Tab", "Enter", "Space"],
|
|
206
|
+
focus: "native",
|
|
207
|
+
},
|
|
208
|
+
native: {
|
|
209
|
+
roles: ["button"],
|
|
210
|
+
states: ["disabled"],
|
|
211
|
+
actions: ["activate"],
|
|
212
|
+
},
|
|
213
|
+
scenarios: [
|
|
214
|
+
"selecting-files-never-mutates-already-accepted-items",
|
|
215
|
+
"rejected-files-report-a-reason-without-blocking-the-rest-of-the-batch",
|
|
216
|
+
"type-and-size-are-judged-at-selection-time-not-upload-time",
|
|
217
|
+
"count-cap-accounts-for-already-selected-items-across-repeated-picks",
|
|
218
|
+
"single-mode-behaves-as-an-implicit-one-file-count-cap",
|
|
219
|
+
"web-dropzone-is-always-paired-with-a-reachable-button-trigger",
|
|
220
|
+
"native-never-exposes-a-dropzone-trigger",
|
|
221
|
+
"rejection-is-announced-without-relying-on-color-alone",
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
//# sourceMappingURL=file-picker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-picker.js","sourceRoot":"","sources":["../src/file-picker.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACL,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,OAAO,EACP,MAAM,GAGP,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0BtD,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,IAAI,EAAE,QAAQ;CACuC,CAAC;AA6BxD,MAAM,cAAc,GAAG,2DAA2D,CAAC;AAEnF,SAAS,cAAc,CAAC,KAAa,EAAE,KAAa;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,SAAS,CAAC,cAAc,KAAK,oBAAoB,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,UAAgC;IAC3E,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC;IACxD,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QAC7C,MAAM,IAAI,SAAS,CAAC,gCAAgC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACpC,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,SAAS,CAAC,mDAAmD,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;QAC/B,KAAK,MAAM,OAAO,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;YACxC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAClC,MAAM,IAAI,SAAS,CAAC,0CAA0C,OAAO,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;YACzC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,SAAS,CAAC,wCAAwC,OAAO,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,IAAI,UAAU,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,UAAU,CAAC,YAAY,IAAI,CAAC,EAAE,CAAC;YAC9E,MAAM,IAAI,UAAU,CAAC,0DAA0D,CAAC,CAAC;QACnF,CAAC;IACH,CAAC;IACD,IAAI,UAAU,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QACtC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACxB,MAAM,IAAI,SAAS,CAAC,4DAA4D,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,QAAQ,GAAG,CAAC,EAAE,CAAC;YACtE,MAAM,IAAI,UAAU,CAAC,gDAAgD,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,UAAgC;IAEhC,4BAA4B,CAAC,UAAU,CAAC,CAAC;IACzC,OAAO,EAAE,GAAG,UAAU,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,IAAI,kBAAkB,CAAC,IAAI,EAAE,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,SAA8B;IACxE,cAAc,CAAC,SAAS,CAAC,EAAE,EAAE,cAAc,CAAC,CAAC;IAC7C,cAAc,CAAC,SAAS,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IACjD,IAAI,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC3C,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,UAAU,CAAC,qEAAqE,CAAC,CAAC;IAC9F,CAAC;AACH,CAAC;AAED,2FAA2F;AAC3F,SAAS,YAAY,CAAC,KAAa;IACjC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;AACzD,CAAC;AAED,SAAS,cAAc,CAAC,SAA8B,EAAE,OAAe;IACrE,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,aAAa,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC;IACjE,CAAC;IACD,IAAI,OAAO,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC,SAAS,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1E,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,YAAY,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/E,IAAI,WAAW,KAAK,GAAG,IAAI,WAAW,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClE,OAAO,cAAc,KAAK,GAAG,IAAI,cAAc,KAAK,WAAW,CAAC;AAClE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAA8B,EAC9B,MAAsD;IAEtD,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7D,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;AACtE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CACxC,UAA0C,EAC1C,UAAgC,EAChC,aAAa,GAAG,CAAC;IAEjB,MAAM,QAAQ,GAAG,2BAA2B,CAAC,UAAU,CAAC,CAAC;IACzD,KAAK,MAAM,SAAS,IAAI,UAAU;QAAE,2BAA2B,CAAC,SAAS,CAAC,CAAC;IAC3E,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,aAAa,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,UAAU,CAAC,yDAAyD,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAC3C,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAE3C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,CAAC,uBAAuB,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;YAC9F,SAAS;QACX,CAAC;QACD,IAAI,QAAQ,CAAC,YAAY,KAAK,SAAS,IAAI,SAAS,CAAC,SAAS,GAAG,QAAQ,CAAC,YAAY,EAAE,CAAC;YACvF,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC;YAC7F,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3B,CAAC;IAED,MAAM,iBAAiB,GAAG,QAAQ,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC7E,IAAI,iBAAiB,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,GAAG,aAAa,CAAC,CAAC;QAC5D,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACvC,KAAK,MAAM,SAAS,IAAI,QAAQ,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,CAAC;YAC5F,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAKD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,GAAG,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC3B,MAAM,EAAE,CAAC,QAAQ,CAAC;CACgE,CAAC;AAErF,MAAM,UAAU,0BAA0B,CACxC,QAAmC,EACnC,QAAsC;IAEtC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAC;IAC9F,CAAC;IACD,IAAI,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,SAAS,CAAC,uDAAuD,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC;AAID,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,CAAU;IAChE,QAAQ,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE;IAChC,OAAO,EAAE;QACP,SAAS,EAAE,OAAO,CAAC,cAAc;QACjC,iBAAiB,EAAE,OAAO,CAAC,EAAE;QAC7B,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,KAAK;QACnC,WAAW,EAAE,OAAO;QACpB,UAAU,EAAE,UAAU,CAAC,IAAI;KAC5B;IACD,QAAQ,EAAE;QACR,WAAW,EAAE,QAAQ;QACrB,WAAW,EAAE,MAAM,CAAC,OAAO;QAC3B,WAAW,EAAE,cAAc,CAAC,MAAM,CAAC,OAAO;QAC1C,iBAAiB,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK;QAC9C,gBAAgB,EAAE,cAAc,CAAC,WAAW,CAAC,KAAK;QAClD,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,OAAO,CAAC,EAAE;KACpB;IACD,IAAI,EAAE;QACJ,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,SAAS;QACvC,WAAW,EAAE,OAAO;KACrB;IACD,KAAK,EAAE;QACL,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,MAAM;QACpC,WAAW,EAAE,OAAO;KACrB;IACD,MAAM,EAAE;QACN,KAAK,EAAE,sBAAsB;QAC7B,eAAe,EAAE,OAAO,CAAC,QAAQ;KAClC;CAwBF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,UAAU,EAAE,EAAE;IACd,MAAM,EAAE,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,MAAM,EAAE,CAAC,UAAU,CAAC;IACpB,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,EAAE;IAC/C,QAAQ,EAAE,kBAAkB;IAC5B,SAAS,EAAE;QACT,YAAY,EAAE,CAAC,SAAS,EAAE,UAAU,CAAC;QACrC,WAAW,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,SAAS,CAAC;KACrE;IACD,GAAG,EAAE;QACH,KAAK,EAAE,CAAC,QAAQ,CAAC;QACjB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC;QACnC,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE;QACN,KAAK,EAAE,CAAC,QAAQ,CAAC;QACjB,MAAM,EAAE,CAAC,UAAU,CAAC;QACpB,OAAO,EAAE,CAAC,UAAU,CAAC;KACtB;IACD,SAAS,EAAE;QACT,sDAAsD;QACtD,uEAAuE;QACvE,4DAA4D;QAC5D,qEAAqE;QACrE,uDAAuD;QACvD,+DAA+D;QAC/D,yCAAyC;QACzC,uDAAuD;KACxD;CACkC,CAAC","sourcesContent":["import type { ColorReference } from \"./color-references.js\";\nimport type { BehaviorContract } from \"./behaviors.js\";\nimport { focusIndicatorContract } from \"./component-contracts.js\";\nimport {\n control,\n fontWeight,\n opacity,\n radius,\n spacing,\n stroke,\n type FontWeightValue,\n type TextVariant,\n} from \"./foundations.js\";\nimport { semanticColors } from \"./semantic-colors.js\";\n\nexport type FilePickerMode = \"single\" | \"multiple\";\n\n/**\n * MIME pattern (\"image/png\", \"image\" + wildcard subtype, or wildcard/wildcard)\n * or extension (\".pdf\"). Extensions exist because native pickers do not\n * always resolve a MIME type.\n */\nexport type FilePickerAcceptPattern = string;\n\nexport type FilePickerDescriptor = Readonly<{\n mode?: FilePickerMode;\n accept?: readonly FilePickerAcceptPattern[];\n maxSizeBytes?: number;\n /** Only meaningful when `mode` is `\"multiple\"` — single mode is always capped at one. */\n maxCount?: number;\n}>;\n\nexport type ResolvedFilePickerDescriptor = Readonly<{\n mode: FilePickerMode;\n accept?: readonly FilePickerAcceptPattern[];\n maxSizeBytes?: number;\n maxCount?: number;\n}>;\n\nexport const filePickerDefaults = {\n mode: \"single\",\n} as const satisfies Readonly<{ mode: FilePickerMode }>;\n\n/** A file the platform picker or drop target produced, before FilePicker judges it. */\nexport type FilePickerCandidate = Readonly<{\n id: string;\n name: string;\n /** `\"\"` when the platform could not resolve a MIME type. */\n mimeType: string;\n sizeBytes: number;\n}>;\n\nexport type FilePickerRejectionReason = \"unsupported-type\" | \"too-large\" | \"count-exceeded\";\n\nexport type FilePickerRejection =\n | Readonly<{ file: FilePickerCandidate; reason: \"unsupported-type\"; accept: readonly FilePickerAcceptPattern[] }>\n | Readonly<{ file: FilePickerCandidate; reason: \"too-large\"; maxSizeBytes: number }>\n | Readonly<{ file: FilePickerCandidate; reason: \"count-exceeded\"; maxCount: number }>;\n\n/**\n * `accepted` and `rejected` are reported together on purpose: a rejection\n * never has to be inferred by diffing two lists, and the rest of the batch\n * staying selected is visible in the same result (identity.md's \"what broke\n * + what stayed fine\").\n */\nexport type FilePickerSelectionResult = Readonly<{\n accepted: readonly FilePickerCandidate[];\n rejected: readonly FilePickerRejection[];\n}>;\n\nconst ACCEPT_PATTERN = /^(\\*\\/\\*|[a-z0-9.+-]+\\/(?:\\*|[a-z0-9.+-]+)|\\.[a-z0-9]+)$/i;\n\nfunction assertNonEmpty(value: string, field: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`FilePicker ${field} must not be empty`);\n }\n}\n\nexport function validateFilePickerDescriptor(descriptor: FilePickerDescriptor): void {\n const mode = descriptor.mode ?? filePickerDefaults.mode;\n if (mode !== \"single\" && mode !== \"multiple\") {\n throw new TypeError(`Unsupported FilePicker mode: ${String(mode)}`);\n }\n if (descriptor.accept !== undefined) {\n if (descriptor.accept.length === 0) {\n throw new TypeError(\"FilePicker accept must not be empty when provided\");\n }\n const seen = new Set<string>();\n for (const pattern of descriptor.accept) {\n if (!ACCEPT_PATTERN.test(pattern)) {\n throw new TypeError(`Unsupported FilePicker accept pattern: ${pattern}`);\n }\n const normalized = pattern.toLowerCase();\n if (seen.has(normalized)) {\n throw new TypeError(`Duplicate FilePicker accept pattern: ${pattern}`);\n }\n seen.add(normalized);\n }\n }\n if (descriptor.maxSizeBytes !== undefined) {\n if (!Number.isFinite(descriptor.maxSizeBytes) || descriptor.maxSizeBytes <= 0) {\n throw new RangeError(\"FilePicker maxSizeBytes must be a positive finite number\");\n }\n }\n if (descriptor.maxCount !== undefined) {\n if (mode !== \"multiple\") {\n throw new TypeError(\"FilePicker maxCount only applies when mode is \\\"multiple\\\"\");\n }\n if (!Number.isInteger(descriptor.maxCount) || descriptor.maxCount < 1) {\n throw new RangeError(\"FilePicker maxCount must be a positive integer\");\n }\n }\n}\n\nexport function resolveFilePickerDescriptor(\n descriptor: FilePickerDescriptor,\n): ResolvedFilePickerDescriptor {\n validateFilePickerDescriptor(descriptor);\n return { ...descriptor, mode: descriptor.mode ?? filePickerDefaults.mode };\n}\n\nexport function validateFilePickerCandidate(candidate: FilePickerCandidate): void {\n assertNonEmpty(candidate.id, \"candidate id\");\n assertNonEmpty(candidate.name, \"candidate name\");\n if (typeof candidate.mimeType !== \"string\") {\n throw new TypeError('FilePicker candidate mimeType must be a string (use \"\" when unknown)');\n }\n if (!Number.isFinite(candidate.sizeBytes) || candidate.sizeBytes < 0) {\n throw new RangeError(\"FilePicker candidate sizeBytes must be a non-negative finite number\");\n }\n}\n\n/** Splits `\"type/subtype\"` into a fixed-length tuple so both sides are always `string`. */\nfunction splitOnSlash(value: string): readonly [string, string] {\n const slash = value.indexOf(\"/\");\n if (slash === -1) return [value, \"\"];\n return [value.slice(0, slash), value.slice(slash + 1)];\n}\n\nfunction fileExtension(name: string): string {\n const dot = name.lastIndexOf(\".\");\n return dot === -1 ? \"\" : name.slice(dot).toLowerCase();\n}\n\nfunction matchesPattern(candidate: FilePickerCandidate, pattern: string): boolean {\n if (pattern.startsWith(\".\")) {\n return fileExtension(candidate.name) === pattern.toLowerCase();\n }\n if (pattern === \"*/*\") return true;\n if (!candidate.mimeType) return false;\n const [patternType, patternSubtype] = splitOnSlash(pattern.toLowerCase());\n const [fileType, fileSubtype] = splitOnSlash(candidate.mimeType.toLowerCase());\n if (patternType !== \"*\" && patternType !== fileType) return false;\n return patternSubtype === \"*\" || patternSubtype === fileSubtype;\n}\n\n/**\n * `undefined`/empty `accept` means unrestricted. Exported so a Web renderer's\n * live drag-over preview and the post-drop judgment share one rule.\n */\nexport function matchesFilePickerAccept(\n candidate: FilePickerCandidate,\n accept: readonly FilePickerAcceptPattern[] | undefined,\n): boolean {\n if (accept === undefined || accept.length === 0) return true;\n return accept.some((pattern) => matchesPattern(candidate, pattern));\n}\n\n/**\n * Judges one selection batch against the descriptor. `existingCount` is a\n * read-only input — like Select's `selectedItem` — for products that let\n * users add files across repeated picks; FilePicker never accumulates a\n * selection list itself.\n */\nexport function resolveFilePickerSelection(\n candidates: readonly FilePickerCandidate[],\n descriptor: FilePickerDescriptor,\n existingCount = 0,\n): FilePickerSelectionResult {\n const resolved = resolveFilePickerDescriptor(descriptor);\n for (const candidate of candidates) validateFilePickerCandidate(candidate);\n if (!Number.isInteger(existingCount) || existingCount < 0) {\n throw new RangeError(\"FilePicker existingCount must be a non-negative integer\");\n }\n\n const accepted: FilePickerCandidate[] = [];\n const rejected: FilePickerRejection[] = [];\n\n for (const candidate of candidates) {\n if (!matchesFilePickerAccept(candidate, resolved.accept)) {\n rejected.push({ file: candidate, reason: \"unsupported-type\", accept: resolved.accept ?? [] });\n continue;\n }\n if (resolved.maxSizeBytes !== undefined && candidate.sizeBytes > resolved.maxSizeBytes) {\n rejected.push({ file: candidate, reason: \"too-large\", maxSizeBytes: resolved.maxSizeBytes });\n continue;\n }\n accepted.push(candidate);\n }\n\n const effectiveMaxCount = resolved.mode === \"single\" ? 1 : resolved.maxCount;\n if (effectiveMaxCount !== undefined) {\n const room = Math.max(0, effectiveMaxCount - existingCount);\n if (accepted.length > room) {\n const overflow = accepted.splice(room);\n for (const candidate of overflow) {\n rejected.push({ file: candidate, reason: \"count-exceeded\", maxCount: effectiveMaxCount });\n }\n }\n }\n\n return { accepted, rejected };\n}\n\nexport type FilePickerTriggerPlatform = \"web\" | \"native\";\nexport type FilePickerTrigger = \"button\" | \"dropzone\";\n\n/**\n * Drag-and-drop is a Web-only enhancement. `validateFilePickerTriggers`\n * enforces the roadmap's adaptive gate: Native never gets a dropzone, and no\n * platform may ship a dropzone without an always-reachable button — the same\n * \"automatic mode keeps a manual fallback\" principle as LoadMore\n * (`src/load-more.ts`).\n */\nexport const filePickerTriggerDefaults = {\n web: [\"button\", \"dropzone\"],\n native: [\"button\"],\n} as const satisfies Record<FilePickerTriggerPlatform, readonly FilePickerTrigger[]>;\n\nexport function validateFilePickerTriggers(\n platform: FilePickerTriggerPlatform,\n triggers: readonly FilePickerTrigger[],\n): void {\n if (triggers.length === 0) {\n throw new TypeError(\"FilePicker triggers must not be empty\");\n }\n if (!triggers.includes(\"button\")) {\n throw new TypeError(\"FilePicker must always expose a button trigger, even with a dropzone\");\n }\n if (platform === \"native\" && triggers.includes(\"dropzone\")) {\n throw new TypeError(\"FilePicker dropzone trigger is a Web-only enhancement\");\n }\n}\n\nexport type FilePickerDensity = \"compact\" | \"regular\";\n\nexport const filePickerRecipe = {\n slots: [\"root\", \"trigger\", \"dropzone\", \"hint\", \"error\"] as const,\n defaults: { density: \"regular\" },\n trigger: {\n minHeight: control.minTouchTarget,\n paddingHorizontal: spacing.md,\n radius: \"md\",\n color: semanticColors.content.brand,\n textVariant: \"label\",\n fontWeight: fontWeight.bold,\n },\n dropzone: {\n borderStyle: \"dashed\",\n borderWidth: stroke.default,\n borderColor: semanticColors.border.default,\n activeBorderColor: semanticColors.border.focus,\n activeBackground: semanticColors.interaction.focus,\n radius: \"lg\",\n padding: spacing.xl,\n },\n hint: {\n color: semanticColors.content.secondary,\n textVariant: \"label\",\n },\n error: {\n color: semanticColors.content.danger,\n textVariant: \"label\",\n },\n states: {\n focus: focusIndicatorContract,\n disabledOpacity: opacity.disabled,\n },\n} as const satisfies {\n slots: readonly [\"root\", \"trigger\", \"dropzone\", \"hint\", \"error\"];\n defaults: { density: FilePickerDensity };\n trigger: {\n minHeight: number;\n paddingHorizontal: number;\n radius: keyof typeof radius;\n color: ColorReference;\n textVariant: TextVariant;\n fontWeight: FontWeightValue;\n };\n dropzone: {\n borderStyle: \"dashed\";\n borderWidth: number;\n borderColor: ColorReference;\n activeBorderColor: ColorReference;\n activeBackground: ColorReference;\n radius: keyof typeof radius;\n padding: number;\n };\n hint: { color: ColorReference; textVariant: TextVariant };\n error: { color: ColorReference; textVariant: TextVariant };\n states: { focus: typeof focusIndicatorContract; disabledOpacity: number };\n};\n\n/**\n * No `open`/dismiss axis: the platform picker sheet is transient OS chrome\n * FilePicker does not model, unlike Select/Sheet which own their surface.\n */\nexport const filePickerBehavior = {\n controlled: [],\n inputs: [\"descriptor\", \"existingCount\"],\n events: [\"onSelect\"],\n configuration: { mode: [\"single\", \"multiple\"] },\n defaults: filePickerDefaults,\n stateAxes: {\n availability: [\"enabled\", \"disabled\"],\n interaction: [\"idle\", \"hover\", \"focusVisible\", \"pressed\", \"dragged\"],\n },\n web: {\n roles: [\"button\"],\n keyboard: [\"Tab\", \"Enter\", \"Space\"],\n focus: \"native\",\n },\n native: {\n roles: [\"button\"],\n states: [\"disabled\"],\n actions: [\"activate\"],\n },\n scenarios: [\n \"selecting-files-never-mutates-already-accepted-items\",\n \"rejected-files-report-a-reason-without-blocking-the-rest-of-the-batch\",\n \"type-and-size-are-judged-at-selection-time-not-upload-time\",\n \"count-cap-accounts-for-already-selected-items-across-repeated-picks\",\n \"single-mode-behaves-as-an-implicit-one-file-count-cap\",\n \"web-dropzone-is-always-paired-with-a-reachable-button-trigger\",\n \"native-never-exposes-a-dropzone-trigger\",\n \"rejection-is-announced-without-relying-on-color-alone\",\n ],\n} as const satisfies BehaviorContract;\n"]}
|