@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.
- package/dist/buildmyiq/components/AccessibilityChecker.js +1 -1
- package/dist/buildmyiq/components/AccessibilityChecker.js.map +1 -1
- package/dist/buildmyiq/components/BuiDiscussion.d.ts.map +1 -1
- package/dist/buildmyiq/components/BuiDiscussion.js +4 -2
- package/dist/buildmyiq/components/BuiDiscussion.js.map +1 -1
- package/dist/buildmyiq/components/BuiTags.d.ts.map +1 -1
- package/dist/buildmyiq/components/BuiTags.js +8 -3
- package/dist/buildmyiq/components/BuiTags.js.map +1 -1
- package/dist/buildmyiq/components/DataTable.js +2 -2
- package/dist/buildmyiq/components/DataTable.js.map +1 -1
- package/dist/buildmyiq/components/EmptyState.d.ts.map +1 -1
- package/dist/buildmyiq/components/EmptyState.js +33 -8
- package/dist/buildmyiq/components/EmptyState.js.map +1 -1
- package/dist/buildmyiq/components/EvidenceUpload.d.ts.map +1 -1
- package/dist/buildmyiq/components/EvidenceUpload.js +20 -9
- package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
- package/dist/buildmyiq/components/FilterTabs.js +1 -1
- package/dist/buildmyiq/components/FilterTabs.js.map +1 -1
- package/dist/buildmyiq/components/GroundedTutorPanel.d.ts.map +1 -1
- package/dist/buildmyiq/components/GroundedTutorPanel.js +5 -3
- package/dist/buildmyiq/components/GroundedTutorPanel.js.map +1 -1
- package/dist/buildmyiq/components/OfflineLessonControl.js +3 -3
- package/dist/buildmyiq/components/OfflineLessonControl.js.map +1 -1
- package/dist/buildmyiq/components/Pagination.js +2 -2
- package/dist/buildmyiq/components/Pagination.js.map +1 -1
- package/dist/buildmyiq/components/StateViews.d.ts +5 -0
- package/dist/buildmyiq/components/StateViews.d.ts.map +1 -1
- package/dist/buildmyiq/components/StateViews.js +11 -2
- package/dist/buildmyiq/components/StateViews.js.map +1 -1
- package/dist/buildmyiq/components/Switch.d.ts +6 -0
- package/dist/buildmyiq/components/Switch.d.ts.map +1 -0
- package/dist/buildmyiq/components/Switch.js +12 -0
- package/dist/buildmyiq/components/Switch.js.map +1 -0
- package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/DashboardPage.js +5 -5
- package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
- package/dist/buildmyiq/pages/_shared/CourseCard.js +1 -1
- package/dist/buildmyiq/pages/_shared/CourseCard.js.map +1 -1
- package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.js +53 -51
- package/dist/buildmyiq/pages/aiSettings/AiSettingsPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.js +5 -3
- package/dist/buildmyiq/pages/assessments/AiAssessmentDraftDialog.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js +2 -2
- package/dist/buildmyiq/pages/assessments/AiMonitoringConsent.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js +12 -10
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +97 -95
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/ProctoringGate.js +5 -5
- package/dist/buildmyiq/pages/assessments/ProctoringGate.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +14 -6
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +132 -128
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseAccessibilityTab.js +1 -1
- package/dist/buildmyiq/pages/authoring/CourseAccessibilityTab.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +206 -202
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.d.ts +19 -2
- package/dist/buildmyiq/pages/authoring/_forms.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.js +51 -35
- package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
- package/dist/buildmyiq/pages/automations/AutomationsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/automations/AutomationsPage.js +125 -122
- package/dist/buildmyiq/pages/automations/AutomationsPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CatalogPage.js +2 -2
- package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +7 -6
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js +9 -8
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CredentialShareDialog.js +4 -4
- package/dist/buildmyiq/pages/certificates/CredentialShareDialog.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js +5 -4
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
- package/dist/buildmyiq/pages/copilot/AdminCopilotActionCard.js +6 -6
- package/dist/buildmyiq/pages/copilot/AdminCopilotActionCard.js.map +1 -1
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js +5 -4
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js.map +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.d.ts.map +1 -1
- package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.js +6 -4
- package/dist/buildmyiq/pages/integrations/LtiPlacementsPanel.js.map +1 -1
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js +7 -5
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +9 -9
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.js +5 -5
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/dist/buildmyiq/pages/lti/LtiLaunchPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/lti/LtiLaunchPage.js +2 -1
- package/dist/buildmyiq/pages/lti/LtiLaunchPage.js.map +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.js +3 -3
- package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
- package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js +4 -4
- package/dist/buildmyiq/pages/proctoring/ProctoringReviewPage.js.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.js +4 -3
- package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js +105 -99
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +179 -177
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/dist/buildmyiq/pages/tutorReview/TutorReviewPage.js +6 -6
- package/dist/buildmyiq/pages/tutorReview/TutorReviewPage.js.map +1 -1
- 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: "
|
|
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,+
|
|
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
|
-
|
|
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: "
|
|
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=\"
|
|
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,+
|
|
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, {
|
|
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-
|
|
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-
|
|
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-
|
|
9
|
-
right: "text-
|
|
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-
|
|
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
|
|
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 {
|
|
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__ */
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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("
|
|
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
|
|
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,+
|
|
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, {
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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: "
|
|
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\"
|
|
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("
|
|
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 '
|
|
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,+
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
190
|
+
children: [/* @__PURE__ */ p(d, { className: "me-1.5 h-4 w-4" }), D.isPending ? "Asking…" : "Ask"]
|
|
189
191
|
})
|
|
190
192
|
]
|
|
191
193
|
})]
|