@eifi1/ui-kit 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -5
- package/dist/account/data-export-setting.js +3 -2
- package/dist/account/data-export-setting.js.map +1 -1
- package/dist/account/delete-account-setting.d.ts +5 -1
- package/dist/account/delete-account-setting.js +3 -2
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting.js +4 -3
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/sessions-setting.js +3 -2
- package/dist/account/sessions-setting.js.map +1 -1
- package/dist/admin/admin-action-confirm.d.ts +5 -1
- package/dist/admin/invitations-panel.d.ts +5 -1
- package/dist/admin/invitations-panel.js +3 -2
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +5 -1
- package/dist/admin/role-select.d.ts +5 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +5 -1
- package/dist/admin/user-roster.d.ts +5 -1
- package/dist/auth/auth-errors.d.ts +24 -6
- package/dist/auth/auth-errors.js +7 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/register-form.d.ts +5 -1
- package/dist/auth/sign-in-form.d.ts +15 -1
- package/dist/auth/sign-in-form.js +12 -3
- package/dist/auth/sign-in-form.js.map +1 -1
- package/dist/chart.d.ts +5 -1
- package/dist/components/account-settings-labels.d.ts +12 -0
- package/dist/components/account-settings-labels.js +2 -1
- package/dist/components/account-settings-labels.js.map +1 -1
- package/dist/components/account-settings.d.ts +8 -2
- package/dist/components/account-settings.js +19 -11
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -1
- package/dist/components/button-group.d.ts +5 -1
- package/dist/components/calculator.d.ts +5 -1
- package/dist/components/confirm-dialog.d.ts +5 -1
- package/dist/components/copy-button.d.ts +5 -1
- package/dist/components/danger-confirm.d.ts +5 -1
- package/dist/components/data-table.js +8 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +9 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/facing-pair.d.ts +5 -1
- package/dist/components/file-button.d.ts +5 -1
- package/dist/components/file-dropzone.d.ts +5 -1
- package/dist/components/form-actions.d.ts +5 -1
- package/dist/components/iban-input.d.ts +5 -1
- package/dist/components/language-select.d.ts +5 -1
- package/dist/components/legal-page.d.ts +5 -1
- package/dist/components/legal.d.ts +5 -1
- package/dist/components/money-field.d.ts +5 -1
- package/dist/components/number-field.d.ts +5 -1
- package/dist/components/number-input.d.ts +5 -1
- package/dist/components/numpad-sheet.d.ts +5 -1
- package/dist/components/passkeys-setting.js +2 -1
- package/dist/components/passkeys-setting.js.map +1 -1
- package/dist/components/phone-input.d.ts +5 -1
- package/dist/components/series-chart.d.ts +5 -1
- package/dist/components/settings-fields.d.ts +40 -3
- package/dist/components/settings-fields.js +20 -1
- package/dist/components/settings-fields.js.map +1 -1
- package/dist/components/share-card.d.ts +5 -1
- package/dist/components/text-link.d.ts +5 -1
- package/dist/components/time-input.d.ts +5 -1
- package/dist/components/ui.d.ts +5 -1
- package/dist/{delete-account-setting-CGVMckMw.d.ts → delete-account-setting-DCFS5Vof.d.ts} +52 -23
- package/dist/demo/demo-banner.d.ts +51 -0
- package/dist/demo/demo-banner.js +97 -0
- package/dist/demo/demo-banner.js.map +1 -0
- package/dist/demo/demo-countdown.d.ts +58 -0
- package/dist/demo/demo-countdown.js +79 -0
- package/dist/demo/demo-countdown.js.map +1 -0
- package/dist/demo/demo-ended.d.ts +45 -0
- package/dist/demo/demo-ended.js +42 -0
- package/dist/demo/demo-ended.js.map +1 -0
- package/dist/demo/demo-labels.d.ts +53 -0
- package/dist/demo/demo-labels.js +29 -0
- package/dist/demo/demo-labels.js.map +1 -0
- package/dist/demo/demo-session.d.ts +30 -0
- package/dist/demo/demo-session.js +8 -0
- package/dist/demo/demo-session.js.map +1 -0
- package/dist/demo/demo-start.d.ts +54 -0
- package/dist/demo/demo-start.js +68 -0
- package/dist/demo/demo-start.js.map +1 -0
- package/dist/feedback/feedback-attachment.d.ts +5 -1
- package/dist/feedback/feedback-context.d.ts +5 -1
- package/dist/feedback/feedback-crash.d.ts +5 -1
- package/dist/feedback/feedback-dialog.d.ts +5 -1
- package/dist/feedback/feedback-inbox.d.ts +5 -1
- package/dist/feedback/feedback-inbox.js +12 -2
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +5 -1
- package/dist/feedback/feedback-labels.js +1 -0
- package/dist/feedback/feedback-labels.js.map +1 -1
- package/dist/feedback/feedback-menu.d.ts +5 -1
- package/dist/feedback/feedback-record.d.ts +5 -1
- package/dist/feedback/feedback-record.js +2 -0
- package/dist/feedback/feedback-record.js.map +1 -1
- package/dist/feedback/feedback-row-detail.d.ts +5 -1
- package/dist/feedback/feedback-row-detail.js +7 -0
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +5 -1
- package/dist/feedback/feedback-submit.d.ts +5 -1
- package/dist/feedback/feedback-swipe.d.ts +6 -2
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +5 -1
- package/dist/feedback/feedback-thread.d.ts +5 -1
- package/dist/feedback.d.ts +5 -1
- package/dist/hooks/use-file-drop.d.ts +5 -1
- package/dist/i18n/defaults.d.ts +5 -1
- package/dist/i18n/defaults.js +6 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +5 -1
- package/dist/i18n/german.js +84 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +5 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +5 -1
- package/dist/i18n/locales/de-CH.d.ts +5 -1
- package/dist/i18n/locales/en.d.ts +5 -1
- package/dist/i18n/locales/es.d.ts +5 -1
- package/dist/i18n/locales/es.js +81 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +5 -1
- package/dist/i18n/locales/fr.js +82 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +5 -1
- package/dist/i18n/locales/hu.js +82 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +5 -1
- package/dist/i18n/locales/it.js +81 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +5 -1
- package/dist/i18n/locales/zh.js +80 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +5 -1
- package/dist/i18n/review.js +14 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +25 -3
- package/dist/index.js +95 -0
- package/dist/index.js.map +1 -1
- package/dist/landing/access.d.ts +48 -0
- package/dist/landing/access.js +38 -0
- package/dist/landing/access.js.map +1 -0
- package/dist/landing/landing-actions.d.ts +138 -0
- package/dist/landing/landing-actions.js +39 -0
- package/dist/landing/landing-actions.js.map +1 -0
- package/dist/landing/landing-labels.d.ts +43 -0
- package/dist/landing/landing-labels.js +23 -0
- package/dist/landing/landing-labels.js.map +1 -0
- package/dist/landing/landing-sections.d.ts +218 -0
- package/dist/landing/landing-sections.js +167 -0
- package/dist/landing/landing-sections.js.map +1 -0
- package/dist/landing/page-seo.d.ts +99 -0
- package/dist/landing/page-seo.js +121 -0
- package/dist/landing/page-seo.js.map +1 -0
- package/dist/landing/public-header.d.ts +148 -0
- package/dist/landing/public-header.js +49 -0
- package/dist/landing/public-header.js.map +1 -0
- package/dist/landing/routing.d.ts +218 -0
- package/dist/landing/routing.js +81 -0
- package/dist/landing/routing.js.map +1 -0
- package/dist/rhf/fields.d.ts +5 -1
- package/dist/rhf/form.d.ts +5 -1
- package/dist/rhf.d.ts +5 -1
- package/dist/settings/settings-catalogue.d.ts +84 -0
- package/dist/settings/settings-catalogue.js +47 -0
- package/dist/settings/settings-catalogue.js.map +1 -0
- package/dist/settings/settings-context.d.ts +31 -0
- package/dist/settings/settings-context.js +11 -0
- package/dist/settings/settings-context.js.map +1 -0
- package/dist/settings/settings-heading.d.ts +50 -0
- package/dist/settings/settings-heading.js +27 -0
- package/dist/settings/settings-heading.js.map +1 -0
- package/dist/settings/settings-labels.d.ts +41 -0
- package/dist/settings/settings-labels.js +38 -0
- package/dist/settings/settings-labels.js.map +1 -0
- package/dist/settings/settings-layout.d.ts +77 -0
- package/dist/settings/settings-layout.js +242 -0
- package/dist/settings/settings-layout.js.map +1 -0
- package/dist/settings/settings-section.d.ts +133 -0
- package/dist/settings/settings-section.js +63 -0
- package/dist/settings/settings-section.js.map +1 -0
- package/dist/settings/use-account-language.d.ts +83 -0
- package/dist/settings/use-account-language.js +62 -0
- package/dist/settings/use-account-language.js.map +1 -0
- package/dist/settings/use-settings-focus.d.ts +34 -0
- package/dist/settings/use-settings-focus.js +77 -0
- package/dist/settings/use-settings-focus.js.map +1 -0
- package/dist/settings/use-settings-route.d.ts +118 -0
- package/dist/settings/use-settings-route.js +120 -0
- package/dist/settings/use-settings-route.js.map +1 -0
- package/dist/shell/app-shell.d.ts +11 -1
- package/dist/shell/app-shell.js +3 -2
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +5 -1
- package/dist/shell.d.ts +15 -1
- package/dist/shell.js +56 -0
- package/dist/shell.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +5 -1
- package/dist/wizard.d.ts +5 -1
- package/package.json +2 -1
- package/src/account/data-export-setting.tsx +3 -2
- package/src/account/delete-account-setting.tsx +3 -2
- package/src/account/email-change-setting.tsx +4 -7
- package/src/account/sessions-setting.tsx +3 -2
- package/src/admin/invitations-panel.tsx +11 -4
- package/src/auth/auth-errors.ts +36 -5
- package/src/auth/sign-in-form.tsx +34 -3
- package/src/components/account-settings-labels.ts +13 -0
- package/src/components/account-settings.tsx +33 -12
- package/src/components/data-table.tsx +8 -0
- package/src/components/date-picker.tsx +9 -4
- package/src/components/passkeys-setting.tsx +2 -1
- package/src/components/settings-fields.tsx +50 -1
- package/src/demo/demo-banner.tsx +159 -0
- package/src/demo/demo-countdown.ts +141 -0
- package/src/demo/demo-ended.tsx +89 -0
- package/src/demo/demo-labels.ts +78 -0
- package/src/demo/demo-session.ts +32 -0
- package/src/demo/demo-start.tsx +150 -0
- package/src/feedback/feedback-inbox.tsx +21 -8
- package/src/feedback/feedback-labels.ts +2 -1
- package/src/feedback/feedback-record.ts +10 -7
- package/src/feedback/feedback-row-detail.tsx +28 -4
- package/src/feedback/feedback-swipe.tsx +1 -1
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/german.ts +86 -0
- package/src/i18n/kit-labels.tsx +9 -0
- package/src/i18n/locales/es.ts +83 -0
- package/src/i18n/locales/fr.ts +84 -0
- package/src/i18n/locales/hu.ts +84 -0
- package/src/i18n/locales/it.ts +83 -0
- package/src/i18n/locales/zh.ts +81 -0
- package/src/i18n/review.ts +13 -0
- package/src/index.ts +92 -1
- package/src/landing/access.ts +97 -0
- package/src/landing/landing-actions.tsx +92 -0
- package/src/landing/landing-labels.ts +59 -0
- package/src/landing/landing-sections.tsx +337 -0
- package/src/landing/page-seo.ts +251 -0
- package/src/landing/public-header.tsx +120 -0
- package/src/landing/routing.tsx +209 -0
- package/src/settings/settings-catalogue.ts +132 -0
- package/src/settings/settings-context.ts +33 -0
- package/src/settings/settings-heading.tsx +65 -0
- package/src/settings/settings-labels.ts +75 -0
- package/src/settings/settings-layout.tsx +379 -0
- package/src/settings/settings-section.tsx +108 -0
- package/src/settings/use-account-language.ts +146 -0
- package/src/settings/use-settings-focus.ts +120 -0
- package/src/settings/use-settings-route.ts +258 -0
- package/src/shell/app-shell.tsx +10 -2
- package/src/shell.ts +45 -0
package/dist/components/ui.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import 'react';
|
|
2
2
|
import './tooltip.js';
|
|
3
|
-
export { bB as Button, bC as ButtonClassesOptions, bD as ButtonLinkProps, bE as ButtonProps, bF as ButtonSize, bG as ButtonTone, bH as ButtonVariant, bJ as Card, bK as CardAction, bL as CardActionProps, bM as CardContent, bN as CardContentProps, bO as CardDensity, bP as CardDescription, bQ as CardDescriptionProps, bR as CardFooter, bS as CardFooterProps, bT as CardHeader, bU as CardHeaderProps, bV as CardProps, bW as CardTitle, bX as CardTitleLevel, bY as CardTitleProps, bZ as CardTone, b_ as CharacterCountLabels, ci as DEFAULT_CHARACTER_COUNT_LABELS, cy as DEFAULT_PASSWORD_REVEAL_LABELS, cG as DEFAULT_TABS_LABELS, cW as EmptyState, cX as EmptyStateProps, cY as FIELD_BASE, cZ as FIELD_DISPLAY, c_ as FIELD_FLOATING_PAD, c$ as FIELD_INVALID, d0 as FIELD_TRIGGER, d1 as FIELD_WRITABLE_LOOK, d2 as FLOATING_INPUT_CLASS, d3 as FLOATING_LABEL_CLASS, d4 as FieldChevron, d5 as FieldChevronProps, d6 as FieldHint, d7 as FieldHintProps, df as FloatingField, dg as FloatingFieldProps, du as IconButton, dv as IconButtonGlyphSize, dw as IconButtonLinkProps, dx as IconButtonProps, dy as IconButtonSize, dz as IconButtonTone, dA as IconButtonToneColor, dB as IconButtonVariant, dC as Input, dD as InputProps, dU as Label, dW as LabelProps, ec as PHONE_QUERY, ed as PasswordRevealLabels, eC as Select, eD as SelectProps, eQ as Spinner, eR as SpinnerProps, eT as TabItem, eU as Tabs, eV as TabsLabels, eW as TabsProps, eX as Textarea, eY as TextareaProps, fq as buttonClasses, fA as resolvePasswordRevealLabels } from '../delete-account-setting-
|
|
3
|
+
export { bB as Button, bC as ButtonClassesOptions, bD as ButtonLinkProps, bE as ButtonProps, bF as ButtonSize, bG as ButtonTone, bH as ButtonVariant, bJ as Card, bK as CardAction, bL as CardActionProps, bM as CardContent, bN as CardContentProps, bO as CardDensity, bP as CardDescription, bQ as CardDescriptionProps, bR as CardFooter, bS as CardFooterProps, bT as CardHeader, bU as CardHeaderProps, bV as CardProps, bW as CardTitle, bX as CardTitleLevel, bY as CardTitleProps, bZ as CardTone, b_ as CharacterCountLabels, ci as DEFAULT_CHARACTER_COUNT_LABELS, cy as DEFAULT_PASSWORD_REVEAL_LABELS, cG as DEFAULT_TABS_LABELS, cW as EmptyState, cX as EmptyStateProps, cY as FIELD_BASE, cZ as FIELD_DISPLAY, c_ as FIELD_FLOATING_PAD, c$ as FIELD_INVALID, d0 as FIELD_TRIGGER, d1 as FIELD_WRITABLE_LOOK, d2 as FLOATING_INPUT_CLASS, d3 as FLOATING_LABEL_CLASS, d4 as FieldChevron, d5 as FieldChevronProps, d6 as FieldHint, d7 as FieldHintProps, df as FloatingField, dg as FloatingFieldProps, du as IconButton, dv as IconButtonGlyphSize, dw as IconButtonLinkProps, dx as IconButtonProps, dy as IconButtonSize, dz as IconButtonTone, dA as IconButtonToneColor, dB as IconButtonVariant, dC as Input, dD as InputProps, dU as Label, dW as LabelProps, ec as PHONE_QUERY, ed as PasswordRevealLabels, eC as Select, eD as SelectProps, eQ as Spinner, eR as SpinnerProps, eT as TabItem, eU as Tabs, eV as TabsLabels, eW as TabsProps, eX as Textarea, eY as TextareaProps, fq as buttonClasses, fA as resolvePasswordRevealLabels } from '../delete-account-setting-DCFS5Vof.js';
|
|
4
4
|
export { FLOATING_LABEL_STATIC, FieldLabel, FieldLabelProps } from './field-parts.js';
|
|
5
5
|
import '../hooks/use-anchored-rect.js';
|
|
6
6
|
import '../lib/clipping.js';
|
|
@@ -55,6 +55,8 @@ import './image-grid.js';
|
|
|
55
55
|
import './lightbox.js';
|
|
56
56
|
import './write-lock.js';
|
|
57
57
|
import '../auth/sign-in-form.js';
|
|
58
|
+
import '../landing/access.js';
|
|
59
|
+
import '../landing/landing-labels.js';
|
|
58
60
|
import '../shell/topbar-controls.js';
|
|
59
61
|
import '../theme/palette-presets.js';
|
|
60
62
|
import '../theme/chart-palette.js';
|
|
@@ -76,6 +78,8 @@ import '../account/data-export-setting.js';
|
|
|
76
78
|
import './account-chips.js';
|
|
77
79
|
import '../admin/admin-parts.js';
|
|
78
80
|
import '../admin/admin-action-log.js';
|
|
81
|
+
import '../settings/settings-labels.js';
|
|
82
|
+
import '../demo/demo-labels.js';
|
|
79
83
|
import './reauth-dialog.js';
|
|
80
84
|
import './server-wake.js';
|
|
81
85
|
import '../lib/server-wake.js';
|
|
@@ -60,6 +60,9 @@ import { RoleVocabulary, AccountState, RoleDefinition, AccountStateLabels } from
|
|
|
60
60
|
import { SortState } from './components/data-table-sort.js';
|
|
61
61
|
import { MaybePromise as MaybePromise$1, AdminPerson } from './admin/admin-parts.js';
|
|
62
62
|
import { AdminActionLogLabels } from './admin/admin-action-log.js';
|
|
63
|
+
import { SettingsLabels } from './settings/settings-labels.js';
|
|
64
|
+
import { LandingLabels } from './landing/landing-labels.js';
|
|
65
|
+
import { DemoLabels } from './demo/demo-labels.js';
|
|
63
66
|
import { ReauthDialogLabels } from './components/reauth-dialog.js';
|
|
64
67
|
import { ServerWakeLabels } from './components/server-wake.js';
|
|
65
68
|
import { TranslationReviewLabels } from './components/translation-review-labels.js';
|
|
@@ -855,12 +858,14 @@ declare function pastedName(type: string): string;
|
|
|
855
858
|
* keksdose's wording), read by default by the badges and the transitions below;
|
|
856
859
|
* a `label` the caller passes still wins.
|
|
857
860
|
*/
|
|
858
|
-
/** The
|
|
861
|
+
/** The eight states a report can be in.
|
|
859
862
|
*
|
|
860
|
-
* Shared value for value across
|
|
861
|
-
* and one agent prompt, work on
|
|
862
|
-
* it — see {@link visibleFeedbackStatuses} for what that buys.
|
|
863
|
-
|
|
863
|
+
* Shared value for value across the apps on purpose: it is what lets one habit,
|
|
864
|
+
* and one agent prompt, work on any repo. Five are a chain and three sit off
|
|
865
|
+
* it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31
|
|
866
|
+
* (feedback contract §8.2): OPEN is "filed, not yet triaged", READY "released for
|
|
867
|
+
* implementation", and nobody works an OPEN row. */
|
|
868
|
+
type FeedbackStatus = "OPEN" | "READY" | "IN_PROGRESS" | "IN_EVALUATION" | "NEEDS_LIVE_TEST" | "POSTPONED" | "DONE" | "WONT_DO";
|
|
864
869
|
/** What a report is about. `CRASH` is filed by an error boundary and never
|
|
865
870
|
* chosen, which is exactly why it is loud below and absent from every picker. */
|
|
866
871
|
type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
|
|
@@ -872,7 +877,7 @@ type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
|
|
|
872
877
|
* its own shape as well. Declaration order is chain order, and
|
|
873
878
|
* {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because
|
|
874
879
|
* a restated list cannot be checked for exhaustiveness: an eighth status would
|
|
875
|
-
* break the build here and leave a literal
|
|
880
|
+
* break the build here and leave a literal eight long (as READY did in 0.31).
|
|
876
881
|
*/
|
|
877
882
|
declare const FEEDBACK_STATUS_META: Record<FeedbackStatus, {
|
|
878
883
|
icon: LucideIcon;
|
|
@@ -890,7 +895,7 @@ declare function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | n
|
|
|
890
895
|
* The steps that make sense from where a row currently stands.
|
|
891
896
|
*
|
|
892
897
|
* For the compact control in a table cell: it is a glanceable triage affordance,
|
|
893
|
-
* and a row of
|
|
898
|
+
* and a row of eight icons in a cell is noise. One step forward, one step back
|
|
894
899
|
* (so an item can be sent back for rework), and any of the three off-chain
|
|
895
900
|
* verdicts — which is what makes those usable as verdicts at all.
|
|
896
901
|
*
|
|
@@ -1511,7 +1516,7 @@ type ChatComposerSlotContext = FeedbackComposerSlotContext;
|
|
|
1511
1516
|
* `feedbackStatus` — one word per status, keyed by the enum value so
|
|
1512
1517
|
* `labels[row.status]` is the whole lookup.
|
|
1513
1518
|
*
|
|
1514
|
-
* de-CH canon: OPEN "Offen" · IN_PROGRESS "In Bearbeitung" · IN_EVALUATION "Zur Prüfung"
|
|
1519
|
+
* de-CH canon: OPEN "Offen" · READY "Bereit zur Umsetzung" · IN_PROGRESS "In Bearbeitung" · IN_EVALUATION "Zur Prüfung"
|
|
1515
1520
|
* · NEEDS_LIVE_TEST "Live testen" · POSTPONED "Zurückgestellt" · DONE "Erledigt" ·
|
|
1516
1521
|
* WONT_DO "Wird nicht umgesetzt".
|
|
1517
1522
|
*/
|
|
@@ -1780,16 +1785,17 @@ declare const FEEDBACK_PICKABLE_CATEGORIES: readonly FeedbackCategory[];
|
|
|
1780
1785
|
declare const FEEDBACK_REWORKABLE_STATUSES: readonly FeedbackStatus[];
|
|
1781
1786
|
/**
|
|
1782
1787
|
* The statuses in which the AUTHOR may still edit title, body and category (§3.4,
|
|
1783
|
-
* keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN` and `IN_PROGRESS`.
|
|
1784
|
-
* team has answered, the description is what they answered and stays as it
|
|
1785
|
-
* correction goes in as a rework.
|
|
1788
|
+
* keksdose `canAuthorEdit`, `feedback-page.tsx:398`): `OPEN`, `READY` and `IN_PROGRESS`.
|
|
1789
|
+
* Once the team has answered, the description is what they answered and stays as it
|
|
1790
|
+
* was; a correction goes in as a rework.
|
|
1786
1791
|
*/
|
|
1787
1792
|
declare const FEEDBACK_AUTHOR_EDITABLE_STATUSES: readonly FeedbackStatus[];
|
|
1788
1793
|
/**
|
|
1789
|
-
* The
|
|
1790
|
-
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1794
|
+
* The statuses that wait on the person triaging: filed and not yet triaged (`OPEN`,
|
|
1795
|
+
* since READY took over "released for implementation" in 0.31, feedback contract §8.2),
|
|
1796
|
+
* answered and handed back for a verdict, or answered and only checkable on a deployed
|
|
1797
|
+
* build (keksdose `AWAITING_VERDICT`, live #330). The phone's "Only what is waiting for
|
|
1798
|
+
* you" toggle narrows the inbox to exactly these (§4.3).
|
|
1793
1799
|
*/
|
|
1794
1800
|
declare const FEEDBACK_AWAITING_STATUSES: readonly FeedbackStatus[];
|
|
1795
1801
|
/** The body line for one uploaded attachment — what {@link appendRework} writes under
|
|
@@ -2380,8 +2386,9 @@ declare function FeedbackEmptyState({ labels: labelsProp }: FeedbackEmptyStatePr
|
|
|
2380
2386
|
* path (`/accounts?p=2`): keksdose's `new URL(…)` throws on one and the section
|
|
2381
2387
|
* vanished. kastlan already fell back to `context.route` (`feedbackRaw`, `:97`).
|
|
2382
2388
|
*
|
|
2383
|
-
* The sections, in the contract's order: **
|
|
2384
|
-
*
|
|
2389
|
+
* The sections, in the contract's order: **Subject** (the category and the title, §8.1;
|
|
2390
|
+
* `showSubject={false}` leaves it out), **Description** (the body; the author's
|
|
2391
|
+
* Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Outcome** (the admin's
|
|
2385
2392
|
* outcome editor, Rework, Open page), **Send for rework** (only while reworking) and
|
|
2386
2393
|
* **Status**. No comment thread (§2.2: kastlan's comments fold into the body, §7.7).
|
|
2387
2394
|
*
|
|
@@ -2395,7 +2402,11 @@ declare function FeedbackEmptyState({ labels: labelsProp }: FeedbackEmptyStatePr
|
|
|
2395
2402
|
* canon (keksdose `de-CH.json`, ss never ß, "Nacharbeit" throughout — §7.14) on each key.
|
|
2396
2403
|
*/
|
|
2397
2404
|
interface FeedbackDetailLabels {
|
|
2398
|
-
/**
|
|
2405
|
+
/** The first section's heading: what the report is called (feedback contract §8.1,
|
|
2406
|
+
* keksdose live #395 — with the row expanded and the table scrolled, nothing said
|
|
2407
|
+
* which entry was being answered). de-CH: "Betreff" */
|
|
2408
|
+
subject: string;
|
|
2409
|
+
/** The description section's heading — keksdose's `feedback.body` (§7.3). The dialog's field asks
|
|
2399
2410
|
* "What happened? (optional)"; the description, once filed, is headed as what it is
|
|
2400
2411
|
* — and as the author's "Edit description" calls it. de-CH: "Beschreibung" */
|
|
2401
2412
|
body: string;
|
|
@@ -2487,7 +2498,7 @@ declare function feedbackAttachmentUrls(row: Pick<FeedbackRecord, "screenshot_ur
|
|
|
2487
2498
|
interface FeedbackRowAccess {
|
|
2488
2499
|
/** The viewer filed it (`viewerId === row.user_id`; never for an erased author). */
|
|
2489
2500
|
isAuthor: boolean;
|
|
2490
|
-
/** "Edit description": the author, while the row is OPEN or IN_PROGRESS — on either
|
|
2501
|
+
/** "Edit description": the author, while the row is OPEN, READY or IN_PROGRESS — on either
|
|
2491
2502
|
* page (keksdose `canAuthorEdit`, `feedback-page.tsx:398`, is not gated on `mine`). */
|
|
2492
2503
|
canEditDescription: boolean;
|
|
2493
2504
|
/** The outcome editor: the admin on the inbox (`canEdit`). */
|
|
@@ -2620,6 +2631,12 @@ interface FeedbackRowDetailProps {
|
|
|
2620
2631
|
* BUDGET, and a person viewing one still owns their own reports.
|
|
2621
2632
|
*/
|
|
2622
2633
|
commit?: boolean;
|
|
2634
|
+
/**
|
|
2635
|
+
* Open with the subject: the category badge and the title (feedback contract §8.1,
|
|
2636
|
+
* keksdose live #395). On by default; `false` for a page that already shows the title
|
|
2637
|
+
* beside the detail.
|
|
2638
|
+
*/
|
|
2639
|
+
showSubject?: boolean;
|
|
2623
2640
|
labels?: Partial<FeedbackDetailLabels>;
|
|
2624
2641
|
className?: string;
|
|
2625
2642
|
}
|
|
@@ -2646,7 +2663,7 @@ interface FeedbackRowDetailProps {
|
|
|
2646
2663
|
* collapsing the row closes it. Each editor is keyed by the row's id, so a draft is
|
|
2647
2664
|
* never thrown away by a list refresh under it.
|
|
2648
2665
|
*/
|
|
2649
|
-
declare function FeedbackRowDetail({ row, canEdit, viewerId, onUpdate, pending, onUpload, fetcher, statusChange, onCaptureScreenshot, reworkAttachment, renderDate, commit, labels: labelsProp, className, }: FeedbackRowDetailProps): react.JSX.Element;
|
|
2666
|
+
declare function FeedbackRowDetail({ row, canEdit, viewerId, onUpdate, pending, onUpload, fetcher, statusChange, onCaptureScreenshot, reworkAttachment, renderDate, commit, showSubject, labels: labelsProp, className, }: FeedbackRowDetailProps): react.JSX.Element;
|
|
2650
2667
|
|
|
2651
2668
|
/**
|
|
2652
2669
|
* The `confirmDialog` namespace of `<UiKitProvider labels>`: the two buttons' fallback
|
|
@@ -4609,8 +4626,10 @@ interface InvitationsPanelProps<R extends string = string> {
|
|
|
4609
4626
|
onInvite?: (draft: InvitationDraft<R>) => MaybePromise$1<InvitationSentAnswer | void>;
|
|
4610
4627
|
/** Mints a new link and mails it. Left out: no resend button. */
|
|
4611
4628
|
onResend?: (invitation: InvitationRow<R>) => MaybePromise$1<InvitationSentAnswer | void>;
|
|
4612
|
-
/** Revokes an open invitation (level `none`, §4.2: re-inviting undoes
|
|
4613
|
-
*
|
|
4629
|
+
/** Revokes an open or an expired invitation (level `none`, §4.2: re-inviting undoes
|
|
4630
|
+
* it). An expired row can be resent, and since 0.31 also removed: in an app that
|
|
4631
|
+
* keeps the rows it would otherwise stay listed for good (Kurvenschmiede's 0.30
|
|
4632
|
+
* adoption). Left out: no revoke button. */
|
|
4614
4633
|
onRevoke?: (invitation: InvitationRow<R>) => MaybePromise$1;
|
|
4615
4634
|
/** The list is being fetched. */
|
|
4616
4635
|
loading?: boolean;
|
|
@@ -4618,10 +4637,14 @@ interface InvitationsPanelProps<R extends string = string> {
|
|
|
4618
4637
|
describeError?: (error: unknown) => ReactNode | undefined;
|
|
4619
4638
|
/** Over the form: who can be invited and what happens, in the app's words. */
|
|
4620
4639
|
intro?: ReactNode;
|
|
4640
|
+
/** The list's visible heading. Default: the `listTitle` label. `null` leaves it out,
|
|
4641
|
+
* for a page whose card is already headed "Invitations" (Kurvenschmiede's 0.30
|
|
4642
|
+
* adoption); the list keeps the label as its accessible name either way. */
|
|
4643
|
+
listTitle?: ReactNode | null;
|
|
4621
4644
|
className?: string;
|
|
4622
4645
|
labels?: Partial<InvitationsLabels>;
|
|
4623
4646
|
}
|
|
4624
|
-
declare function InvitationsPanel<R extends string = string>({ invitations, roles, invitableRoles, defaultRole, scopes, scopeRequired, languages, defaultLocale, mailBackend, onInvite, onResend, onRevoke, loading, describeError, intro, className, labels: labelsProp, }: InvitationsPanelProps<R>): react.JSX.Element;
|
|
4647
|
+
declare function InvitationsPanel<R extends string = string>({ invitations, roles, invitableRoles, defaultRole, scopes, scopeRequired, languages, defaultLocale, mailBackend, onInvite, onResend, onRevoke, loading, describeError, intro, listTitle, className, labels: labelsProp, }: InvitationsPanelProps<R>): react.JSX.Element;
|
|
4625
4648
|
|
|
4626
4649
|
/**
|
|
4627
4650
|
* Who holds one record, and the owner's every way of changing that — Kurvenschmiede's
|
|
@@ -5224,6 +5247,12 @@ interface UiKitLabels {
|
|
|
5224
5247
|
transferOwnership: TransferOwnershipLabels;
|
|
5225
5248
|
/** 0.30.0: the invitations panel (§5). */
|
|
5226
5249
|
invitations: InvitationsLabels;
|
|
5250
|
+
/** 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §4.3). */
|
|
5251
|
+
settings: SettingsLabels;
|
|
5252
|
+
/** 0.31.0: the public landing page's kit words (docs/landing-demo-harmonization.md §4). */
|
|
5253
|
+
landing: LandingLabels;
|
|
5254
|
+
/** 0.31.0: the demo's start, banner and end (§5). */
|
|
5255
|
+
demo: DemoLabels;
|
|
5227
5256
|
/** 0.22.0: the screen-reader words of `Input` / `Textarea`'s `showCount` counter. */
|
|
5228
5257
|
characterCount: CharacterCountLabels;
|
|
5229
5258
|
/** 0.22.0: `CountrySelect`. Its list's "no results" and counts are `combobox`'s. */
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef } from 'react';
|
|
3
|
+
import { AccessChoice } from '../landing/access.js';
|
|
4
|
+
import { LandingLabels } from '../landing/landing-labels.js';
|
|
5
|
+
import { DemoExpiry } from './demo-countdown.js';
|
|
6
|
+
import { DemoLabels } from './demo-labels.js';
|
|
7
|
+
import { DemoModel } from './demo-session.js';
|
|
8
|
+
|
|
9
|
+
interface DemoBannerProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "onClick"> {
|
|
10
|
+
/** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from
|
|
11
|
+
* `/auth/me` (§5.3). */
|
|
12
|
+
expiresAt: DemoExpiry | null | undefined;
|
|
13
|
+
/** Which line the banner says about the data (§2.3). */
|
|
14
|
+
model: DemoModel;
|
|
15
|
+
/** "Request access". */
|
|
16
|
+
access: AccessChoice;
|
|
17
|
+
/** "Sign in" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default
|
|
18
|
+
* `/login`. */
|
|
19
|
+
signInHref?: string;
|
|
20
|
+
/** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */
|
|
21
|
+
onEnded?: () => void;
|
|
22
|
+
/** Where the collapsed state is kept for the tab's session. Default
|
|
23
|
+
* `kit.demoBanner.collapsed`. */
|
|
24
|
+
collapseKey?: string;
|
|
25
|
+
/** The clock, for tests and previews. Default `Date.now`. Keep it stable. */
|
|
26
|
+
now?: () => number;
|
|
27
|
+
labels?: Partial<DemoLabels>;
|
|
28
|
+
/** The access words (`landing` namespace) for this instance. */
|
|
29
|
+
landingLabels?: Partial<LandingLabels>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The demo's strip under the top bar, on every page of a demo session
|
|
33
|
+
* (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:
|
|
34
|
+
*
|
|
35
|
+
* - the time left, "Demo · 23 h 12 min left" — minutes only in the last hour, and the
|
|
36
|
+
* warning tone in the last ten minutes (`useDemoCountdown`);
|
|
37
|
+
* - one line on the model: R "You're looking at sample data. Changes aren't possible.",
|
|
38
|
+
* S "Your own work is deleted when the demo ends.";
|
|
39
|
+
* - "Request access" (the mail) and "Sign in".
|
|
40
|
+
*
|
|
41
|
+
* It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never
|
|
42
|
+
* hidden for good: it is a live state, like keksdose's preview banner, which a demo user
|
|
43
|
+
* gets instead of. At zero it calls `onEnded`.
|
|
44
|
+
*
|
|
45
|
+
* The countdown is no live region: a sentence read out every minute would drown the
|
|
46
|
+
* page. A screen-reader user meets it at the top of the page, and the end page says
|
|
47
|
+
* the rest.
|
|
48
|
+
*/
|
|
49
|
+
declare function DemoBanner({ expiresAt, model, access, signInHref, onEnded, collapseKey, now, labels: labelsProp, landingLabels, className, ...rest }: DemoBannerProps): react.JSX.Element;
|
|
50
|
+
|
|
51
|
+
export { DemoBanner, type DemoBannerProps };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ChevronDown, ChevronUp, Timer } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/cn.js";
|
|
6
|
+
import { AlertBanner } from "../components/alert-banner.js";
|
|
7
|
+
import { Button, IconButton } from "../components/ui.js";
|
|
8
|
+
import { useAccessAction } from "../landing/access.js";
|
|
9
|
+
import { useLandingLabels } from "../landing/landing-labels.js";
|
|
10
|
+
import { useDemoCountdown } from "./demo-countdown.js";
|
|
11
|
+
import { useDemoLabels } from "./demo-labels.js";
|
|
12
|
+
const COLLAPSE_KEY = "kit.demoBanner.collapsed";
|
|
13
|
+
function readSession(key) {
|
|
14
|
+
if (typeof window === "undefined") return null;
|
|
15
|
+
try {
|
|
16
|
+
return window.sessionStorage.getItem(key);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeSession(key, value) {
|
|
22
|
+
if (typeof window === "undefined") return;
|
|
23
|
+
try {
|
|
24
|
+
if (value === null) window.sessionStorage.removeItem(key);
|
|
25
|
+
else window.sessionStorage.setItem(key, value);
|
|
26
|
+
} catch {
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function timeText(countdown, labels) {
|
|
30
|
+
if (countdown.minutesLeft === null) return labels.badge;
|
|
31
|
+
if (countdown.ended) return labels.endedTitle;
|
|
32
|
+
return countdown.lastHour ? labels.minutesLeft(countdown.minutes) : labels.hoursLeft(countdown.hours, countdown.minutes);
|
|
33
|
+
}
|
|
34
|
+
function DemoBanner({
|
|
35
|
+
expiresAt,
|
|
36
|
+
model,
|
|
37
|
+
access,
|
|
38
|
+
signInHref = "/login",
|
|
39
|
+
onEnded,
|
|
40
|
+
collapseKey = COLLAPSE_KEY,
|
|
41
|
+
now,
|
|
42
|
+
labels: labelsProp,
|
|
43
|
+
landingLabels,
|
|
44
|
+
className,
|
|
45
|
+
...rest
|
|
46
|
+
}) {
|
|
47
|
+
const labels = useDemoLabels(labelsProp);
|
|
48
|
+
const landing = useLandingLabels(landingLabels);
|
|
49
|
+
const accessLink = useAccessAction(access, landingLabels);
|
|
50
|
+
const countdown = useDemoCountdown(expiresAt, { now, onEnded });
|
|
51
|
+
const [collapsed, setCollapsed] = useState(() => readSession(collapseKey) === "1");
|
|
52
|
+
const toggle = () => {
|
|
53
|
+
const next = !collapsed;
|
|
54
|
+
setCollapsed(next);
|
|
55
|
+
writeSession(collapseKey, next ? "1" : null);
|
|
56
|
+
};
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
AlertBanner,
|
|
59
|
+
{
|
|
60
|
+
...rest,
|
|
61
|
+
variant: "strip",
|
|
62
|
+
size: "sm",
|
|
63
|
+
tone: countdown.warning || countdown.ended ? "warning" : "info",
|
|
64
|
+
icon: /* @__PURE__ */ jsx(Timer, {}),
|
|
65
|
+
"data-demo-model": model,
|
|
66
|
+
className: cn("sm:text-sm", className),
|
|
67
|
+
action: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
68
|
+
!collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "brand", size: "sm", children: accessLink.label }),
|
|
70
|
+
/* @__PURE__ */ jsx(Button, { href: signInHref, variant: "secondary", size: "sm", children: landing.signIn })
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
IconButton,
|
|
74
|
+
{
|
|
75
|
+
size: "2xs",
|
|
76
|
+
variant: "ghost",
|
|
77
|
+
label: labels.details,
|
|
78
|
+
"aria-expanded": !collapsed,
|
|
79
|
+
onClick: toggle,
|
|
80
|
+
children: collapsed ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {})
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] }),
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold", children: timeText(countdown, labels) }),
|
|
86
|
+
!collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
87
|
+
" ",
|
|
88
|
+
/* @__PURE__ */ jsx("span", { children: model === "read-only" ? labels.readOnly : labels.sandbox })
|
|
89
|
+
] })
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
DemoBanner
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=demo-banner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/demo/demo-banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { ChevronDown, ChevronUp, Timer } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button, IconButton } from \"../components/ui\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoCountdown } from \"./demo-countdown\";\nimport type { DemoCountdown, DemoExpiry } from \"./demo-countdown\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\n\n/** The default `sessionStorage` key of the collapsed state. */\nconst COLLAPSE_KEY = \"kit.demoBanner.collapsed\";\n\n/** `sessionStorage`, guarded like `lib/safe-storage`'s `localStorage`: blocked storage\n * reads as \"not set\" and writes nothing. */\nfunction readSession(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeSession(key: string, value: string | null): void {\n if (typeof window === \"undefined\") return;\n try {\n if (value === null) window.sessionStorage.removeItem(key);\n else window.sessionStorage.setItem(key, value);\n } catch {\n // storage blocked: the banner just opens again next time\n }\n}\n\n/** \"Demo · 23 h 12 min left\", \"Demo · 50 min left\", the end, or plain \"Demo\". */\nfunction timeText(countdown: DemoCountdown, labels: DemoLabels): string {\n if (countdown.minutesLeft === null) return labels.badge;\n if (countdown.ended) return labels.endedTitle;\n return countdown.lastHour\n ? labels.minutesLeft(countdown.minutes)\n : labels.hoursLeft(countdown.hours, countdown.minutes);\n}\n\nexport interface DemoBannerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"onClick\"> {\n /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from\n * `/auth/me` (§5.3). */\n expiresAt: DemoExpiry | null | undefined;\n /** Which line the banner says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Sign in\" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default\n * `/login`. */\n signInHref?: string;\n /** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n /** Where the collapsed state is kept for the tab's session. Default\n * `kit.demoBanner.collapsed`. */\n collapseKey?: string;\n /** The clock, for tests and previews. Default `Date.now`. Keep it stable. */\n now?: () => number;\n labels?: Partial<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The demo's strip under the top bar, on every page of a demo session\n * (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:\n *\n * - the time left, \"Demo · 23 h 12 min left\" — minutes only in the last hour, and the\n * warning tone in the last ten minutes (`useDemoCountdown`);\n * - one line on the model: R \"You're looking at sample data. Changes aren't possible.\",\n * S \"Your own work is deleted when the demo ends.\";\n * - \"Request access\" (the mail) and \"Sign in\".\n *\n * It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never\n * hidden for good: it is a live state, like keksdose's preview banner, which a demo user\n * gets instead of. At zero it calls `onEnded`.\n *\n * The countdown is no live region: a sentence read out every minute would drown the\n * page. A screen-reader user meets it at the top of the page, and the end page says\n * the rest.\n */\nexport function DemoBanner({\n expiresAt,\n model,\n access,\n signInHref = \"/login\",\n onEnded,\n collapseKey = COLLAPSE_KEY,\n now,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoBannerProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n const countdown = useDemoCountdown(expiresAt, { now, onEnded });\n const [collapsed, setCollapsed] = useState(() => readSession(collapseKey) === \"1\");\n\n const toggle = () => {\n const next = !collapsed;\n setCollapsed(next);\n writeSession(collapseKey, next ? \"1\" : null);\n };\n\n return (\n <AlertBanner\n {...rest}\n variant=\"strip\"\n size=\"sm\"\n tone={countdown.warning || countdown.ended ? \"warning\" : \"info\"}\n icon={<Timer />}\n data-demo-model={model}\n className={cn(\"sm:text-sm\", className)}\n action={\n <>\n {!collapsed && (\n <>\n <Button href={accessLink.href} variant=\"brand\" size=\"sm\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\" size=\"sm\">\n {landing.signIn}\n </Button>\n </>\n )}\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n label={labels.details}\n aria-expanded={!collapsed}\n onClick={toggle}\n >\n {collapsed ? <ChevronDown /> : <ChevronUp />}\n </IconButton>\n </>\n }\n >\n <span className=\"font-semibold\">{timeText(countdown, labels)}</span>\n {!collapsed && (\n <>\n {\" \"}\n <span>{model === \"read-only\" ? labels.readOnly : labels.sandbox}</span>\n </>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AA0HY,SAMA,UANA,KAMA,YANA;AA1HZ,SAAS,gBAAgB;AAEzB,SAAS,aAAa,WAAW,aAAa;AAE9C,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,kBAAkB;AACnC,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAK9B,MAAM,eAAe;AAIrB,SAAS,YAAY,KAA4B;AAC/C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,eAAe,QAAQ,GAAG;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,KAAa,OAA4B;AAC7D,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,QAAI,UAAU,KAAM,QAAO,eAAe,WAAW,GAAG;AAAA,QACnD,QAAO,eAAe,QAAQ,KAAK,KAAK;AAAA,EAC/C,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,WAA0B,QAA4B;AACtE,MAAI,UAAU,gBAAgB,KAAM,QAAO,OAAO;AAClD,MAAI,UAAU,MAAO,QAAO,OAAO;AACnC,SAAO,UAAU,WACb,OAAO,YAAY,UAAU,OAAO,IACpC,OAAO,UAAU,UAAU,OAAO,UAAU,OAAO;AACzD;AA2CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,YAAY,iBAAiB,WAAW,EAAE,KAAK,QAAQ,CAAC;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM,YAAY,WAAW,MAAM,GAAG;AAEjF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,iBAAa,IAAI;AACjB,iBAAa,aAAa,OAAO,MAAM,IAAI;AAAA,EAC7C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,UAAU,WAAW,UAAU,QAAQ,YAAY;AAAA,MACzD,MAAM,oBAAC,SAAM;AAAA,MACb,mBAAiB;AAAA,MACjB,WAAW,GAAG,cAAc,SAAS;AAAA,MACrC,QACE,iCACG;AAAA,SAAC,aACA,iCACE;AAAA,8BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SAAQ,MAAK,MACjD,qBAAW,OACd;AAAA,UACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,kBAAQ,QACX;AAAA,WACF;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,OAAO;AAAA,YACd,iBAAe,CAAC;AAAA,YAChB,SAAS;AAAA,YAER,sBAAY,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,MAGF;AAAA,4BAAC,UAAK,WAAU,iBAAiB,mBAAS,WAAW,MAAM,GAAE;AAAA,QAC5D,CAAC,aACA,iCACG;AAAA;AAAA,UACD,oBAAC,UAAM,oBAAU,cAAc,OAAO,WAAW,OAAO,SAAQ;AAAA,WAClE;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to
|
|
3
|
+
* the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`
|
|
4
|
+
* (`/auth/me`). The client never decodes the JWT.
|
|
5
|
+
*/
|
|
6
|
+
/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a
|
|
7
|
+
* `Date`. */
|
|
8
|
+
type DemoExpiry = string | number | Date;
|
|
9
|
+
/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */
|
|
10
|
+
declare const DEMO_WARNING_MINUTES = 10;
|
|
11
|
+
/** Below this many minutes the countdown shows minutes only (§5.4). */
|
|
12
|
+
declare const DEMO_MINUTES_ONLY = 60;
|
|
13
|
+
interface DemoCountdown {
|
|
14
|
+
/**
|
|
15
|
+
* Whole minutes left, ROUNDED UP — 30 s left is "1 min", and 0 only once the demo has
|
|
16
|
+
* ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and
|
|
17
|
+
* never ends by itself (a parse slip must not throw a visitor out).
|
|
18
|
+
*/
|
|
19
|
+
minutesLeft: number | null;
|
|
20
|
+
/** The display split: whole hours, and the minutes past them. In the last hour
|
|
21
|
+
* (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */
|
|
22
|
+
hours: number;
|
|
23
|
+
minutes: number;
|
|
24
|
+
/** `minutesLeft <= 60`: show minutes only. */
|
|
25
|
+
lastHour: boolean;
|
|
26
|
+
/** `minutesLeft <= 10`: the warning tone. */
|
|
27
|
+
warning: boolean;
|
|
28
|
+
/** The demo's time is up. */
|
|
29
|
+
ended: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The countdown at `now` — pure, for a test or a server-rendered value. See
|
|
33
|
+
* {@link DemoCountdown} for the rounding and the thresholds.
|
|
34
|
+
*/
|
|
35
|
+
declare function demoCountdown(expiresAt: DemoExpiry | null | undefined, now: number): DemoCountdown;
|
|
36
|
+
interface DemoCountdownOptions {
|
|
37
|
+
/**
|
|
38
|
+
* The clock, in epoch milliseconds. Default `Date.now`. A test passes its own (or uses
|
|
39
|
+
* fake timers, which move `Date.now` too). Keep it a stable function — a new one each
|
|
40
|
+
* render re-arms the timer each render.
|
|
41
|
+
*/
|
|
42
|
+
now?: () => number;
|
|
43
|
+
/** Called once when the countdown reaches zero — also on mount, if it already has.
|
|
44
|
+
* The app clears the session and replaces to `/demo/ended` (§5.5). */
|
|
45
|
+
onEnded?: () => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The time left to `expiresAt`, re-rendering when the shown minute changes — one timer
|
|
49
|
+
* to the next minute boundary, not a tick a second — and once more when the tab comes
|
|
50
|
+
* back from the background (a hidden tab's timers are throttled).
|
|
51
|
+
*
|
|
52
|
+
* `onEnded` fires at zero (§5.5). The countdown is the client's, from `expires_at`; the
|
|
53
|
+
* server's answer to a request after the end (a 401) ends the demo as well — the app's
|
|
54
|
+
* 401 handler, not this hook.
|
|
55
|
+
*/
|
|
56
|
+
declare function useDemoCountdown(expiresAt: DemoExpiry | null | undefined, { now, onEnded }?: DemoCountdownOptions): DemoCountdown;
|
|
57
|
+
|
|
58
|
+
export { DEMO_MINUTES_ONLY, DEMO_WARNING_MINUTES, type DemoCountdown, type DemoCountdownOptions, type DemoExpiry, demoCountdown, useDemoCountdown };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from "react";
|
|
3
|
+
const DEMO_WARNING_MINUTES = 10;
|
|
4
|
+
const DEMO_MINUTES_ONLY = 60;
|
|
5
|
+
const MINUTE = 6e4;
|
|
6
|
+
function expiryMs(expiresAt) {
|
|
7
|
+
if (expiresAt === null || expiresAt === void 0) return Number.NaN;
|
|
8
|
+
if (typeof expiresAt === "number") return expiresAt;
|
|
9
|
+
if (expiresAt instanceof Date) return expiresAt.getTime();
|
|
10
|
+
return Date.parse(expiresAt);
|
|
11
|
+
}
|
|
12
|
+
function minutesAt(expiry, now) {
|
|
13
|
+
if (!Number.isFinite(expiry)) return null;
|
|
14
|
+
return Math.max(0, Math.ceil((expiry - now) / MINUTE));
|
|
15
|
+
}
|
|
16
|
+
function demoCountdown(expiresAt, now) {
|
|
17
|
+
return countdownOf(minutesAt(expiryMs(expiresAt), now));
|
|
18
|
+
}
|
|
19
|
+
function countdownOf(minutesLeft) {
|
|
20
|
+
if (minutesLeft === null) {
|
|
21
|
+
return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };
|
|
22
|
+
}
|
|
23
|
+
const lastHour = minutesLeft <= DEMO_MINUTES_ONLY;
|
|
24
|
+
return {
|
|
25
|
+
minutesLeft,
|
|
26
|
+
hours: lastHour ? 0 : Math.floor(minutesLeft / 60),
|
|
27
|
+
minutes: lastHour ? minutesLeft : minutesLeft % 60,
|
|
28
|
+
lastHour,
|
|
29
|
+
warning: minutesLeft <= DEMO_WARNING_MINUTES,
|
|
30
|
+
ended: minutesLeft === 0
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function useDemoCountdown(expiresAt, { now = Date.now, onEnded } = {}) {
|
|
34
|
+
const expiry = expiryMs(expiresAt);
|
|
35
|
+
const subscribe = useCallback(
|
|
36
|
+
(notify) => {
|
|
37
|
+
if (!Number.isFinite(expiry)) return () => {
|
|
38
|
+
};
|
|
39
|
+
let timer;
|
|
40
|
+
const arm = () => {
|
|
41
|
+
const left = expiry - now();
|
|
42
|
+
if (left <= 0) return;
|
|
43
|
+
const delay = left - (Math.ceil(left / MINUTE) - 1) * MINUTE;
|
|
44
|
+
timer = setTimeout(() => {
|
|
45
|
+
notify();
|
|
46
|
+
arm();
|
|
47
|
+
}, delay);
|
|
48
|
+
};
|
|
49
|
+
const onVisible = () => {
|
|
50
|
+
if (document.visibilityState !== "visible") return;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
notify();
|
|
53
|
+
arm();
|
|
54
|
+
};
|
|
55
|
+
arm();
|
|
56
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
57
|
+
return () => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
[expiry, now]
|
|
63
|
+
);
|
|
64
|
+
const snapshot = () => minutesAt(expiry, now());
|
|
65
|
+
const minutesLeft = useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
66
|
+
const countdown = countdownOf(minutesLeft);
|
|
67
|
+
const fireEnded = useEffectEvent(() => onEnded?.());
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (countdown.ended) fireEnded();
|
|
70
|
+
}, [countdown.ended]);
|
|
71
|
+
return countdown;
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
DEMO_MINUTES_ONLY,
|
|
75
|
+
DEMO_WARNING_MINUTES,
|
|
76
|
+
demoCountdown,
|
|
77
|
+
useDemoCountdown
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=demo-countdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/demo/demo-countdown.ts"],"sourcesContent":["import { useCallback, useEffect, useEffectEvent, useSyncExternalStore } from \"react\";\n\n/**\n * The demo's countdown (docs/landing-demo-harmonization.md §5.3, §5.4): the time left to\n * the demo account's end, from `expires_at` (the `TokenResponse`) or `demo_expires_at`\n * (`/auth/me`). The client never decodes the JWT.\n */\n\n/** When the demo ends: an ISO timestamp from the server, epoch milliseconds, or a\n * `Date`. */\nexport type DemoExpiry = string | number | Date;\n\n/** The last stretch, in minutes, in which the banner turns to the warning tone (§5.4). */\nexport const DEMO_WARNING_MINUTES = 10;\n/** Below this many minutes the countdown shows minutes only (§5.4). */\nexport const DEMO_MINUTES_ONLY = 60;\n\nconst MINUTE = 60_000;\n\nexport interface DemoCountdown {\n /**\n * Whole minutes left, ROUNDED UP — 30 s left is \"1 min\", and 0 only once the demo has\n * ended. `null` when `expiresAt` cannot be read: the countdown then shows no time and\n * never ends by itself (a parse slip must not throw a visitor out).\n */\n minutesLeft: number | null;\n /** The display split: whole hours, and the minutes past them. In the last hour\n * (`lastHour`) `hours` is 0 and `minutes` is `minutesLeft`. */\n hours: number;\n minutes: number;\n /** `minutesLeft <= 60`: show minutes only. */\n lastHour: boolean;\n /** `minutesLeft <= 10`: the warning tone. */\n warning: boolean;\n /** The demo's time is up. */\n ended: boolean;\n}\n\n/** `expiresAt` as epoch milliseconds, or NaN. */\nfunction expiryMs(expiresAt: DemoExpiry | null | undefined): number {\n if (expiresAt === null || expiresAt === undefined) return Number.NaN;\n if (typeof expiresAt === \"number\") return expiresAt;\n if (expiresAt instanceof Date) return expiresAt.getTime();\n return Date.parse(expiresAt);\n}\n\n/** Whole minutes left at `now`, rounded up, never below 0; null for an unreadable end. */\nfunction minutesAt(expiry: number, now: number): number | null {\n if (!Number.isFinite(expiry)) return null;\n return Math.max(0, Math.ceil((expiry - now) / MINUTE));\n}\n\n/**\n * The countdown at `now` — pure, for a test or a server-rendered value. See\n * {@link DemoCountdown} for the rounding and the thresholds.\n */\nexport function demoCountdown(expiresAt: DemoExpiry | null | undefined, now: number): DemoCountdown {\n return countdownOf(minutesAt(expiryMs(expiresAt), now));\n}\n\nfunction countdownOf(minutesLeft: number | null): DemoCountdown {\n if (minutesLeft === null) {\n return { minutesLeft, hours: 0, minutes: 0, lastHour: false, warning: false, ended: false };\n }\n const lastHour = minutesLeft <= DEMO_MINUTES_ONLY;\n return {\n minutesLeft,\n hours: lastHour ? 0 : Math.floor(minutesLeft / 60),\n minutes: lastHour ? minutesLeft : minutesLeft % 60,\n lastHour,\n warning: minutesLeft <= DEMO_WARNING_MINUTES,\n ended: minutesLeft === 0,\n };\n}\n\nexport interface DemoCountdownOptions {\n /**\n * The clock, in epoch milliseconds. Default `Date.now`. A test passes its own (or uses\n * fake timers, which move `Date.now` too). Keep it a stable function — a new one each\n * render re-arms the timer each render.\n */\n now?: () => number;\n /** Called once when the countdown reaches zero — also on mount, if it already has.\n * The app clears the session and replaces to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n}\n\n/**\n * The time left to `expiresAt`, re-rendering when the shown minute changes — one timer\n * to the next minute boundary, not a tick a second — and once more when the tab comes\n * back from the background (a hidden tab's timers are throttled).\n *\n * `onEnded` fires at zero (§5.5). The countdown is the client's, from `expires_at`; the\n * server's answer to a request after the end (a 401) ends the demo as well — the app's\n * 401 handler, not this hook.\n */\nexport function useDemoCountdown(\n expiresAt: DemoExpiry | null | undefined,\n { now = Date.now, onEnded }: DemoCountdownOptions = {},\n): DemoCountdown {\n const expiry = expiryMs(expiresAt);\n const subscribe = useCallback(\n (notify: () => void) => {\n if (!Number.isFinite(expiry)) return () => {};\n let timer: ReturnType<typeof setTimeout> | undefined;\n const arm = () => {\n const left = expiry - now();\n if (left <= 0) return;\n // The rounded-up minute changes when `left` crosses the next whole minute below.\n const delay = left - (Math.ceil(left / MINUTE) - 1) * MINUTE;\n timer = setTimeout(() => {\n notify();\n arm();\n }, delay);\n };\n const onVisible = () => {\n if (document.visibilityState !== \"visible\") return;\n clearTimeout(timer);\n notify();\n arm();\n };\n arm();\n document.addEventListener(\"visibilitychange\", onVisible);\n return () => {\n clearTimeout(timer);\n document.removeEventListener(\"visibilitychange\", onVisible);\n };\n },\n [expiry, now],\n );\n const snapshot = () => minutesAt(expiry, now());\n const minutesLeft = useSyncExternalStore(subscribe, snapshot, snapshot);\n const countdown = countdownOf(minutesLeft);\n\n const fireEnded = useEffectEvent(() => onEnded?.());\n useEffect(() => {\n if (countdown.ended) fireEnded();\n }, [countdown.ended]);\n\n return countdown;\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,gBAAgB,4BAA4B;AAatE,MAAM,uBAAuB;AAE7B,MAAM,oBAAoB;AAEjC,MAAM,SAAS;AAsBf,SAAS,SAAS,WAAkD;AAClE,MAAI,cAAc,QAAQ,cAAc,OAAW,QAAO,OAAO;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO;AAC1C,MAAI,qBAAqB,KAAM,QAAO,UAAU,QAAQ;AACxD,SAAO,KAAK,MAAM,SAAS;AAC7B;AAGA,SAAS,UAAU,QAAgB,KAA4B;AAC7D,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACrC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,OAAO,MAAM,CAAC;AACvD;AAMO,SAAS,cAAc,WAA0C,KAA4B;AAClG,SAAO,YAAY,UAAU,SAAS,SAAS,GAAG,GAAG,CAAC;AACxD;AAEA,SAAS,YAAY,aAA2C;AAC9D,MAAI,gBAAgB,MAAM;AACxB,WAAO,EAAE,aAAa,OAAO,GAAG,SAAS,GAAG,UAAU,OAAO,SAAS,OAAO,OAAO,MAAM;AAAA,EAC5F;AACA,QAAM,WAAW,eAAe;AAChC,SAAO;AAAA,IACL;AAAA,IACA,OAAO,WAAW,IAAI,KAAK,MAAM,cAAc,EAAE;AAAA,IACjD,SAAS,WAAW,cAAc,cAAc;AAAA,IAChD;AAAA,IACA,SAAS,eAAe;AAAA,IACxB,OAAO,gBAAgB;AAAA,EACzB;AACF;AAuBO,SAAS,iBACd,WACA,EAAE,MAAM,KAAK,KAAK,QAAQ,IAA0B,CAAC,GACtC;AACf,QAAM,SAAS,SAAS,SAAS;AACjC,QAAM,YAAY;AAAA,IAChB,CAAC,WAAuB;AACtB,UAAI,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO,MAAM;AAAA,MAAC;AAC5C,UAAI;AACJ,YAAM,MAAM,MAAM;AAChB,cAAM,OAAO,SAAS,IAAI;AAC1B,YAAI,QAAQ,EAAG;AAEf,cAAM,QAAQ,QAAQ,KAAK,KAAK,OAAO,MAAM,IAAI,KAAK;AACtD,gBAAQ,WAAW,MAAM;AACvB,iBAAO;AACP,cAAI;AAAA,QACN,GAAG,KAAK;AAAA,MACV;AACA,YAAM,YAAY,MAAM;AACtB,YAAI,SAAS,oBAAoB,UAAW;AAC5C,qBAAa,KAAK;AAClB,eAAO;AACP,YAAI;AAAA,MACN;AACA,UAAI;AACJ,eAAS,iBAAiB,oBAAoB,SAAS;AACvD,aAAO,MAAM;AACX,qBAAa,KAAK;AAClB,iBAAS,oBAAoB,oBAAoB,SAAS;AAAA,MAC5D;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,GAAG;AAAA,EACd;AACA,QAAM,WAAW,MAAM,UAAU,QAAQ,IAAI,CAAC;AAC9C,QAAM,cAAc,qBAAqB,WAAW,UAAU,QAAQ;AACtE,QAAM,YAAY,YAAY,WAAW;AAEzC,QAAM,YAAY,eAAe,MAAM,UAAU,CAAC;AAClD,YAAU,MAAM;AACd,QAAI,UAAU,MAAO,WAAU;AAAA,EACjC,GAAG,CAAC,UAAU,KAAK,CAAC;AAEpB,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
3
|
+
import { AuthHeadingLevel } from '../auth/status-parts.js';
|
|
4
|
+
import { AccessChoice } from '../landing/access.js';
|
|
5
|
+
import { LandingLabels } from '../landing/landing-labels.js';
|
|
6
|
+
import { DemoLabels } from './demo-labels.js';
|
|
7
|
+
import { DemoModel } from './demo-session.js';
|
|
8
|
+
|
|
9
|
+
interface DemoEndedProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "title"> {
|
|
10
|
+
/** Which line the page says about the data (§2.3). */
|
|
11
|
+
model: DemoModel;
|
|
12
|
+
/** "Request access". */
|
|
13
|
+
access: AccessChoice;
|
|
14
|
+
/** "Start a new demo". Default `/demo` — a new demo may start after one ends, within
|
|
15
|
+
* the per-IP window (§2.7). */
|
|
16
|
+
restartHref?: string;
|
|
17
|
+
/** "Sign in". Default `/login`. */
|
|
18
|
+
signInHref?: string;
|
|
19
|
+
/** App content under the line. */
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
/** `false` adds no `noindex` — for a preview. Default `true` (§4.3). */
|
|
22
|
+
noIndex?: boolean;
|
|
23
|
+
/** Default `h2`, under `AuthLayout`'s `h1`. */
|
|
24
|
+
headingAs?: AuthHeadingLevel;
|
|
25
|
+
labels?: Partial<DemoLabels>;
|
|
26
|
+
/** The access words (`landing` namespace) for this instance. */
|
|
27
|
+
landingLabels?: Partial<LandingLabels>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The `/demo/ended` page's content (docs/landing-demo-harmonization.md §5.5): "The demo
|
|
31
|
+
* has ended", one line on what became of the data, and three ways on — "Start a new
|
|
32
|
+
* demo", "Request access", "Sign in".
|
|
33
|
+
*
|
|
34
|
+
* Where a demo lands at `expires_at`, or on ANY 401 while the session is a demo: the
|
|
35
|
+
* client clears the session and replaces to `/demo/ended`, never to `/login`. keksdose's
|
|
36
|
+
* expired demo met an unexplained sign-in form. The server deletes the user at the next
|
|
37
|
+
* reap; nothing is kept.
|
|
38
|
+
*
|
|
39
|
+
* Content, like `NotFoundPage`: put it in the app's `AuthLayout` (its logo, language
|
|
40
|
+
* menu and legal footer). It marks the page `noindex`; robots.txt must NOT disallow it,
|
|
41
|
+
* or a crawler could never read that (§10.8).
|
|
42
|
+
*/
|
|
43
|
+
declare function DemoEnded({ model, access, restartHref, signInHref, children, noIndex, headingAs: Heading, labels: labelsProp, landingLabels, className, ...rest }: DemoEndedProps): react.JSX.Element;
|
|
44
|
+
|
|
45
|
+
export { DemoEnded, type DemoEndedProps };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Hourglass } from "lucide-react";
|
|
4
|
+
import { cn } from "../lib/cn.js";
|
|
5
|
+
import { Button } from "../components/ui.js";
|
|
6
|
+
import { useNoIndex } from "../hooks/use-noindex.js";
|
|
7
|
+
import { useAccessAction } from "../landing/access.js";
|
|
8
|
+
import { useLandingLabels } from "../landing/landing-labels.js";
|
|
9
|
+
import { useDemoLabels } from "./demo-labels.js";
|
|
10
|
+
function DemoEnded({
|
|
11
|
+
model,
|
|
12
|
+
access,
|
|
13
|
+
restartHref = "/demo",
|
|
14
|
+
signInHref = "/login",
|
|
15
|
+
children,
|
|
16
|
+
noIndex = true,
|
|
17
|
+
headingAs: Heading = "h2",
|
|
18
|
+
labels: labelsProp,
|
|
19
|
+
landingLabels,
|
|
20
|
+
className,
|
|
21
|
+
...rest
|
|
22
|
+
}) {
|
|
23
|
+
const labels = useDemoLabels(labelsProp);
|
|
24
|
+
const landing = useLandingLabels(landingLabels);
|
|
25
|
+
const accessLink = useAccessAction(access, landingLabels);
|
|
26
|
+
useNoIndex(noIndex);
|
|
27
|
+
return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("flex flex-col items-center gap-2 py-4 text-center", className), children: [
|
|
28
|
+
/* @__PURE__ */ jsx(Hourglass, { "aria-hidden": true, className: "size-10 text-[var(--text-placeholder)]" }),
|
|
29
|
+
/* @__PURE__ */ jsx(Heading, { className: "mt-2 text-xl font-semibold text-[var(--text-primary)]", children: labels.endedTitle }),
|
|
30
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-prose text-sm text-[var(--text-secondary)]", children: model === "sandbox" ? labels.endedSandbox : labels.endedReadOnly }),
|
|
31
|
+
children,
|
|
32
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-4 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:justify-center", children: [
|
|
33
|
+
/* @__PURE__ */ jsx(Button, { href: restartHref, variant: "brand", children: labels.restart }),
|
|
34
|
+
/* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "secondary", children: accessLink.label }),
|
|
35
|
+
/* @__PURE__ */ jsx(Button, { href: signInHref, variant: "secondary", children: landing.signIn })
|
|
36
|
+
] })
|
|
37
|
+
] });
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
DemoEnded
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=demo-ended.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/demo/demo-ended.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { Hourglass } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/ui\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport type { AuthHeadingLevel } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\n\nexport interface DemoEndedProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"title\"> {\n /** Which line the page says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Start a new demo\". Default `/demo` — a new demo may start after one ends, within\n * the per-IP window (§2.7). */\n restartHref?: string;\n /** \"Sign in\". Default `/login`. */\n signInHref?: string;\n /** App content under the line. */\n children?: ReactNode;\n /** `false` adds no `noindex` — for a preview. Default `true` (§4.3). */\n noIndex?: boolean;\n /** Default `h2`, under `AuthLayout`'s `h1`. */\n headingAs?: AuthHeadingLevel;\n labels?: Partial<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo/ended` page's content (docs/landing-demo-harmonization.md §5.5): \"The demo\n * has ended\", one line on what became of the data, and three ways on — \"Start a new\n * demo\", \"Request access\", \"Sign in\".\n *\n * Where a demo lands at `expires_at`, or on ANY 401 while the session is a demo: the\n * client clears the session and replaces to `/demo/ended`, never to `/login`. keksdose's\n * expired demo met an unexplained sign-in form. The server deletes the user at the next\n * reap; nothing is kept.\n *\n * Content, like `NotFoundPage`: put it in the app's `AuthLayout` (its logo, language\n * menu and legal footer). It marks the page `noindex`; robots.txt must NOT disallow it,\n * or a crawler could never read that (§10.8).\n */\nexport function DemoEnded({\n model,\n access,\n restartHref = \"/demo\",\n signInHref = \"/login\",\n children,\n noIndex = true,\n headingAs: Heading = \"h2\",\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoEndedProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n useNoIndex(noIndex);\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-2 py-4 text-center\", className)}>\n <Hourglass aria-hidden className=\"size-10 text-[var(--text-placeholder)]\" />\n <Heading className=\"mt-2 text-xl font-semibold text-[var(--text-primary)]\">{labels.endedTitle}</Heading>\n <p className=\"max-w-prose text-sm text-[var(--text-secondary)]\">\n {model === \"sandbox\" ? labels.endedSandbox : labels.endedReadOnly}\n </p>\n {children}\n <div className=\"mt-4 flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:flex-wrap sm:justify-center\">\n <Button href={restartHref} variant=\"brand\">\n {labels.restart}\n </Button>\n <Button href={accessLink.href} variant=\"secondary\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\">\n {landing.signIn}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":";AAqEM,cAMA,YANA;AApEN,SAAS,iBAAiB;AAE1B,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAE3B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAuCvB,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,aAAa;AAAA,EACb;AAAA,EACA,UAAU;AAAA,EACV,WAAW,UAAU;AAAA,EACrB,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,aAAW,OAAO;AAClB,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GACzF;AAAA,wBAAC,aAAU,eAAW,MAAC,WAAU,0CAAyC;AAAA,IAC1E,oBAAC,WAAQ,WAAU,yDAAyD,iBAAO,YAAW;AAAA,IAC9F,oBAAC,OAAE,WAAU,oDACV,oBAAU,YAAY,OAAO,eAAe,OAAO,eACtD;AAAA,IACC;AAAA,IACD,qBAAC,SAAI,WAAU,wFACb;AAAA,0BAAC,UAAO,MAAM,aAAa,SAAQ,SAChC,iBAAO,SACV;AAAA,MACA,oBAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,aACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,kBAAQ,QACX;AAAA,OACF;AAAA,KACF;AAEJ;","names":[]}
|