@burdenoff/microfe-buildmyiq 2026.1030.1 → 2026.1030.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/buildmyiq/components/AccessibilityChecker.js +1 -1
  2. package/dist/buildmyiq/components/AccessibilityChecker.js.map +1 -1
  3. package/dist/buildmyiq/components/BuiDiscussion.d.ts.map +1 -1
  4. package/dist/buildmyiq/components/BuiDiscussion.js +4 -2
  5. package/dist/buildmyiq/components/BuiDiscussion.js.map +1 -1
  6. package/dist/buildmyiq/components/BuiTags.d.ts.map +1 -1
  7. package/dist/buildmyiq/components/BuiTags.js +8 -3
  8. package/dist/buildmyiq/components/BuiTags.js.map +1 -1
  9. package/dist/buildmyiq/components/DataTable.js +2 -2
  10. package/dist/buildmyiq/components/DataTable.js.map +1 -1
  11. package/dist/buildmyiq/components/EmptyState.d.ts.map +1 -1
  12. package/dist/buildmyiq/components/EmptyState.js +33 -8
  13. package/dist/buildmyiq/components/EmptyState.js.map +1 -1
  14. package/dist/buildmyiq/components/EvidenceUpload.d.ts.map +1 -1
  15. package/dist/buildmyiq/components/EvidenceUpload.js +20 -9
  16. package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
  17. package/dist/buildmyiq/components/FilterTabs.js +1 -1
  18. package/dist/buildmyiq/components/FilterTabs.js.map +1 -1
  19. package/dist/buildmyiq/components/GroundedTutorPanel.d.ts.map +1 -1
  20. package/dist/buildmyiq/components/GroundedTutorPanel.js +5 -3
  21. package/dist/buildmyiq/components/GroundedTutorPanel.js.map +1 -1
  22. package/dist/buildmyiq/components/OfflineLessonControl.js +3 -3
  23. package/dist/buildmyiq/components/OfflineLessonControl.js.map +1 -1
  24. package/dist/buildmyiq/components/Pagination.js +2 -2
  25. package/dist/buildmyiq/components/Pagination.js.map +1 -1
  26. package/dist/buildmyiq/components/StateViews.d.ts +5 -0
  27. package/dist/buildmyiq/components/StateViews.d.ts.map +1 -1
  28. package/dist/buildmyiq/components/StateViews.js +11 -2
  29. package/dist/buildmyiq/components/StateViews.js.map +1 -1
  30. package/dist/buildmyiq/components/Switch.d.ts +6 -0
  31. package/dist/buildmyiq/components/Switch.d.ts.map +1 -0
  32. package/dist/buildmyiq/components/Switch.js +12 -0
  33. package/dist/buildmyiq/components/Switch.js.map +1 -0
  34. package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -1
  35. package/dist/buildmyiq/pages/DashboardPage.js +5 -5
  36. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  37. package/dist/buildmyiq/pages/_shared/CourseCard.js +1 -1
  38. package/dist/buildmyiq/pages/_shared/CourseCard.js.map +1 -1
  39. package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.d.ts.map +1 -1
  40. package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.js +53 -51
  41. package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.js.map +1 -1
  42. package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.d.ts.map +1 -1
  43. package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.js +5 -3
  44. package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.js.map +1 -1
  45. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js +2 -2
  46. package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js.map +1 -1
  47. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -1
  48. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js +12 -10
  49. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
  50. package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -1
  51. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +97 -95
  52. package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
  53. package/dist/buildmyiq/pages/assessments/ProctoringGate.js +5 -5
  54. package/dist/buildmyiq/pages/assessments/ProctoringGate.js.map +1 -1
  55. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -1
  56. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +14 -6
  57. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
  58. package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
  59. package/dist/buildmyiq/pages/authoring/AuthoringPage.js +132 -128
  60. package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
  61. package/dist/buildmyiq/pages/authoring/CourseAccessibilityTab.js +1 -1
  62. package/dist/buildmyiq/pages/authoring/CourseAccessibilityTab.js.map +1 -1
  63. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
  64. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +206 -202
  65. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
  66. package/dist/buildmyiq/pages/authoring/_forms.d.ts +19 -2
  67. package/dist/buildmyiq/pages/authoring/_forms.d.ts.map +1 -1
  68. package/dist/buildmyiq/pages/authoring/_forms.js +51 -35
  69. package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
  70. package/dist/buildmyiq/pages/automations/AutomationsPage.d.ts.map +1 -1
  71. package/dist/buildmyiq/pages/automations/AutomationsPage.js +125 -122
  72. package/dist/buildmyiq/pages/automations/AutomationsPage.js.map +1 -1
  73. package/dist/buildmyiq/pages/catalog/CatalogPage.js +2 -2
  74. package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
  75. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
  76. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +7 -6
  77. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  78. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
  79. package/dist/buildmyiq/pages/certificates/CertificatesPage.js +9 -8
  80. package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
  81. package/dist/buildmyiq/pages/certificates/CredentialShareDialog.js +4 -4
  82. package/dist/buildmyiq/pages/certificates/CredentialShareDialog.js.map +1 -1
  83. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
  84. package/dist/buildmyiq/pages/cohorts/CohortsPage.js +5 -4
  85. package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
  86. package/dist/buildmyiq/pages/copilot/AdminCopilotActionCard.js +6 -6
  87. package/dist/buildmyiq/pages/copilot/AdminCopilotActionCard.js.map +1 -1
  88. package/dist/buildmyiq/pages/copilot/AdminCopilotPage.d.ts.map +1 -1
  89. package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js +5 -4
  90. package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js.map +1 -1
  91. package/dist/buildmyiq/pages/insights/InsightsPage.js +1 -1
  92. package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
  93. package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.d.ts.map +1 -1
  94. package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.js +6 -4
  95. package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.js.map +1 -1
  96. package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.d.ts.map +1 -1
  97. package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js +7 -5
  98. package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js.map +1 -1
  99. package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
  100. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +9 -9
  101. package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
  102. package/dist/buildmyiq/pages/learning/MyLearningPage.js +5 -5
  103. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  104. package/dist/buildmyiq/pages/lti/LtiLaunchPage.d.ts.map +1 -1
  105. package/dist/buildmyiq/pages/lti/LtiLaunchPage.js +2 -1
  106. package/dist/buildmyiq/pages/lti/LtiLaunchPage.js.map +1 -1
  107. package/dist/buildmyiq/pages/paths/PathsPage.js +3 -3
  108. package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
  109. package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js +4 -4
  110. package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js.map +1 -1
  111. package/dist/buildmyiq/pages/skills/SkillsPage.d.ts.map +1 -1
  112. package/dist/buildmyiq/pages/skills/SkillsPage.js +4 -3
  113. package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
  114. package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts.map +1 -1
  115. package/dist/buildmyiq/pages/submissions/BulkGradingPage.js +105 -99
  116. package/dist/buildmyiq/pages/submissions/BulkGradingPage.js.map +1 -1
  117. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
  118. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +179 -177
  119. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
  120. package/dist/buildmyiq/pages/tutorReview/TutorReviewPage.js +6 -6
  121. package/dist/buildmyiq/pages/tutorReview/TutorReviewPage.js.map +1 -1
  122. package/package.json +1 -1
@@ -47,7 +47,7 @@ var p = 600, m = {
47
47
  }),
48
48
  r(`buildmyiq.accessibility.rules.${n.rule}`, m[n.rule] ?? n.rule),
49
49
  n.line != null && /* @__PURE__ */ d("span", {
50
- className: "ml-1.5 text-xs font-normal text-text-secondary",
50
+ className: "ms-1.5 text-xs font-normal text-text-secondary",
51
51
  children: r("buildmyiq.accessibility.line", "line {{line}}", { line: n.line })
52
52
  })
53
53
  ]
@@ -1 +1 @@
1
- {"version":3,"file":"AccessibilityChecker.js","names":[],"sources":["../../../src/buildmyiq/components/AccessibilityChecker.tsx"],"sourcesContent":["/**\n * Accessibility checker UI (BOFF-4423).\n * @module buildmyiq/components/AccessibilityChecker\n *\n * - `AccessibilityChecker` — live, debounced check of a draft inside an\n * authoring form (lesson body, question prompt/options/explanation).\n * - `AccessibilityIssueList` — the issue list, shared with the course-level\n * report on the Course builder's Accessibility tab.\n *\n * Rules run server-side (`buiCheckContentAccessibility`); this only renders.\n * Status changes are announced through a polite live region so screen-reader\n * authors hear the result without losing their place in the form.\n */\n\nimport type { FC } from 'react';\nimport { AlertTriangle, CheckCircle2, ExternalLink, Loader2, XCircle } from 'lucide-react';\n\nimport {\n hasCheckableContent,\n useBuiCheckContentAccessibility,\n useDebouncedValue,\n} from '../hooks/useBuiAccessibility';\nimport { useTr } from '../shared/hooks/useTr';\nimport type {\n BuiAccessibilityIssue,\n BuiAccessibilityReport,\n BuiAccessibilityRule,\n BuiCheckContentAccessibilityInput,\n} from '../types';\nimport { cn } from '../utils/cn';\nimport { wcagUnderstandingUrl } from '../utils/wcag';\n\nconst DEBOUNCE_MS = 600;\n\n/** Short English title per rule (translatable via `buildmyiq.accessibility.rules.<RULE>`). */\nconst RULE_TITLES: Record<BuiAccessibilityRule, string> = {\n IMAGE_ALT_MISSING: 'Image without alt text',\n IMAGE_ALT_EMPTY: 'Image marked decorative',\n IMAGE_ALT_NOT_DESCRIPTIVE: 'Alt text is not descriptive',\n LINK_TEXT_EMPTY: 'Link without text',\n LINK_TEXT_NOT_DESCRIPTIVE: 'Vague link text',\n LINK_TEXT_RAW_URL: 'Raw URL as link text',\n HEADING_EMPTY: 'Empty heading',\n HEADING_LEVEL_SKIPPED: 'Skipped heading level',\n TABLE_HEADER_MISSING: 'Table without headers',\n MEDIA_CAPTIONS_MISSING: 'Video without captions',\n MEDIA_AUTOPLAY: 'Autoplaying media',\n MEDIA_TRANSCRIPT_MISSING: 'No transcript',\n IFRAME_TITLE_MISSING: 'Embed without a title',\n MOVING_CONTENT: 'Moving or blinking text',\n COLOR_STYLING: 'Inline text colour',\n SENSORY_INSTRUCTION: 'Colour or position-only instruction',\n OPTION_LABEL_EMPTY: 'Answer option without a label',\n OPTION_LABEL_DUPLICATE: 'Duplicate answer options',\n};\n\nconst IssueRow: FC<{ issue: BuiAccessibilityIssue }> = ({ issue }) => {\n const tr = useTr();\n const isError = issue.severity === 'ERROR';\n const Icon = isError ? XCircle : AlertTriangle;\n const url = wcagUnderstandingUrl(issue.wcag);\n const wcagLabel = tr('buildmyiq.accessibility.wcagLink', 'WCAG {{criterion}} (Level {{level}})', {\n criterion: issue.wcag,\n level: issue.wcagLevel,\n });\n return (\n <li className=\"flex gap-2.5 py-2.5 first:pt-0 last:pb-0\">\n <Icon\n aria-hidden=\"true\"\n className={cn(\n 'mt-0.5 h-4 w-4 shrink-0',\n isError ? 'text-status-error-text' : 'text-status-warning-text'\n )}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n <span className=\"sr-only\">\n {isError\n ? tr('buildmyiq.accessibility.severity.error', 'Error:')\n : tr('buildmyiq.accessibility.severity.warning', 'Warning:')}{' '}\n </span>\n {tr(`buildmyiq.accessibility.rules.${issue.rule}`, RULE_TITLES[issue.rule] ?? issue.rule)}\n {issue.line != null && (\n <span className=\"ml-1.5 text-xs font-normal text-text-secondary\">\n {tr('buildmyiq.accessibility.line', 'line {{line}}', { line: issue.line })}\n </span>\n )}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n {tr(`buildmyiq.accessibility.messages.${issue.rule}`, issue.message)}\n </p>\n {issue.excerpt && (\n <code className=\"mt-1 block truncate rounded bg-bg-sunken px-1.5 py-0.5 font-mono text-xs text-text-secondary\">\n {issue.excerpt}\n </code>\n )}\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"mt-1 inline-flex items-center gap-1 text-xs text-text-secondary underline underline-offset-2 hover:text-text-primary\"\n >\n {wcagLabel}\n <ExternalLink aria-hidden=\"true\" className=\"h-3 w-3\" />\n <span className=\"sr-only\">\n {tr('buildmyiq.accessibility.opensNewTab', '(opens in a new tab)')}\n </span>\n </a>\n ) : (\n <span className=\"mt-1 block text-xs text-text-secondary\">{wcagLabel}</span>\n )}\n </div>\n </li>\n );\n};\n\nexport const AccessibilityIssueList: FC<{\n issues: BuiAccessibilityIssue[];\n className?: string;\n}> = ({ issues, className }) => (\n <ul className={cn('divide-y divide-border-seam', className)}>\n {issues.map((issue, index) => (\n <IssueRow key={`${issue.rule}-${issue.line ?? 'x'}-${index}`} issue={issue} />\n ))}\n </ul>\n);\n\n/** One-line result summary, e.g. \"2 errors · 1 warning\". */\nexport const useAccessibilitySummary = (): ((\n report: Pick<BuiAccessibilityReport, 'errorCount' | 'warningCount'>\n) => string) => {\n const tr = useTr();\n return (report) => {\n if (report.errorCount === 0 && report.warningCount === 0) {\n return tr('buildmyiq.accessibility.noIssues', 'No accessibility issues found');\n }\n const parts: string[] = [];\n if (report.errorCount > 0) {\n parts.push(\n report.errorCount === 1\n ? tr('buildmyiq.accessibility.oneError', '1 error')\n : tr('buildmyiq.accessibility.errors', '{{count}} errors', { count: report.errorCount })\n );\n }\n if (report.warningCount > 0) {\n parts.push(\n report.warningCount === 1\n ? tr('buildmyiq.accessibility.oneWarning', '1 warning')\n : tr('buildmyiq.accessibility.warnings', '{{count}} warnings', {\n count: report.warningCount,\n })\n );\n }\n return parts.join(' · ');\n };\n};\n\ntype StatusTone = 'checking' | 'error' | 'warning' | 'clean';\n\nconst STATUS_ICONS = {\n checking: Loader2,\n error: XCircle,\n warning: AlertTriangle,\n clean: CheckCircle2,\n} as const;\n\nconst STATUS_TONE_CLASS: Record<StatusTone, string> = {\n checking: 'motion-safe:animate-spin',\n error: 'text-status-error-text',\n warning: 'text-status-warning-text',\n clean: 'text-status-success-text',\n};\n\nconst statusTone = (stale: boolean, report: BuiAccessibilityReport | undefined): StatusTone => {\n if (stale || !report) return 'checking';\n if (report.errorCount > 0) return 'error';\n if (report.warningCount > 0) return 'warning';\n return 'clean';\n};\n\ninterface AccessibilityCheckerProps {\n /** Draft to check; re-checked 600 ms after it stops changing. */\n input: BuiCheckContentAccessibilityInput;\n id?: string;\n className?: string;\n}\n\n/**\n * Live accessibility check for an authoring form. Advisory: it never blocks\n * saving, it tells the author what to fix and why (with the WCAG criterion).\n */\nexport const AccessibilityChecker: FC<AccessibilityCheckerProps> = ({\n input,\n id = 'accessibility-check',\n className,\n}) => {\n const tr = useTr();\n const summarise = useAccessibilitySummary();\n const debounced = useDebouncedValue(input, DEBOUNCE_MS);\n const query = useBuiCheckContentAccessibility(debounced);\n // A cleared draft is idle again: never show the previous draft's result.\n const checkable = hasCheckableContent(input);\n const report = checkable ? query.data : undefined;\n // The shown report belongs to an older draft while the author is still\n // typing (debounce pending), while a request is in flight, or while React\n // Query is showing the previous key's data as a placeholder.\n const stale =\n JSON.stringify(input) !== JSON.stringify(debounced) ||\n query.isFetching ||\n query.isPlaceholderData;\n const failed = checkable && !stale && query.isError;\n const idle = !checkable;\n const headingId = `${id}-heading`;\n\n let status: string;\n if (idle) {\n status = tr(\n 'buildmyiq.accessibility.idle',\n 'Start writing and the content will be checked for accessibility.'\n );\n } else if (failed) {\n status = tr(\n 'buildmyiq.accessibility.unavailable',\n 'Accessibility check is unavailable right now.'\n );\n } else if (stale || !report) {\n status = tr('buildmyiq.accessibility.checking', 'Checking accessibility…');\n } else {\n status = summarise(report);\n }\n // Screen readers only hear settled results, never the transient \"checking\"\n // state, so typing does not trigger two announcements per pause.\n const announcement = idle || stale || (!report && !failed) ? '' : status;\n\n const tone = statusTone(stale, report);\n const StatusIcon = STATUS_ICONS[tone];\n\n return (\n <section\n aria-labelledby={headingId}\n data-testid=\"accessibility-checker\"\n className={cn('rounded-xl border border-border-seam bg-bg-sunken/40 p-3', className)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <h3 id={headingId} className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.accessibility.title', 'Accessibility check')}\n </h3>\n <p\n aria-hidden=\"true\"\n data-testid=\"accessibility-checker-status\"\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n {!idle && !failed && (\n <StatusIcon className={cn('h-3.5 w-3.5', STATUS_TONE_CLASS[tone])} />\n )}\n {status}\n </p>\n <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </p>\n </div>\n {report && !failed && report.issues.length > 0 && (\n <AccessibilityIssueList issues={report.issues} className=\"mt-3\" />\n )}\n {report?.truncated && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.accessibility.truncated',\n 'Only the first part of this content was checked because it is very long.'\n )}\n </p>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AccessibilityChecker.js","names":[],"sources":["../../../src/buildmyiq/components/AccessibilityChecker.tsx"],"sourcesContent":["/**\n * Accessibility checker UI (BOFF-4423).\n * @module buildmyiq/components/AccessibilityChecker\n *\n * - `AccessibilityChecker` — live, debounced check of a draft inside an\n * authoring form (lesson body, question prompt/options/explanation).\n * - `AccessibilityIssueList` — the issue list, shared with the course-level\n * report on the Course builder's Accessibility tab.\n *\n * Rules run server-side (`buiCheckContentAccessibility`); this only renders.\n * Status changes are announced through a polite live region so screen-reader\n * authors hear the result without losing their place in the form.\n */\n\nimport type { FC } from 'react';\nimport { AlertTriangle, CheckCircle2, ExternalLink, Loader2, XCircle } from 'lucide-react';\n\nimport {\n hasCheckableContent,\n useBuiCheckContentAccessibility,\n useDebouncedValue,\n} from '../hooks/useBuiAccessibility';\nimport { useTr } from '../shared/hooks/useTr';\nimport type {\n BuiAccessibilityIssue,\n BuiAccessibilityReport,\n BuiAccessibilityRule,\n BuiCheckContentAccessibilityInput,\n} from '../types';\nimport { cn } from '../utils/cn';\nimport { wcagUnderstandingUrl } from '../utils/wcag';\n\nconst DEBOUNCE_MS = 600;\n\n/** Short English title per rule (translatable via `buildmyiq.accessibility.rules.<RULE>`). */\nconst RULE_TITLES: Record<BuiAccessibilityRule, string> = {\n IMAGE_ALT_MISSING: 'Image without alt text',\n IMAGE_ALT_EMPTY: 'Image marked decorative',\n IMAGE_ALT_NOT_DESCRIPTIVE: 'Alt text is not descriptive',\n LINK_TEXT_EMPTY: 'Link without text',\n LINK_TEXT_NOT_DESCRIPTIVE: 'Vague link text',\n LINK_TEXT_RAW_URL: 'Raw URL as link text',\n HEADING_EMPTY: 'Empty heading',\n HEADING_LEVEL_SKIPPED: 'Skipped heading level',\n TABLE_HEADER_MISSING: 'Table without headers',\n MEDIA_CAPTIONS_MISSING: 'Video without captions',\n MEDIA_AUTOPLAY: 'Autoplaying media',\n MEDIA_TRANSCRIPT_MISSING: 'No transcript',\n IFRAME_TITLE_MISSING: 'Embed without a title',\n MOVING_CONTENT: 'Moving or blinking text',\n COLOR_STYLING: 'Inline text colour',\n SENSORY_INSTRUCTION: 'Colour or position-only instruction',\n OPTION_LABEL_EMPTY: 'Answer option without a label',\n OPTION_LABEL_DUPLICATE: 'Duplicate answer options',\n};\n\nconst IssueRow: FC<{ issue: BuiAccessibilityIssue }> = ({ issue }) => {\n const tr = useTr();\n const isError = issue.severity === 'ERROR';\n const Icon = isError ? XCircle : AlertTriangle;\n const url = wcagUnderstandingUrl(issue.wcag);\n const wcagLabel = tr('buildmyiq.accessibility.wcagLink', 'WCAG {{criterion}} (Level {{level}})', {\n criterion: issue.wcag,\n level: issue.wcagLevel,\n });\n return (\n <li className=\"flex gap-2.5 py-2.5 first:pt-0 last:pb-0\">\n <Icon\n aria-hidden=\"true\"\n className={cn(\n 'mt-0.5 h-4 w-4 shrink-0',\n isError ? 'text-status-error-text' : 'text-status-warning-text'\n )}\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n <span className=\"sr-only\">\n {isError\n ? tr('buildmyiq.accessibility.severity.error', 'Error:')\n : tr('buildmyiq.accessibility.severity.warning', 'Warning:')}{' '}\n </span>\n {tr(`buildmyiq.accessibility.rules.${issue.rule}`, RULE_TITLES[issue.rule] ?? issue.rule)}\n {issue.line != null && (\n <span className=\"ms-1.5 text-xs font-normal text-text-secondary\">\n {tr('buildmyiq.accessibility.line', 'line {{line}}', { line: issue.line })}\n </span>\n )}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n {tr(`buildmyiq.accessibility.messages.${issue.rule}`, issue.message)}\n </p>\n {issue.excerpt && (\n <code className=\"mt-1 block truncate rounded bg-bg-sunken px-1.5 py-0.5 font-mono text-xs text-text-secondary\">\n {issue.excerpt}\n </code>\n )}\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className=\"mt-1 inline-flex items-center gap-1 text-xs text-text-secondary underline underline-offset-2 hover:text-text-primary\"\n >\n {wcagLabel}\n <ExternalLink aria-hidden=\"true\" className=\"h-3 w-3\" />\n <span className=\"sr-only\">\n {tr('buildmyiq.accessibility.opensNewTab', '(opens in a new tab)')}\n </span>\n </a>\n ) : (\n <span className=\"mt-1 block text-xs text-text-secondary\">{wcagLabel}</span>\n )}\n </div>\n </li>\n );\n};\n\nexport const AccessibilityIssueList: FC<{\n issues: BuiAccessibilityIssue[];\n className?: string;\n}> = ({ issues, className }) => (\n <ul className={cn('divide-y divide-border-seam', className)}>\n {issues.map((issue, index) => (\n <IssueRow key={`${issue.rule}-${issue.line ?? 'x'}-${index}`} issue={issue} />\n ))}\n </ul>\n);\n\n/** One-line result summary, e.g. \"2 errors · 1 warning\". */\nexport const useAccessibilitySummary = (): ((\n report: Pick<BuiAccessibilityReport, 'errorCount' | 'warningCount'>\n) => string) => {\n const tr = useTr();\n return (report) => {\n if (report.errorCount === 0 && report.warningCount === 0) {\n return tr('buildmyiq.accessibility.noIssues', 'No accessibility issues found');\n }\n const parts: string[] = [];\n if (report.errorCount > 0) {\n parts.push(\n report.errorCount === 1\n ? tr('buildmyiq.accessibility.oneError', '1 error')\n : tr('buildmyiq.accessibility.errors', '{{count}} errors', { count: report.errorCount })\n );\n }\n if (report.warningCount > 0) {\n parts.push(\n report.warningCount === 1\n ? tr('buildmyiq.accessibility.oneWarning', '1 warning')\n : tr('buildmyiq.accessibility.warnings', '{{count}} warnings', {\n count: report.warningCount,\n })\n );\n }\n return parts.join(' · ');\n };\n};\n\ntype StatusTone = 'checking' | 'error' | 'warning' | 'clean';\n\nconst STATUS_ICONS = {\n checking: Loader2,\n error: XCircle,\n warning: AlertTriangle,\n clean: CheckCircle2,\n} as const;\n\nconst STATUS_TONE_CLASS: Record<StatusTone, string> = {\n checking: 'motion-safe:animate-spin',\n error: 'text-status-error-text',\n warning: 'text-status-warning-text',\n clean: 'text-status-success-text',\n};\n\nconst statusTone = (stale: boolean, report: BuiAccessibilityReport | undefined): StatusTone => {\n if (stale || !report) return 'checking';\n if (report.errorCount > 0) return 'error';\n if (report.warningCount > 0) return 'warning';\n return 'clean';\n};\n\ninterface AccessibilityCheckerProps {\n /** Draft to check; re-checked 600 ms after it stops changing. */\n input: BuiCheckContentAccessibilityInput;\n id?: string;\n className?: string;\n}\n\n/**\n * Live accessibility check for an authoring form. Advisory: it never blocks\n * saving, it tells the author what to fix and why (with the WCAG criterion).\n */\nexport const AccessibilityChecker: FC<AccessibilityCheckerProps> = ({\n input,\n id = 'accessibility-check',\n className,\n}) => {\n const tr = useTr();\n const summarise = useAccessibilitySummary();\n const debounced = useDebouncedValue(input, DEBOUNCE_MS);\n const query = useBuiCheckContentAccessibility(debounced);\n // A cleared draft is idle again: never show the previous draft's result.\n const checkable = hasCheckableContent(input);\n const report = checkable ? query.data : undefined;\n // The shown report belongs to an older draft while the author is still\n // typing (debounce pending), while a request is in flight, or while React\n // Query is showing the previous key's data as a placeholder.\n const stale =\n JSON.stringify(input) !== JSON.stringify(debounced) ||\n query.isFetching ||\n query.isPlaceholderData;\n const failed = checkable && !stale && query.isError;\n const idle = !checkable;\n const headingId = `${id}-heading`;\n\n let status: string;\n if (idle) {\n status = tr(\n 'buildmyiq.accessibility.idle',\n 'Start writing and the content will be checked for accessibility.'\n );\n } else if (failed) {\n status = tr(\n 'buildmyiq.accessibility.unavailable',\n 'Accessibility check is unavailable right now.'\n );\n } else if (stale || !report) {\n status = tr('buildmyiq.accessibility.checking', 'Checking accessibility…');\n } else {\n status = summarise(report);\n }\n // Screen readers only hear settled results, never the transient \"checking\"\n // state, so typing does not trigger two announcements per pause.\n const announcement = idle || stale || (!report && !failed) ? '' : status;\n\n const tone = statusTone(stale, report);\n const StatusIcon = STATUS_ICONS[tone];\n\n return (\n <section\n aria-labelledby={headingId}\n data-testid=\"accessibility-checker\"\n className={cn('rounded-xl border border-border-seam bg-bg-sunken/40 p-3', className)}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <h3 id={headingId} className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.accessibility.title', 'Accessibility check')}\n </h3>\n <p\n aria-hidden=\"true\"\n data-testid=\"accessibility-checker-status\"\n className=\"inline-flex items-center gap-1.5 text-xs text-text-secondary\"\n >\n {!idle && !failed && (\n <StatusIcon className={cn('h-3.5 w-3.5', STATUS_TONE_CLASS[tone])} />\n )}\n {status}\n </p>\n <p role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {announcement}\n </p>\n </div>\n {report && !failed && report.issues.length > 0 && (\n <AccessibilityIssueList issues={report.issues} className=\"mt-3\" />\n )}\n {report?.truncated && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.accessibility.truncated',\n 'Only the first part of this content was checked because it is very long.'\n )}\n </p>\n )}\n </section>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BuiDiscussion.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/BuiDiscussion.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,QAAQ,EACR,KAAoB,EACpB,WAAkE,GACnE,EAAE,kBAAkB,+BAoGpB"}
1
+ {"version":3,"file":"BuiDiscussion.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/BuiDiscussion.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAaD,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,QAAQ,EACR,KAAoB,EACpB,WAAkE,GACnE,EAAE,kBAAkB,+BAyGpB"}
@@ -47,17 +47,19 @@ function _({ entityType: _, entityId: v, title: y = "Discussion", description: b
47
47
  value: D,
48
48
  onChange: (e) => O(e.target.value),
49
49
  placeholder: "Add a comment for your team…",
50
+ "aria-label": "Comment",
50
51
  rows: 2
51
52
  }), /* @__PURE__ */ f("div", {
52
53
  className: "mt-2 flex items-center justify-end gap-2",
53
54
  children: [k && /* @__PURE__ */ d("span", {
54
- className: "mr-auto text-xs text-status-error-text",
55
+ role: "alert",
56
+ className: "me-auto text-xs text-status-error-text",
55
57
  children: k
56
58
  }), /* @__PURE__ */ f(p, {
57
59
  size: "sm",
58
60
  onClick: N,
59
61
  disabled: !D.trim() || E.isPending,
60
- children: [/* @__PURE__ */ d(u, { className: "mr-1.5 h-4 w-4" }), E.isPending ? "Posting…" : "Comment"]
62
+ children: [/* @__PURE__ */ d(u, { className: "me-1.5 h-4 w-4" }), E.isPending ? "Posting…" : "Comment"]
61
63
  })]
62
64
  })]
63
65
  })]
@@ -1 +1 @@
1
- {"version":3,"file":"BuiDiscussion.js","names":[],"sources":["../../../src/buildmyiq/components/BuiDiscussion.tsx"],"sourcesContent":["/**\n * BuiDiscussion — a comment thread on any Buildmyiq entity (Integration B).\n * @module buildmyiq/components/BuiDiscussion\n *\n * Reuses the platform EntityComment API (`getEntityComments` /\n * `createEntityComment`) keyed by `(entityType, entityId)` — no buildmyiq-svc change.\n * Renders root comments (author, body, time) + a composer. Robust to empty\n * state and errors.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { Section, LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useBuildmyiq } from '../context';\nimport { useBuiComments, useEcoCreateComment, type BuiComment } from '../hooks';\nimport { formatDateTime } from '../utils/formatters';\n\ninterface BuiDiscussionProps {\n entityType: string;\n entityId: string;\n title?: string;\n description?: string;\n}\n\nfunction displayName(user: ReturnType<typeof useBuildmyiq>['currentUser']): string {\n if (!user) return 'You';\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n return user.name || full || user.username || user.email || 'You';\n}\n\nfunction initialFor(label: string): string {\n const trimmed = label.trim();\n return trimmed ? trimmed[0]!.toUpperCase() : '?';\n}\n\nexport function BuiDiscussion({\n entityType,\n entityId,\n title = 'Discussion',\n description = 'Coordinate review and verification with your team.',\n}: BuiDiscussionProps) {\n const { currentUser } = useBuildmyiq();\n const { data, isLoading, isError, refetch } = useBuiComments(entityType, entityId);\n const createComment = useEcoCreateComment(entityType, entityId);\n\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const selfName = displayName(currentUser);\n\n const authorLabel = (comment: BuiComment): string =>\n currentUser?.id && comment.authorId === currentUser.id ? selfName : 'Workspace member';\n\n const handleSubmit = async () => {\n const text = draft.trim();\n if (!text) return;\n setError(null);\n try {\n await createComment.mutateAsync(text);\n setDraft('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to post comment.');\n }\n };\n\n const comments = data?.items ?? [];\n\n return (\n <Section title={title} description={description}>\n <div className=\"rounded-xl border border-border-seam bg-bg-surface/40 p-4\">\n {/* Composer */}\n <div className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg\">\n {initialFor(selfName)}\n </div>\n <div className=\"flex-1\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Add a comment for your team…\"\n rows={2}\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n {error && <span className=\"mr-auto text-xs text-status-error-text\">{error}</span>}\n <Button\n size=\"sm\"\n onClick={handleSubmit}\n disabled={!draft.trim() || createComment.isPending}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {createComment.isPending ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Thread */}\n <div className=\"mt-4 border-t border-border-seam pt-4\">\n {isLoading ? (\n <LoadingView label=\"Loading discussion…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load the discussion.\" onRetry={() => refetch()} />\n ) : comments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MessageSquare className=\"h-9 w-9\" />}\n title=\"No comments yet\"\n description=\"Start the discussion by adding the first comment.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {comments.map((comment) => {\n const label = authorLabel(comment);\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <li key={comment.id} className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary\">\n {initialFor(label)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{label}</span>\n <span className=\"text-xs text-text-tertiary\">\n {formatDateTime(comment.createdAt)}\n {comment.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {body}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Section>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BuiDiscussion.js","names":[],"sources":["../../../src/buildmyiq/components/BuiDiscussion.tsx"],"sourcesContent":["/**\n * BuiDiscussion — a comment thread on any Buildmyiq entity (Integration B).\n * @module buildmyiq/components/BuiDiscussion\n *\n * Reuses the platform EntityComment API (`getEntityComments` /\n * `createEntityComment`) keyed by `(entityType, entityId)` — no buildmyiq-svc change.\n * Renders root comments (author, body, time) + a composer. Robust to empty\n * state and errors.\n */\n\nimport { useState } from 'react';\nimport { MessageSquare, Send } from 'lucide-react';\nimport { Button, Textarea } from '@burdenoff/fe-libs/ui';\nimport { Section, LoadingView, ErrorView } from './StateViews';\nimport { EmptyState } from './EmptyState';\nimport { useBuildmyiq } from '../context';\nimport { useBuiComments, useEcoCreateComment, type BuiComment } from '../hooks';\nimport { formatDateTime } from '../utils/formatters';\n\ninterface BuiDiscussionProps {\n entityType: string;\n entityId: string;\n title?: string;\n description?: string;\n}\n\nfunction displayName(user: ReturnType<typeof useBuildmyiq>['currentUser']): string {\n if (!user) return 'You';\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n return user.name || full || user.username || user.email || 'You';\n}\n\nfunction initialFor(label: string): string {\n const trimmed = label.trim();\n return trimmed ? trimmed[0]!.toUpperCase() : '?';\n}\n\nexport function BuiDiscussion({\n entityType,\n entityId,\n title = 'Discussion',\n description = 'Coordinate review and verification with your team.',\n}: BuiDiscussionProps) {\n const { currentUser } = useBuildmyiq();\n const { data, isLoading, isError, refetch } = useBuiComments(entityType, entityId);\n const createComment = useEcoCreateComment(entityType, entityId);\n\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const selfName = displayName(currentUser);\n\n const authorLabel = (comment: BuiComment): string =>\n currentUser?.id && comment.authorId === currentUser.id ? selfName : 'Workspace member';\n\n const handleSubmit = async () => {\n const text = draft.trim();\n if (!text) return;\n setError(null);\n try {\n await createComment.mutateAsync(text);\n setDraft('');\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to post comment.');\n }\n };\n\n const comments = data?.items ?? [];\n\n return (\n <Section title={title} description={description}>\n <div className=\"rounded-xl border border-border-seam bg-bg-surface/40 p-4\">\n {/* Composer */}\n <div className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/15 text-sm font-semibold text-action-primary-bg\">\n {initialFor(selfName)}\n </div>\n <div className=\"flex-1\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n placeholder=\"Add a comment for your team…\"\n aria-label=\"Comment\"\n rows={2}\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n {error && (\n <span role=\"alert\" className=\"me-auto text-xs text-status-error-text\">\n {error}\n </span>\n )}\n <Button\n size=\"sm\"\n onClick={handleSubmit}\n disabled={!draft.trim() || createComment.isPending}\n >\n <Send className=\"me-1.5 h-4 w-4\" />\n {createComment.isPending ? 'Posting…' : 'Comment'}\n </Button>\n </div>\n </div>\n </div>\n\n {/* Thread */}\n <div className=\"mt-4 border-t border-border-seam pt-4\">\n {isLoading ? (\n <LoadingView label=\"Loading discussion…\" />\n ) : isError ? (\n <ErrorView message=\"Couldn't load the discussion.\" onRetry={() => refetch()} />\n ) : comments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<MessageSquare className=\"h-9 w-9\" />}\n title=\"No comments yet\"\n description=\"Start the discussion by adding the first comment.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {comments.map((comment) => {\n const label = authorLabel(comment);\n const body = comment.content.text ?? comment.content.markdown ?? '';\n return (\n <li key={comment.id} className=\"flex gap-3\">\n <div className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary\">\n {initialFor(label)}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-baseline gap-x-2\">\n <span className=\"text-sm font-semibold text-text-primary\">{label}</span>\n <span className=\"text-xs text-text-tertiary\">\n {formatDateTime(comment.createdAt)}\n {comment.edited ? ' · edited' : ''}\n </span>\n </div>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {body}\n </p>\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Section>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BuiTags.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,OAAO,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAc,EAAE,EAAE,YAAY,+BAuL7E"}
1
+ {"version":3,"file":"BuiTags.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH,UAAU,YAAY;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,OAAO,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAc,EAAE,EAAE,YAAY,+BA2L7E"}
@@ -89,18 +89,23 @@ function b({ entityType: b, entityId: x, label: S = "Tags" }) {
89
89
  value: k,
90
90
  onChange: (e) => A(e.target.value),
91
91
  placeholder: "Search or create a tag…",
92
+ "aria-label": "Search or create a tag",
92
93
  className: "h-8"
93
94
  }), /* @__PURE__ */ f("div", {
94
95
  className: "mt-2 max-h-56 overflow-y-auto",
95
96
  children: F.isFetching && R.length > 0 ? /* @__PURE__ */ p("div", {
97
+ role: "status",
96
98
  className: "flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary",
97
- children: [/* @__PURE__ */ f(y, { size: "sm" }), " Searching…"]
99
+ children: [/* @__PURE__ */ f(y, {
100
+ size: "sm",
101
+ "aria-hidden": "true"
102
+ }), " Searching…"]
98
103
  }) : /* @__PURE__ */ p(d, { children: [
99
104
  z.map((e) => /* @__PURE__ */ p("button", {
100
105
  type: "button",
101
106
  onClick: () => W(e.id),
102
107
  disabled: H,
103
- className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60",
108
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60",
104
109
  children: [e.color ? /* @__PURE__ */ f("span", {
105
110
  className: "h-2 w-2 shrink-0 rounded-full",
106
111
  style: { backgroundColor: e.color },
@@ -114,7 +119,7 @@ function b({ entityType: b, entityId: x, label: S = "Tags" }) {
114
119
  type: "button",
115
120
  onClick: G,
116
121
  disabled: H,
117
- className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60",
122
+ className: "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60",
118
123
  children: [/* @__PURE__ */ f(c, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ p("span", {
119
124
  className: "truncate",
120
125
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"BuiTags.js","names":[],"sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"sourcesContent":["/**\n * BuiTags — a lightweight tag control for Buildmyiq entities (Integration C).\n * @module buildmyiq/components/BuiTags\n *\n * The buildmyiq svc exposes no tags field, so this drives the platform tag store\n * directly: `getEntityTags` (display), `searchTags` (+ `createTag`) to pick or\n * create, `assignTag`/`removeTagAssignment` to link — all via the workspace\n * gateway. No buildmyiq-svc change.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n useEcoEntityTags,\n useEcoTagSearch,\n useEcoAssignTag,\n useEcoRemoveTagAssignment,\n useEcoCreateAndAssignTag,\n} from '../hooks';\n\ninterface BuiTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function BuiTags({ entityType, entityId, label = 'Tags' }: BuiTagsProps) {\n const tags = useEcoEntityTags(entityType, entityId);\n const assign = useEcoAssignTag(entityType, entityId);\n const remove = useEcoRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useEcoCreateAndAssignTag(entityType, entityId);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [debounced, setDebounced] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => setDebounced(query), 250);\n return () => clearTimeout(timer);\n }, [query]);\n\n const search = useEcoTagSearch(debounced);\n\n const assignments = tags.data ?? [];\n const assignedIds = new Set(assignments.map((a) => a.tag.id));\n const q = debounced.trim();\n const searchResults = (search.data ?? []).filter((t) => !assignedIds.has(t.id));\n const hasExact =\n q.length > 0 &&\n [...assignments.map((a) => a.tag.label), ...(search.data ?? []).map((t) => t.label)].some(\n (l) => l.toLowerCase() === q.toLowerCase()\n );\n const showCreate = q.length > 0 && !hasExact;\n const busy = assign.isPending || createAndAssign.isPending;\n\n const resetInput = () => {\n setQuery('');\n setDebounced('');\n setError(null);\n };\n\n const handleAssign = async (tagId: string) => {\n setError(null);\n try {\n await assign.mutateAsync(tagId);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add tag.');\n }\n };\n\n const handleCreate = async () => {\n if (!q) return;\n setError(null);\n try {\n await createAndAssign.mutateAsync(q);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create tag.');\n }\n };\n\n const handleRemove = async (assignmentId: string) => {\n setError(null);\n try {\n await remove.mutateAsync(assignmentId);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove tag.');\n }\n };\n\n return (\n <div>\n <p className=\"mb-1.5 text-xs text-text-tertiary\">{label}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {tags.isLoading ? (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading tags…\n </span>\n ) : (\n assignments.map((assignment) => (\n <span\n key={assignment.id}\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary\"\n >\n {assignment.tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: assignment.tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3 w-3 text-text-tertiary\" />\n )}\n <span className=\"max-w-[10rem] truncate\">{assignment.tag.label}</span>\n <button\n type=\"button\"\n onClick={() => handleRemove(assignment.id)}\n disabled={remove.isPending}\n aria-label={`Remove tag ${assignment.tag.label}`}\n className=\"rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))\n )}\n\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetInput();\n }}\n >\n <PopoverTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 gap-1 px-2 text-xs\">\n <Plus className=\"h-3.5 w-3.5\" /> Add tag\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search or create a tag…\"\n className=\"h-8\"\n />\n <div className=\"mt-2 max-h-56 overflow-y-auto\">\n {search.isFetching && q.length > 0 ? (\n <div className=\"flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary\">\n <Spinner size=\"sm\" /> Searching…\n </div>\n ) : (\n <>\n {searchResults.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAssign(tag.id)}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60\"\n >\n {tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n <span className=\"truncate\">{tag.label}</span>\n </button>\n ))}\n\n {showCreate && (\n <button\n type=\"button\"\n onClick={handleCreate}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Plus className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate\">Create “{q}”</span>\n </button>\n )}\n\n {!showCreate && searchResults.length === 0 && q.length > 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">Already added.</p>\n )}\n {q.length === 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">\n Type to search existing tags or create a new one.\n </p>\n )}\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n\n {error && <p className=\"mt-1.5 text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BuiTags.js","names":[],"sources":["../../../src/buildmyiq/components/BuiTags.tsx"],"sourcesContent":["/**\n * BuiTags — a lightweight tag control for Buildmyiq entities (Integration C).\n * @module buildmyiq/components/BuiTags\n *\n * The buildmyiq svc exposes no tags field, so this drives the platform tag store\n * directly: `getEntityTags` (display), `searchTags` (+ `createTag`) to pick or\n * create, `assignTag`/`removeTagAssignment` to link — all via the workspace\n * gateway. No buildmyiq-svc change.\n */\n\nimport { useEffect, useState } from 'react';\nimport { Loader2, Plus, Tag as TagIcon, X } from 'lucide-react';\nimport {\n Button,\n Input,\n Popover,\n PopoverContent,\n PopoverTrigger,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n useEcoEntityTags,\n useEcoTagSearch,\n useEcoAssignTag,\n useEcoRemoveTagAssignment,\n useEcoCreateAndAssignTag,\n} from '../hooks';\n\ninterface BuiTagsProps {\n entityType: string;\n entityId: string;\n label?: string;\n}\n\nexport function BuiTags({ entityType, entityId, label = 'Tags' }: BuiTagsProps) {\n const tags = useEcoEntityTags(entityType, entityId);\n const assign = useEcoAssignTag(entityType, entityId);\n const remove = useEcoRemoveTagAssignment(entityType, entityId);\n const createAndAssign = useEcoCreateAndAssignTag(entityType, entityId);\n\n const [open, setOpen] = useState(false);\n const [query, setQuery] = useState('');\n const [debounced, setDebounced] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const timer = setTimeout(() => setDebounced(query), 250);\n return () => clearTimeout(timer);\n }, [query]);\n\n const search = useEcoTagSearch(debounced);\n\n const assignments = tags.data ?? [];\n const assignedIds = new Set(assignments.map((a) => a.tag.id));\n const q = debounced.trim();\n const searchResults = (search.data ?? []).filter((t) => !assignedIds.has(t.id));\n const hasExact =\n q.length > 0 &&\n [...assignments.map((a) => a.tag.label), ...(search.data ?? []).map((t) => t.label)].some(\n (l) => l.toLowerCase() === q.toLowerCase()\n );\n const showCreate = q.length > 0 && !hasExact;\n const busy = assign.isPending || createAndAssign.isPending;\n\n const resetInput = () => {\n setQuery('');\n setDebounced('');\n setError(null);\n };\n\n const handleAssign = async (tagId: string) => {\n setError(null);\n try {\n await assign.mutateAsync(tagId);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to add tag.');\n }\n };\n\n const handleCreate = async () => {\n if (!q) return;\n setError(null);\n try {\n await createAndAssign.mutateAsync(q);\n resetInput();\n setOpen(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to create tag.');\n }\n };\n\n const handleRemove = async (assignmentId: string) => {\n setError(null);\n try {\n await remove.mutateAsync(assignmentId);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove tag.');\n }\n };\n\n return (\n <div>\n <p className=\"mb-1.5 text-xs text-text-tertiary\">{label}</p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {tags.isLoading ? (\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-tertiary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" /> Loading tags…\n </span>\n ) : (\n assignments.map((assignment) => (\n <span\n key={assignment.id}\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border-seam bg-bg-sunken px-2.5 py-1 text-xs text-text-secondary\"\n >\n {assignment.tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: assignment.tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3 w-3 text-text-tertiary\" />\n )}\n <span className=\"max-w-[10rem] truncate\">{assignment.tag.label}</span>\n <button\n type=\"button\"\n onClick={() => handleRemove(assignment.id)}\n disabled={remove.isPending}\n aria-label={`Remove tag ${assignment.tag.label}`}\n className=\"rounded-full p-0.5 text-text-tertiary hover:bg-bg-canvas hover:text-text-primary\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))\n )}\n\n <Popover\n open={open}\n onOpenChange={(next) => {\n setOpen(next);\n if (!next) resetInput();\n }}\n >\n <PopoverTrigger asChild>\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 gap-1 px-2 text-xs\">\n <Plus className=\"h-3.5 w-3.5\" /> Add tag\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-64 p-2\">\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search or create a tag…\"\n aria-label=\"Search or create a tag\"\n className=\"h-8\"\n />\n <div className=\"mt-2 max-h-56 overflow-y-auto\">\n {search.isFetching && q.length > 0 ? (\n <div\n role=\"status\"\n className=\"flex items-center gap-2 px-1 py-2 text-sm text-text-tertiary\"\n >\n <Spinner size=\"sm\" aria-hidden=\"true\" /> Searching…\n </div>\n ) : (\n <>\n {searchResults.map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n onClick={() => handleAssign(tag.id)}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm text-text-secondary hover:bg-bg-sunken disabled:opacity-60\"\n >\n {tag.color ? (\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: tag.color }}\n aria-hidden\n />\n ) : (\n <TagIcon className=\"h-3.5 w-3.5 text-text-tertiary\" />\n )}\n <span className=\"truncate\">{tag.label}</span>\n </button>\n ))}\n\n {showCreate && (\n <button\n type=\"button\"\n onClick={handleCreate}\n disabled={busy}\n className=\"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm text-action-primary-bg hover:bg-bg-sunken disabled:opacity-60\"\n >\n <Plus className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate\">Create “{q}”</span>\n </button>\n )}\n\n {!showCreate && searchResults.length === 0 && q.length > 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">Already added.</p>\n )}\n {q.length === 0 && (\n <p className=\"px-2 py-2 text-sm text-text-tertiary\">\n Type to search existing tags or create a new one.\n </p>\n )}\n </>\n )}\n </div>\n </PopoverContent>\n </Popover>\n </div>\n\n {error && <p className=\"mt-1.5 text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -5,8 +5,8 @@ import { jsx as o, jsxs as s } from "react/jsx-runtime";
5
5
  import { GlassCard as c } from "@burdenoff/fe-libs/ui";
6
6
  //#region src/buildmyiq/components/DataTable.tsx
7
7
  var l = {
8
- left: "text-left",
9
- right: "text-right",
8
+ left: "text-start",
9
+ right: "text-end",
10
10
  center: "text-center"
11
11
  };
12
12
  function u({ columns: u, rows: d, rowKey: f, onRowClick: p, className: m, defaultSort: h }) {
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n}\n\nconst alignClass = {\n left: 'text-left',\n right: 'text-right',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n}: DataTableProps<T>) {\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n onClick={sortable ? () => toggleSort(col.key) : undefined}\n aria-sort={\n active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : undefined\n }\n className={cn(\n 'px-4 py-3 font-medium text-text-secondary whitespace-nowrap',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\n >\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n className={cn(\n 'group border-b border-border-seam/60 last:border-0',\n onRowClick && 'cursor-pointer transition-colors hover:bg-bg-sunken/60'\n )}\n >\n {columns.map((col) => (\n <td\n key={col.key}\n className={cn(\n 'px-4 py-3 text-text-primary align-middle',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n col.className\n )}\n >\n {col.render(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;AAqCA,IAAM,IAAa;CACjB,MAAM;CACN,OAAO;CACP,QAAQ;AACV;AAEA,SAAgB,EAAa,EAC3B,YACA,SACA,WACA,eACA,cACA,kBACoB;CACpB,IAAM,CAAC,GAAM,KAAW,EACtB,KAAe,IACjB,GAEM,IAAa,QAAc;EAC/B,IAAI,CAAC,GAAM,OAAO;EAClB,IAAM,IAAM,EAAQ,MAAM,MAAM,EAAE,QAAQ,EAAK,GAAG;EAClD,IAAI,CAAC,GAAK,WAAW,OAAO;EAC5B,IAAM,IAAM,EAAK,QAAQ,QAAQ,IAAI;EACrC,OAAO,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM;GAC9B,IAAM,IAAK,EAAI,UAAW,CAAC,GACrB,IAAK,EAAI,UAAW,CAAC;GAG3B,OAFI,IAAK,IAAW,KAAK,IACrB,IAAK,IAAW,IAAI,IACjB;EACT,CAAC;CACH,GAAG;EAAC;EAAM;EAAM;CAAO,CAAC,GAElB,KAAc,MAAgB;EAClC,GAAS,MACP,GAAM,QAAQ,IAAM;GAAE;GAAK,KAAK,EAAK,QAAQ,QAAQ,SAAS;EAAM,IAAI;GAAE;GAAK,KAAK;EAAM,CAC5F;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,mBAAmB,CAAS;YACrE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MAAQ;MACpB,IAAM,IAAW,EAAQ,EAAI,WACvB,IAAS,GAAM,QAAQ,EAAI;MACjC,OACE,kBAAC,MAAD;OAEE,SAAS,UAAiB,EAAW,EAAI,GAAG,IAAI,KAAA;OAChD,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,KAAY,oDACd;iBAEA,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,kBAC3B;kBAJF,CAMG,EAAI,QACJ,MACE,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,iDAAkD,CAAA,IAEvE,kBAAC,GAAD,EAAa,WAAU,iDAAkD,CAAA,IAG3E,kBAAC,GAAD,EAAgB,WAAU,yBAA0B,CAAA,EAEpD;;MACJ,GA9BG,EAAI,GA8BP;KAER,CAAC;IACC,CAAA,EACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,MAAD;KAEE,SAAS,UAAmB,EAAW,CAAG,IAAI,KAAA;KAC9C,WAAW,EACT,sDACA,KAAc,wDAChB;eAEC,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,4CACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,EAAI,SACN;gBAEC,EAAI,OAAO,CAAG;KACb,GATG,EAAI,GASP,CACL;IACC,GApBG,EAAO,CAAG,CAoBb,CACL,EACI,CAAA,CACF;;EACJ,CAAA;CACI,CAAA;AAEf"}
1
+ {"version":3,"file":"DataTable.js","names":[],"sources":["../../../src/buildmyiq/components/DataTable.tsx"],"sourcesContent":["/**\n * DataTable — a lightweight, semantic-token table for list pages.\n * @module buildmyiq/components\n *\n * Deliberately minimal (no external table lib). Renders a responsive,\n * horizontally-scrollable table wrapped in a GlassCard. Columns declare a\n * header, a render fn, optional alignment, and optionally a `sortValue` to make\n * the column click-sortable.\n */\n\nimport { useMemo, useState, type ReactNode } from 'react';\nimport { ChevronUp, ChevronDown, ChevronsUpDown } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\nexport interface Column<T> {\n key: string;\n header: ReactNode;\n render: (row: T) => ReactNode;\n align?: 'left' | 'right' | 'center';\n className?: string;\n /** Hide below the `md` breakpoint for a mobile-friendly layout. */\n hideOnMobile?: boolean;\n /** Provide to make this column click-sortable (returns a comparable value). */\n sortValue?: (row: T) => string | number;\n}\n\ninterface DataTableProps<T> {\n columns: Column<T>[];\n rows: T[];\n rowKey: (row: T) => string;\n onRowClick?: (row: T) => void;\n className?: string;\n /** Default sort: a column key + direction. */\n defaultSort?: { key: string; dir: 'asc' | 'desc' };\n}\n\nconst alignClass = {\n left: 'text-start',\n right: 'text-end',\n center: 'text-center',\n} as const;\n\nexport function DataTable<T>({\n columns,\n rows,\n rowKey,\n onRowClick,\n className,\n defaultSort,\n}: DataTableProps<T>) {\n const [sort, setSort] = useState<{ key: string; dir: 'asc' | 'desc' } | null>(\n defaultSort ?? null\n );\n\n const sortedRows = useMemo(() => {\n if (!sort) return rows;\n const col = columns.find((c) => c.key === sort.key);\n if (!col?.sortValue) return rows;\n const dir = sort.dir === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const va = col.sortValue!(a);\n const vb = col.sortValue!(b);\n if (va < vb) return -1 * dir;\n if (va > vb) return 1 * dir;\n return 0;\n });\n }, [rows, sort, columns]);\n\n const toggleSort = (key: string) => {\n setSort((prev) =>\n prev?.key === key ? { key, dir: prev.dir === 'asc' ? 'desc' : 'asc' } : { key, dir: 'asc' }\n );\n };\n\n return (\n <GlassCard treatment=\"plain\" className={cn('overflow-hidden', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-seam\">\n {columns.map((col) => {\n const sortable = Boolean(col.sortValue);\n const active = sort?.key === col.key;\n return (\n <th\n key={col.key}\n onClick={sortable ? () => toggleSort(col.key) : undefined}\n aria-sort={\n active ? (sort?.dir === 'asc' ? 'ascending' : 'descending') : undefined\n }\n className={cn(\n 'px-4 py-3 font-medium text-text-secondary whitespace-nowrap',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n sortable && 'cursor-pointer select-none hover:text-text-primary'\n )}\n >\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n col.align === 'right' && 'flex-row-reverse'\n )}\n >\n {col.header}\n {sortable &&\n (active ? (\n sort?.dir === 'asc' ? (\n <ChevronUp className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n ) : (\n <ChevronDown className=\"h-3.5 w-3.5 text-[var(--color-accent-primary)]\" />\n )\n ) : (\n <ChevronsUpDown className=\"h-3.5 w-3.5 opacity-40\" />\n ))}\n </span>\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.map((row) => (\n <tr\n key={rowKey(row)}\n onClick={onRowClick ? () => onRowClick(row) : undefined}\n className={cn(\n 'group border-b border-border-seam/60 last:border-0',\n onRowClick && 'cursor-pointer transition-colors hover:bg-bg-sunken/60'\n )}\n >\n {columns.map((col) => (\n <td\n key={col.key}\n className={cn(\n 'px-4 py-3 text-text-primary align-middle',\n alignClass[col.align ?? 'left'],\n col.hideOnMobile && 'hidden md:table-cell',\n col.className\n )}\n >\n {col.render(row)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;AAqCA,IAAM,IAAa;CACjB,MAAM;CACN,OAAO;CACP,QAAQ;AACV;AAEA,SAAgB,EAAa,EAC3B,YACA,SACA,WACA,eACA,cACA,kBACoB;CACpB,IAAM,CAAC,GAAM,KAAW,EACtB,KAAe,IACjB,GAEM,IAAa,QAAc;EAC/B,IAAI,CAAC,GAAM,OAAO;EAClB,IAAM,IAAM,EAAQ,MAAM,MAAM,EAAE,QAAQ,EAAK,GAAG;EAClD,IAAI,CAAC,GAAK,WAAW,OAAO;EAC5B,IAAM,IAAM,EAAK,QAAQ,QAAQ,IAAI;EACrC,OAAO,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM;GAC9B,IAAM,IAAK,EAAI,UAAW,CAAC,GACrB,IAAK,EAAI,UAAW,CAAC;GAG3B,OAFI,IAAK,IAAW,KAAK,IACrB,IAAK,IAAW,IAAI,IACjB;EACT,CAAC;CACH,GAAG;EAAC;EAAM;EAAM;CAAO,CAAC,GAElB,KAAc,MAAgB;EAClC,GAAS,MACP,GAAM,QAAQ,IAAM;GAAE;GAAK,KAAK,EAAK,QAAQ,QAAQ,SAAS;EAAM,IAAI;GAAE;GAAK,KAAK;EAAM,CAC5F;CACF;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAW,EAAG,mBAAmB,CAAS;YACrE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;cAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MAAQ;MACpB,IAAM,IAAW,EAAQ,EAAI,WACvB,IAAS,GAAM,QAAQ,EAAI;MACjC,OACE,kBAAC,MAAD;OAEE,SAAS,UAAiB,EAAW,EAAI,GAAG,IAAI,KAAA;OAChD,aACE,IAAU,GAAM,QAAQ,QAAQ,cAAc,eAAgB,KAAA;OAEhE,WAAW,EACT,+DACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,KAAY,oDACd;iBAEA,kBAAC,QAAD;QACE,WAAW,EACT,kCACA,EAAI,UAAU,WAAW,kBAC3B;kBAJF,CAMG,EAAI,QACJ,MACE,IACC,GAAM,QAAQ,QACZ,kBAAC,GAAD,EAAW,WAAU,iDAAkD,CAAA,IAEvE,kBAAC,GAAD,EAAa,WAAU,iDAAkD,CAAA,IAG3E,kBAAC,GAAD,EAAgB,WAAU,yBAA0B,CAAA,EAEpD;;MACJ,GA9BG,EAAI,GA8BP;KAER,CAAC;IACC,CAAA,EACC,CAAA,GACP,kBAAC,SAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,MAAD;KAEE,SAAS,UAAmB,EAAW,CAAG,IAAI,KAAA;KAC9C,WAAW,EACT,sDACA,KAAc,wDAChB;eAEC,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAEE,WAAW,EACT,4CACA,EAAW,EAAI,SAAS,SACxB,EAAI,gBAAgB,wBACpB,EAAI,SACN;gBAEC,EAAI,OAAO,CAAG;KACb,GATG,EAAI,GASP,CACL;IACC,GApBG,EAAO,CAAG,CAoBb,CACL,EACI,CAAA,CACF;;EACJ,CAAA;CACI,CAAA;AAEf"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI3C,UAAU,eAAe;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CA8B1C,CAAC"}
1
+ {"version":3,"file":"EmptyState.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAI3C,UAAU,eAAe;IACvB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CAgD1C,CAAC"}
@@ -1,21 +1,46 @@
1
1
  import { cn as e } from "../utils/cn.js";
2
2
  import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
- import { IllustratedEmptyState as r } from "@burdenoff/fe-libs/ui";
3
+ import { Illustration as r } from "@burdenoff/fe-libs/ui";
4
4
  //#region src/buildmyiq/components/EmptyState.tsx
5
- var i = ({ icon: i, illustration: a, title: o, description: s, action: c, className: l }) => a ? /* @__PURE__ */ t(r, {
6
- illustration: a,
7
- title: o,
8
- description: s,
9
- action: c,
10
- className: l
5
+ var i = ({ icon: i, illustration: a, title: o, description: s, action: c, className: l }) => a ? /* @__PURE__ */ n("div", {
6
+ className: e("relative flex flex-col items-center justify-center gap-4 px-6 py-12 text-center", l),
7
+ children: [
8
+ /* @__PURE__ */ n("div", {
9
+ className: "relative flex items-center justify-center",
10
+ children: [/* @__PURE__ */ t("div", {
11
+ "aria-hidden": "true",
12
+ className: "pointer-events-none absolute size-48 rounded-full bg-[radial-gradient(circle,var(--color-accent-soft),transparent_68%)] blur-2xl"
13
+ }), /* @__PURE__ */ t(r, {
14
+ name: a,
15
+ alt: "",
16
+ size: "lg",
17
+ className: "relative"
18
+ })]
19
+ }),
20
+ /* @__PURE__ */ n("div", {
21
+ className: "space-y-1.5",
22
+ children: [/* @__PURE__ */ t("h2", {
23
+ className: "text-lg font-semibold tracking-tight text-text-primary",
24
+ children: o
25
+ }), s && /* @__PURE__ */ t("p", {
26
+ className: "mx-auto max-w-sm text-sm text-text-secondary",
27
+ children: s
28
+ })]
29
+ }),
30
+ c && /* @__PURE__ */ t("div", {
31
+ className: "mt-1",
32
+ children: c
33
+ })
34
+ ]
11
35
  }) : /* @__PURE__ */ n("div", {
12
36
  className: e("flex flex-col items-center justify-center py-12 px-4 text-center", l),
13
37
  children: [
14
38
  i && /* @__PURE__ */ t("div", {
39
+ "aria-hidden": "true",
15
40
  className: "mb-4 text-text-secondary opacity-50",
16
41
  children: i
17
42
  }),
18
- /* @__PURE__ */ t("h3", {
43
+ /* @__PURE__ */ t("h2", {
19
44
  className: "text-lg font-medium text-text-primary",
20
45
  children: o
21
46
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 zero-state. When an `illustration` name is provided it renders the\n * fe-libs {@link IllustratedEmptyState} (on-brand glass illustration); otherwise\n * it falls back to the icon-based layout.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n /**\n * On-brand illustration name → `/illustrations/<name>.webp`. Standard names:\n * empty-data, empty-search, empty-files, empty-people, empty-notifications,\n * onboarding, empty-generic.\n */\n illustration?: string;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n illustration,\n title,\n description,\n action,\n className,\n}) => {\n if (illustration) {\n return (\n <IllustratedEmptyState\n illustration={illustration}\n title={title}\n description={description}\n action={action}\n className={className}\n />\n );\n }\n\n return (\n <div\n className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}\n >\n {icon && <div className=\"mb-4 text-text-secondary opacity-50\">{icon}</div>}\n <h3 className=\"text-lg font-medium text-text-primary\">{title}</h3>\n {description && <p className=\"mt-2 text-sm text-text-secondary max-w-sm\">{description}</p>}\n {action && <div className=\"mt-6\">{action}</div>}\n </div>\n );\n};\n"],"mappings":";;;;AA2BA,IAAa,KAAmC,EAC9C,SACA,iBACA,UACA,gBACA,WACA,mBAEI,IAEA,kBAAC,GAAD;CACgB;CACP;CACM;CACL;CACG;AACZ,CAAA,IAKH,kBAAC,OAAD;CACE,WAAW,EAAG,oEAAoE,CAAS;WAD7F;EAGG,KAAQ,kBAAC,OAAD;GAAK,WAAU;aAAuC;EAAU,CAAA;EACzE,kBAAC,MAAD;GAAI,WAAU;aAAyC;EAAU,CAAA;EAChE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAA6C;EAAe,CAAA;EACxF,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;EAAY,CAAA;CAC3C"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/buildmyiq/components/EmptyState.tsx"],"sourcesContent":["/**\n * Empty State Component\n * @module buildmyiq/components\n *\n * Boff UI 2.0 zero-state. When an `illustration` name is provided it renders the\n * on-brand fe-libs {@link Illustration}; otherwise it falls back to the\n * icon-based layout.\n *\n * Accessibility: the title is an `<h2>` (the page `<h1>` is the PageLayout\n * title, so an `<h3>` here skipped a level and failed axe `heading-order`), and\n * the illustration is decorative (`alt=\"\"`) because the heading right next to\n * it already says the same thing.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { Illustration } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n /**\n * On-brand illustration name → `/illustrations/<name>.webp`. Standard names:\n * empty-data, empty-search, empty-files, empty-people, empty-notifications,\n * onboarding, empty-generic.\n */\n illustration?: string;\n title: string;\n description?: string;\n action?: ReactNode;\n className?: string;\n}\n\nexport const EmptyState: FC<EmptyStateProps> = ({\n icon,\n illustration,\n title,\n description,\n action,\n className,\n}) => {\n if (illustration) {\n return (\n <div\n className={cn(\n 'relative flex flex-col items-center justify-center gap-4 px-6 py-12 text-center',\n className\n )}\n >\n <div className=\"relative flex items-center justify-center\">\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute size-48 rounded-full bg-[radial-gradient(circle,var(--color-accent-soft),transparent_68%)] blur-2xl\"\n />\n <Illustration name={illustration} alt=\"\" size=\"lg\" className=\"relative\" />\n </div>\n <div className=\"space-y-1.5\">\n <h2 className=\"text-lg font-semibold tracking-tight text-text-primary\">{title}</h2>\n {description && (\n <p className=\"mx-auto max-w-sm text-sm text-text-secondary\">{description}</p>\n )}\n </div>\n {action && <div className=\"mt-1\">{action}</div>}\n </div>\n );\n }\n\n return (\n <div\n className={cn('flex flex-col items-center justify-center py-12 px-4 text-center', className)}\n >\n {icon && (\n <div aria-hidden=\"true\" className=\"mb-4 text-text-secondary opacity-50\">\n {icon}\n </div>\n )}\n <h2 className=\"text-lg font-medium text-text-primary\">{title}</h2>\n {description && <p className=\"mt-2 text-sm text-text-secondary max-w-sm\">{description}</p>}\n {action && <div className=\"mt-6\">{action}</div>}\n </div>\n );\n};\n"],"mappings":";;;;AAgCA,IAAa,KAAmC,EAC9C,SACA,iBACA,UACA,gBACA,WACA,mBAEI,IAEA,kBAAC,OAAD;CACE,WAAW,EACT,mFACA,CACF;WAJF;EAME,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IACE,eAAY;IACZ,WAAU;GACX,CAAA,GACD,kBAAC,GAAD;IAAc,MAAM;IAAc,KAAI;IAAG,MAAK;IAAK,WAAU;GAAY,CAAA,CACtE;;EACL,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IAAI,WAAU;cAA0D;GAAU,CAAA,GACjF,KACC,kBAAC,KAAD;IAAG,WAAU;cAAgD;GAAe,CAAA,CAE3E;;EACJ,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;EAAY,CAAA;CAC3C;KAKP,kBAAC,OAAD;CACE,WAAW,EAAG,oEAAoE,CAAS;WAD7F;EAGG,KACC,kBAAC,OAAD;GAAK,eAAY;GAAO,WAAU;aAC/B;EACE,CAAA;EAEP,kBAAC,MAAD;GAAI,WAAU;aAAyC;EAAU,CAAA;EAChE,KAAe,kBAAC,KAAD;GAAG,WAAU;aAA6C;EAAe,CAAA;EACxF,KAAU,kBAAC,OAAD;GAAK,WAAU;aAAQ;EAAY,CAAA;CAC3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"EvidenceUpload.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAE1F,UAAU,mBAAmB;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAA0C,EAC1C,QAAgB,EAChB,IAAI,EACJ,UAAU,GACX,EAAE,mBAAmB,+BAkFrB"}
1
+ {"version":3,"file":"EvidenceUpload.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAoB,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,UAAU,CAAC;AAE1F,UAAU,mBAAmB;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,UAAU,CAAC,EAAE,iBAAiB,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC7B,EAAE,EACF,KAAK,EACL,KAAK,EACL,QAAQ,EACR,MAA0C,EAC1C,QAAgB,EAChB,IAAI,EACJ,UAAU,GACX,EAAE,mBAAmB,+BAmGrB"}
@@ -51,17 +51,27 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
51
51
  }) : b ? /* @__PURE__ */ o("div", {
52
52
  className: "flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2",
53
53
  children: [
54
- /* @__PURE__ */ a(l, { size: "sm" }),
55
- /* @__PURE__ */ o("span", {
54
+ /* @__PURE__ */ a(l, {
55
+ size: "sm",
56
+ "aria-hidden": "true"
57
+ }),
58
+ /* @__PURE__ */ a("span", {
59
+ role: "status",
56
60
  className: "text-sm text-text-secondary",
57
- children: [
58
- "Uploading… ",
59
- x,
60
- "%"
61
- ]
61
+ children: "Uploading…"
62
+ }),
63
+ /* @__PURE__ */ o("span", {
64
+ "aria-hidden": "true",
65
+ className: "text-sm text-text-secondary tabular-nums",
66
+ children: [x, "%"]
62
67
  }),
63
68
  /* @__PURE__ */ a("div", {
64
- className: "ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken",
69
+ role: "progressbar",
70
+ "aria-label": "Upload progress",
71
+ "aria-valuemin": 0,
72
+ "aria-valuemax": 100,
73
+ "aria-valuenow": x,
74
+ className: "ms-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken",
65
75
  children: /* @__PURE__ */ a("div", {
66
76
  className: "h-full rounded-full bg-action-primary-bg transition-[width] duration-150",
67
77
  style: { width: `${x}%` }
@@ -74,13 +84,14 @@ function u({ id: u, label: d, value: f, onChange: p, accept: m = "image/*,applic
74
84
  disabled: h,
75
85
  onClick: () => v.current?.click(),
76
86
  className: "justify-start",
77
- children: [/* @__PURE__ */ a(r, { className: "mr-1.5 h-4 w-4" }), " Choose file"]
87
+ children: [/* @__PURE__ */ a(r, { className: "me-1.5 h-4 w-4" }), " Choose file"]
78
88
  }),
79
89
  g && !S && /* @__PURE__ */ a("p", {
80
90
  className: "text-xs text-text-tertiary",
81
91
  children: g
82
92
  }),
83
93
  S && /* @__PURE__ */ a("p", {
94
+ role: "alert",
84
95
  className: "text-xs text-status-error-text",
85
96
  children: S
86
97
  })
@@ -1 +1 @@
1
- {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useBuiFileUpload, type BuiFileVisibility, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n /** File-store visibility of the upload; WORKSPACE unless the file is the uploader's alone. */\n visibility?: BuiFileVisibility;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n visibility,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useBuiFileUpload({ visibility });\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" />\n <span className=\"text-sm text-text-secondary\">Uploading… {progress}%</span>\n <div className=\"ml-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"mr-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2BA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,SACA,iBACsB;CACtB,IAAM,IAAW,EAAyB,IAAI,GACxC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,EAAiB,EAAE,cAAW,CAAC;CAoBvF,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;GAAa,CAAA;GAElC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UAAU,OA7BY,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;KAElC,MAAM,OAAO,QAAQ,IAChB,GACL,IAAI;MAEF,EAAS,MADc,EAAO,CAAI,CACjB;KACnB,QAAQ,CAER;IACF;GAmBK,CAAA;GAEA,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,sCAAuC,CAAA;KAC5D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;KACH,CAAA;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;OAEzB,AADA,EAAM,GACN,EAAS,IAAI;MACf;MA4BoB;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA;IACL;QACH,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAS,MAAK,KAAM,CAAA;KACpB,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAA8C;OAAY;OAAS;MAAO;;KAC1E,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,GAAG;MAChC,CAAA;KACE,CAAA;IACF;QAEL,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,MAAM;IACvC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,GAAC,cAC/B;;GAGT,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;GAAQ,CAAA;GACrE,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAkC;GAAS,CAAA;EAC/D;;AAET"}
1
+ {"version":3,"file":"EvidenceUpload.js","names":[],"sources":["../../../src/buildmyiq/components/EvidenceUpload.tsx"],"sourcesContent":["/**\n * EvidenceUpload — a controlled file-attach control (Integration A).\n * @module buildmyiq/components/EvidenceUpload\n *\n * Wraps `useBuiFileUpload` (the wspace-files 3-step flow). On file select it\n * uploads, shows byte-progress + the filename, and emits `{ fileId, fileName }`\n * via `onChange`. The parent stores `fileId` and passes it to the buildmyiq mutations\n * (`buiCreateCourse.coverFileId`, `buiIssueCertificate.fileId`).\n */\n\nimport { useRef, type ChangeEvent } from 'react';\nimport { Paperclip, Upload, X } from 'lucide-react';\nimport { Button, Label, Spinner } from '@burdenoff/fe-libs/ui';\nimport { useBuiFileUpload, type BuiFileVisibility, type UploadedBuiFile } from '../hooks';\n\ninterface EvidenceUploadProps {\n id: string;\n label: string;\n value: UploadedBuiFile | null;\n onChange: (value: UploadedBuiFile | null) => void;\n accept?: string;\n disabled?: boolean;\n hint?: string;\n /** File-store visibility of the upload; WORKSPACE unless the file is the uploader's alone. */\n visibility?: BuiFileVisibility;\n}\n\nexport function EvidenceUpload({\n id,\n label,\n value,\n onChange,\n accept = 'image/*,application/pdf,audio/*',\n disabled = false,\n hint,\n visibility,\n}: EvidenceUploadProps) {\n const inputRef = useRef<HTMLInputElement>(null);\n const { upload, isUploading, progress, error, reset } = useBuiFileUpload({ visibility });\n\n const handleSelect = async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target.files?.[0];\n // Clear the native input so the same file can be re-selected after a remove.\n event.target.value = '';\n if (!file) return;\n try {\n const uploaded = await upload(file);\n onChange(uploaded);\n } catch {\n // Error surfaced inline via `error`; keep the form usable.\n }\n };\n\n const handleRemove = () => {\n reset();\n onChange(null);\n };\n\n return (\n <div className=\"grid gap-1.5\">\n <Label htmlFor={id}>{label}</Label>\n\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n accept={accept}\n className=\"hidden\"\n disabled={disabled || isUploading}\n onChange={handleSelect}\n />\n\n {value ? (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Paperclip className=\"h-4 w-4 shrink-0 text-text-tertiary\" />\n <span\n className=\"min-w-0 flex-1 truncate text-sm text-text-primary\"\n title={value.fileName}\n >\n {value.fileName}\n </span>\n <button\n type=\"button\"\n onClick={handleRemove}\n disabled={disabled}\n aria-label=\"Remove attachment\"\n className=\"rounded-md p-1 text-text-tertiary hover:bg-bg-sunken hover:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ) : isUploading ? (\n <div className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-sunken/60 px-3 py-2\">\n <Spinner size=\"sm\" aria-hidden=\"true\" />\n {/* Announce the start once; the per-percent text is exposed via the progressbar, not re-announced. */}\n <span role=\"status\" className=\"text-sm text-text-secondary\">\n Uploading…\n </span>\n <span aria-hidden=\"true\" className=\"text-sm text-text-secondary tabular-nums\">\n {progress}%\n </span>\n <div\n role=\"progressbar\"\n aria-label=\"Upload progress\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={progress}\n className=\"ms-auto h-1.5 w-24 overflow-hidden rounded-full bg-bg-sunken\"\n >\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-150\"\n style={{ width: `${progress}%` }}\n />\n </div>\n </div>\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={disabled}\n onClick={() => inputRef.current?.click()}\n className=\"justify-start\"\n >\n <Upload className=\"me-1.5 h-4 w-4\" /> Choose file\n </Button>\n )}\n\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && (\n <p role=\"alert\" className=\"text-xs text-status-error-text\">\n {error}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2BA,SAAgB,EAAe,EAC7B,OACA,UACA,UACA,aACA,YAAS,mCACT,cAAW,IACX,SACA,iBACsB;CACtB,IAAM,IAAW,EAAyB,IAAI,GACxC,EAAE,WAAQ,gBAAa,aAAU,UAAO,aAAU,EAAiB,EAAE,cAAW,CAAC;CAoBvF,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IAAO,SAAS;cAAK;GAAa,CAAA;GAElC,kBAAC,SAAD;IACE,KAAK;IACD;IACJ,MAAK;IACG;IACR,WAAU;IACV,UAAU,KAAY;IACtB,UAAU,OA7BY,MAAyC;KACnE,IAAM,IAAO,EAAM,OAAO,QAAQ;KAElC,MAAM,OAAO,QAAQ,IAChB,GACL,IAAI;MAEF,EAAS,MADc,EAAO,CAAI,CACjB;KACnB,QAAQ,CAER;IACF;GAmBK,CAAA;GAEA,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD,EAAW,WAAU,sCAAuC,CAAA;KAC5D,kBAAC,QAAD;MACE,WAAU;MACV,OAAO,EAAM;gBAEZ,EAAM;KACH,CAAA;KACN,kBAAC,UAAD;MACE,MAAK;MACL,eA9BiB;OAEzB,AADA,EAAM,GACN,EAAS,IAAI;MACf;MA4BoB;MACV,cAAW;MACX,WAAU;gBAEV,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;KAClB,CAAA;IACL;QACH,IACF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAS,MAAK;MAAK,eAAY;KAAQ,CAAA;KAEvC,kBAAC,QAAD;MAAM,MAAK;MAAS,WAAU;gBAA8B;KAEtD,CAAA;KACN,kBAAC,QAAD;MAAM,eAAY;MAAO,WAAU;gBAAnC,CACG,GAAS,GACN;;KACN,kBAAC,OAAD;MACE,MAAK;MACL,cAAW;MACX,iBAAe;MACf,iBAAe;MACf,iBAAe;MACf,WAAU;gBAEV,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAS,GAAG;MAChC,CAAA;KACE,CAAA;IACF;QAEL,kBAAC,GAAD;IACE,MAAK;IACL,SAAQ;IACE;IACV,eAAe,EAAS,SAAS,MAAM;IACvC,WAAU;cALZ,CAOE,kBAAC,GAAD,EAAQ,WAAU,iBAAkB,CAAA,GAAC,cAC/B;;GAGT,KAAQ,CAAC,KAAS,kBAAC,KAAD;IAAG,WAAU;cAA8B;GAAQ,CAAA;GACrE,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;cACvB;GACA,CAAA;EAEF;;AAET"}
@@ -14,7 +14,7 @@ function r({ options: r, value: i, onChange: a, className: o }) {
14
14
  onClick: () => a(r.value),
15
15
  className: e("rounded-lg px-3 py-1.5 text-sm font-medium transition-colors", o ? "bg-action-primary-bg text-action-primary-text" : "text-text-secondary hover:bg-bg-sunken hover:text-text-primary"),
16
16
  children: [r.label, typeof r.count == "number" && /* @__PURE__ */ t("span", {
17
- className: e("ml-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums", o ? "bg-black/10 dark:bg-white/15" : "bg-bg-sunken"),
17
+ className: e("ms-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums", o ? "bg-black/10 dark:bg-white/15" : "bg-bg-sunken"),
18
18
  children: r.count
19
19
  })]
20
20
  }, r.value);
@@ -1 +1 @@
1
- {"version":3,"file":"FilterTabs.js","names":[],"sources":["../../../src/buildmyiq/components/FilterTabs.tsx"],"sourcesContent":["/**\n * FilterTabs — a segmented status/enum filter control.\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\nexport interface FilterOption<V extends string> {\n value: V;\n label: string;\n count?: number;\n}\n\ninterface FilterTabsProps<V extends string> {\n options: FilterOption<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function FilterTabs<V extends string>({\n options,\n value,\n onChange,\n className,\n}: FilterTabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n 'inline-flex flex-wrap items-center gap-1 rounded-xl border border-border-seam bg-bg-surface p-1',\n className\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(opt.value)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {opt.label}\n {typeof opt.count === 'number' && (\n <span\n className={cn(\n 'ml-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active ? 'bg-black/10 dark:bg-white/15' : 'bg-bg-sunken'\n )}\n >\n {opt.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AAoBA,SAAgB,EAA6B,EAC3C,YACA,UACA,aACA,gBACqB;CACrB,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EACT,mGACA,CACF;YAEC,EAAQ,KAAK,MAAQ;GACpB,IAAM,IAAS,EAAI,UAAU;GAC7B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAI,KAAK;IACjC,WAAW,EACT,gEACA,IACI,kDACA,gEACN;cAXF,CAaG,EAAI,OACJ,OAAO,EAAI,SAAU,YACpB,kBAAC,QAAD;KACE,WAAW,EACT,0DACA,IAAS,iCAAiC,cAC5C;eAEC,EAAI;IACD,CAAA,CAEF;MAvBD,EAAI,KAuBH;EAEZ,CAAC;CACE,CAAA;AAET"}
1
+ {"version":3,"file":"FilterTabs.js","names":[],"sources":["../../../src/buildmyiq/components/FilterTabs.tsx"],"sourcesContent":["/**\n * FilterTabs — a segmented status/enum filter control.\n * @module buildmyiq/components\n */\n\nimport { cn } from '../utils/cn';\n\nexport interface FilterOption<V extends string> {\n value: V;\n label: string;\n count?: number;\n}\n\ninterface FilterTabsProps<V extends string> {\n options: FilterOption<V>[];\n value: V;\n onChange: (value: V) => void;\n className?: string;\n}\n\nexport function FilterTabs<V extends string>({\n options,\n value,\n onChange,\n className,\n}: FilterTabsProps<V>) {\n return (\n <div\n role=\"tablist\"\n className={cn(\n 'inline-flex flex-wrap items-center gap-1 rounded-xl border border-border-seam bg-bg-surface p-1',\n className\n )}\n >\n {options.map((opt) => {\n const active = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => onChange(opt.value)}\n className={cn(\n 'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',\n active\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {opt.label}\n {typeof opt.count === 'number' && (\n <span\n className={cn(\n 'ms-1.5 rounded-full px-1.5 py-0.5 text-xs tabular-nums',\n active ? 'bg-black/10 dark:bg-white/15' : 'bg-bg-sunken'\n )}\n >\n {opt.count}\n </span>\n )}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;AAoBA,SAAgB,EAA6B,EAC3C,YACA,UACA,aACA,gBACqB;CACrB,OACE,kBAAC,OAAD;EACE,MAAK;EACL,WAAW,EACT,mGACA,CACF;YAEC,EAAQ,KAAK,MAAQ;GACpB,IAAM,IAAS,EAAI,UAAU;GAC7B,OACE,kBAAC,UAAD;IAEE,MAAK;IACL,MAAK;IACL,iBAAe;IACf,eAAe,EAAS,EAAI,KAAK;IACjC,WAAW,EACT,gEACA,IACI,kDACA,gEACN;cAXF,CAaG,EAAI,OACJ,OAAO,EAAI,SAAU,YACpB,kBAAC,QAAD;KACE,WAAW,EACT,0DACA,IAAS,iCAAiC,cAC5C;eAEC,EAAI;IACD,CAAA,CAEF;MAvBD,EAAI,KAuBH;EAEZ,CAAC;CACE,CAAA;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"GroundedTutorPanel.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/GroundedTutorPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAYH,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChD;AAmBD,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,EACX,gBAAgB,GACjB,EAAE,uBAAuB,+BA6MzB"}
1
+ {"version":3,"file":"GroundedTutorPanel.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/GroundedTutorPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAYH,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChD;AAmBD,wBAAgB,kBAAkB,CAAC,EACjC,QAAQ,EACR,WAAW,EACX,gBAAgB,GACjB,EAAE,uBAAuB,+BAkNzB"}
@@ -163,17 +163,19 @@ function x({ lessonId: x, lessonTitle: S, lessonTitlesById: C }) {
163
163
  e.key === "Enter" && !e.shiftKey && (e.preventDefault(), H());
164
164
  },
165
165
  placeholder: "Ask about this lesson…",
166
+ "aria-label": "Ask the tutor about this lesson",
166
167
  rows: 2,
167
168
  disabled: D.isPending || !z
168
169
  }), /* @__PURE__ */ m("div", {
169
170
  className: "mt-2 flex items-center justify-end gap-2",
170
171
  children: [
171
172
  M && /* @__PURE__ */ p("span", {
172
- className: "mr-auto text-xs text-status-error-text",
173
+ role: "alert",
174
+ className: "me-auto text-xs text-status-error-text",
173
175
  children: M
174
176
  }),
175
177
  E.isError && !M && /* @__PURE__ */ m("span", {
176
- className: "mr-auto flex items-center gap-2 text-xs text-status-error-text",
178
+ className: "me-auto flex items-center gap-2 text-xs text-status-error-text",
177
179
  children: ["Your tutor history could not be loaded.", /* @__PURE__ */ p(h, {
178
180
  size: "sm",
179
181
  variant: "outline",
@@ -185,7 +187,7 @@ function x({ lessonId: x, lessonTitle: S, lessonTitlesById: C }) {
185
187
  size: "sm",
186
188
  onClick: H,
187
189
  disabled: !A.trim() || D.isPending || !z,
188
- children: [/* @__PURE__ */ p(d, { className: "mr-1.5 h-4 w-4" }), D.isPending ? "Asking…" : "Ask"]
190
+ children: [/* @__PURE__ */ p(d, { className: "me-1.5 h-4 w-4" }), D.isPending ? "Asking…" : "Ask"]
189
191
  })
190
192
  ]
191
193
  })]