@eifi1/ui-kit 0.32.1 → 0.33.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 +16 -8
- package/dist/account/delete-account-setting.d.ts +10 -10
- package/dist/account/delete-account-setting.js +2 -1
- package/dist/account/delete-account-setting.js.map +1 -1
- package/dist/account/email-change-setting.d.ts +5 -0
- package/dist/account/email-change-setting.js +4 -3
- package/dist/account/email-change-setting.js.map +1 -1
- package/dist/account/sessions-setting.d.ts +4 -0
- 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 +10 -10
- package/dist/admin/admin-action-confirm.js.map +1 -1
- package/dist/admin/invitations-panel.d.ts +10 -10
- package/dist/admin/invitations-panel.js +4 -3
- package/dist/admin/invitations-panel.js.map +1 -1
- package/dist/admin/plan-change.d.ts +100 -0
- package/dist/admin/plan-change.js +208 -0
- package/dist/admin/plan-change.js.map +1 -0
- package/dist/admin/reviewer-scope-editor.d.ts +10 -10
- package/dist/admin/reviewer-scope-editor.js.map +1 -1
- package/dist/admin/role-select.d.ts +7 -7
- package/dist/admin/role-select.js.map +1 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +10 -10
- package/dist/admin/user-roster.d.ts +7 -7
- package/dist/admin/user-roster.js +2 -2
- package/dist/admin/user-roster.js.map +1 -1
- package/dist/auth/auth-errors.d.ts +13 -2
- package/dist/auth/auth-errors.js +5 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/auth/complete-name-dialog.d.ts +4 -1
- package/dist/auth/complete-name-dialog.js +2 -1
- package/dist/auth/complete-name-dialog.js.map +1 -1
- package/dist/auth/register-form.d.ts +10 -10
- package/dist/billing/billing-banner.d.ts +13 -11
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +10 -10
- package/dist/billing/billing-labels.js +10 -1
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-lock.d.ts +39 -12
- package/dist/billing/billing-lock.js +9 -3
- package/dist/billing/billing-lock.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +154 -0
- package/dist/billing/billing-overview.js +2 -0
- package/dist/billing/billing-overview.js.map +1 -0
- package/dist/billing/billing-standing.d.ts +10 -10
- package/dist/billing/checkout-processing.d.ts +217 -0
- package/dist/billing/checkout-processing.js +198 -0
- package/dist/billing/checkout-processing.js.map +1 -0
- package/dist/billing/checkout-return.d.ts +51 -0
- package/dist/billing/checkout-return.js +43 -0
- package/dist/billing/checkout-return.js.map +1 -0
- package/dist/billing/pay-page.d.ts +210 -0
- package/dist/billing/pay-page.js +113 -0
- package/dist/billing/pay-page.js.map +1 -0
- package/dist/billing/plan-card.d.ts +10 -10
- package/dist/billing/plan-limit-notice.d.ts +10 -10
- package/dist/billing/plan-limit-toast.d.ts +143 -0
- package/dist/billing/plan-limit-toast.js +40 -0
- package/dist/billing/plan-limit-toast.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +38 -15
- package/dist/billing/subscription-actions.js +9 -2
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/billing/subscription-status-chip.d.ts +9 -9
- package/dist/bin/eifi1-pay-page.mjs +166 -0
- package/dist/chart.d.ts +10 -10
- package/dist/components/account-chips.d.ts +6 -1
- package/dist/components/account-settings.d.ts +4 -1
- package/dist/components/account-settings.js +2 -1
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +10 -10
- package/dist/components/autocomplete.d.ts +2 -1
- package/dist/components/autocomplete.js +3 -1
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +10 -10
- package/dist/components/button-group.js +1 -1
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +10 -10
- package/dist/components/calculator.js +3 -3
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/checkbox-group.d.ts +2 -1
- package/dist/components/checkbox-group.js.map +1 -1
- package/dist/components/checkbox.d.ts +2 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/chip.d.ts +2 -1
- package/dist/components/chip.js +19 -17
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +3 -1
- package/dist/components/choice-card.js +1 -1
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox.d.ts +4 -3
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +10 -10
- package/dist/components/confirm-dialog.js +6 -3
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/copy-button.d.ts +11 -11
- package/dist/components/country-select.d.ts +2 -1
- package/dist/components/country-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +10 -10
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-cells.d.ts +12 -1
- package/dist/components/data-table-filter-popover.d.ts +12 -1
- package/dist/components/data-table-filters.d.ts +12 -1
- package/dist/components/data-table-labels.d.ts +12 -1
- package/dist/components/data-table-pagination.d.ts +12 -1
- package/dist/components/data-table.d.ts +12 -1
- package/dist/components/data-table.js +3 -3
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +6 -4
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +3 -2
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/facing-pair.d.ts +10 -10
- package/dist/components/field-parts.d.ts +32 -4
- package/dist/components/field-parts.js +54 -19
- package/dist/components/field-parts.js.map +1 -1
- package/dist/components/file-button.d.ts +10 -10
- package/dist/components/file-button.js +3 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -11
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.js +5 -5
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/form-actions.d.ts +9 -9
- package/dist/components/form-actions.js +3 -3
- package/dist/components/form-actions.js.map +1 -1
- package/dist/components/iban-input.d.ts +10 -10
- package/dist/components/inline-edit-field.d.ts +2 -1
- package/dist/components/inline-edit-field.js +6 -6
- package/dist/components/inline-edit-field.js.map +1 -1
- package/dist/components/language-select.d.ts +10 -10
- package/dist/components/legal-page.d.ts +10 -10
- package/dist/components/legal.d.ts +10 -10
- package/dist/components/list.d.ts +2 -1
- package/dist/components/list.js +4 -5
- package/dist/components/list.js.map +1 -1
- package/dist/components/money-field.d.ts +10 -10
- package/dist/components/month-picker.js +54 -44
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.d.ts +3 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +10 -10
- package/dist/components/number-input.d.ts +10 -10
- package/dist/components/numpad-sheet.d.ts +10 -10
- package/dist/components/numpad-sheet.js +2 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.js +6 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/phone-input.d.ts +10 -10
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.js +2 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +5 -5
- package/dist/components/row-actions.js +130 -38
- package/dist/components/row-actions.js.map +1 -1
- package/dist/components/series-chart.d.ts +10 -10
- package/dist/components/settings-fields.d.ts +10 -10
- package/dist/components/share-card.d.ts +7 -7
- package/dist/components/share-card.js +26 -10
- package/dist/components/share-card.js.map +1 -1
- package/dist/components/status-dot.d.ts +1 -0
- package/dist/components/status-dot.js +2 -2
- package/dist/components/status-dot.js.map +1 -1
- package/dist/components/swatch-picker.d.ts +2 -1
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +88 -17
- package/dist/components/swipeable-row.js +46 -9
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/switch.d.ts +2 -1
- package/dist/components/switch.js.map +1 -1
- package/dist/components/table.d.ts +2 -1
- package/dist/components/table.js +6 -0
- package/dist/components/table.js.map +1 -1
- package/dist/components/text-link.d.ts +9 -9
- package/dist/components/time-input.d.ts +10 -10
- package/dist/components/toggle-group.d.ts +2 -1
- package/dist/components/toggle-group.js +1 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/translation-review-editor.d.ts +4 -0
- package/dist/components/translation-review-editor.js +4 -3
- package/dist/components/translation-review-editor.js.map +1 -1
- package/dist/components/translation-review-parts.d.ts +1 -0
- package/dist/components/translation-review.d.ts +1 -0
- package/dist/components/translation-review.js +15 -18
- package/dist/components/translation-review.js.map +1 -1
- package/dist/components/ui.d.ts +9 -9
- package/dist/components/ui.js +48 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +12 -1
- package/dist/components/user-avatar.d.ts +1 -0
- package/dist/components/write-lock.d.ts +101 -13
- package/dist/components/write-lock.js +37 -13
- package/dist/components/write-lock.js.map +1 -1
- package/dist/{data-table-labels-kpAAVi6g.d.ts → data-table-labels-BJ5JE_X2.d.ts} +17 -0
- package/dist/data-table.d.ts +12 -1
- package/dist/{delete-account-setting-FLyJf-kZ.d.ts → delete-account-setting-DiPX4Vue.d.ts} +365 -47
- package/dist/feedback/feedback-attachment.d.ts +10 -10
- package/dist/feedback/feedback-attachment.js +7 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-context.d.ts +10 -10
- package/dist/feedback/feedback-crash.d.ts +10 -10
- package/dist/feedback/feedback-dialog.d.ts +10 -10
- package/dist/feedback/feedback-inbox.d.ts +9 -9
- package/dist/feedback/feedback-inbox.js +7 -3
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-labels.d.ts +10 -10
- package/dist/feedback/feedback-menu.d.ts +3 -3
- package/dist/feedback/feedback-record.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.d.ts +10 -10
- package/dist/feedback/feedback-row-detail.js.map +1 -1
- package/dist/feedback/feedback-status-undo.d.ts +10 -10
- package/dist/feedback/feedback-submit.d.ts +10 -10
- package/dist/feedback/feedback-swipe.d.ts +10 -10
- package/dist/feedback/feedback-swipe.js +3 -6
- package/dist/feedback/feedback-swipe.js.map +1 -1
- package/dist/feedback/feedback-table.d.ts +10 -10
- package/dist/feedback/feedback-table.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +10 -10
- package/dist/feedback.d.ts +10 -10
- package/dist/hooks/use-file-drop.d.ts +10 -10
- package/dist/i18n/defaults.d.ts +10 -10
- package/dist/i18n/defaults.js +2 -0
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.d.ts +10 -10
- package/dist/i18n/german.js +34 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +4 -4
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/languages.d.ts +10 -10
- package/dist/i18n/locales/de-CH.d.ts +10 -10
- package/dist/i18n/locales/en.d.ts +10 -10
- package/dist/i18n/locales/es.d.ts +10 -10
- package/dist/i18n/locales/es.js +34 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +10 -10
- package/dist/i18n/locales/fr.js +35 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +10 -10
- package/dist/i18n/locales/hu.js +36 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +10 -10
- package/dist/i18n/locales/it.js +34 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +10 -10
- package/dist/i18n/locales/zh.js +34 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.d.ts +10 -10
- package/dist/i18n/review.js +8 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +10 -10
- package/dist/landing/landing-sections.d.ts +10 -10
- package/dist/landing/public-header.d.ts +10 -10
- package/dist/landing/routing.d.ts +10 -10
- package/dist/pay/.well-known/apple-developer-merchantid-domain-association +1 -0
- package/dist/pay/index.html +30 -0
- package/dist/pay/pay.css +126 -0
- package/dist/pay/pay.js +198 -0
- package/dist/rhf/fields.d.ts +10 -10
- package/dist/rhf/form.d.ts +10 -10
- package/dist/rhf.d.ts +10 -10
- package/dist/settings/settings-section.d.ts +10 -10
- package/dist/shell/app-shell.d.ts +9 -9
- package/dist/shell/app-shell.js +2 -1
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +10 -10
- package/dist/shell/topbar-action-menu.d.ts +1 -0
- package/dist/shell.d.ts +10 -10
- package/dist/testing/css-variables.d.ts +74 -0
- package/dist/testing/css-variables.generated.d.ts +7 -0
- package/dist/testing/css-variables.generated.js +8 -0
- package/dist/testing/css-variables.generated.js.map +1 -0
- package/dist/testing/css-variables.js +93 -0
- package/dist/testing/css-variables.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/theme/contrast-tokens.d.ts +44 -1
- package/dist/theme/contrast-tokens.js +21 -0
- package/dist/theme/contrast-tokens.js.map +1 -1
- package/dist/theme/contrast.d.ts +2 -2
- package/dist/theme/palette-derive.js +60 -11
- package/dist/theme/palette-derive.js.map +1 -1
- package/dist/theme/palette-presets.js +45 -43
- package/dist/theme/palette-presets.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +11 -11
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard.d.ts +10 -10
- package/package.json +10 -2
- package/src/account/delete-account-setting.tsx +6 -1
- package/src/account/email-change-setting.tsx +9 -3
- package/src/account/sessions-setting.tsx +7 -2
- package/src/admin/admin-action-confirm.tsx +2 -1
- package/src/admin/invitations-panel.tsx +9 -3
- package/src/admin/plan-change.tsx +459 -0
- package/src/admin/reviewer-scope-editor.tsx +2 -1
- package/src/admin/role-select.tsx +2 -1
- package/src/admin/user-roster.tsx +5 -3
- package/src/auth/auth-errors.ts +25 -2
- package/src/auth/complete-name-dialog.tsx +6 -2
- package/src/billing/billing-banner.tsx +3 -1
- package/src/billing/billing-labels.ts +45 -0
- package/src/billing/billing-lock.ts +36 -3
- package/src/billing/billing-overview.ts +58 -0
- package/src/billing/checkout-processing.ts +391 -0
- package/src/billing/checkout-return.ts +90 -0
- package/src/billing/pay-page.ts +242 -0
- package/src/billing/plan-limit-toast.tsx +91 -0
- package/src/billing/subscription-actions.tsx +41 -8
- package/src/components/account-settings.tsx +6 -2
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/button-group.tsx +4 -1
- package/src/components/calculator.tsx +10 -6
- package/src/components/checkbox-group.tsx +2 -2
- package/src/components/checkbox.tsx +2 -1
- package/src/components/chip.tsx +37 -32
- package/src/components/choice-card.tsx +4 -2
- package/src/components/combobox.tsx +4 -4
- package/src/components/confirm-dialog.tsx +13 -6
- package/src/components/country-select.tsx +2 -2
- package/src/components/danger-confirm.tsx +2 -2
- package/src/components/data-table.tsx +20 -9
- package/src/components/date-picker.tsx +10 -7
- package/src/components/entity-combobox.tsx +3 -3
- package/src/components/field-parts.tsx +61 -14
- package/src/components/file-button.tsx +5 -4
- package/src/components/file-dropzone.tsx +2 -2
- package/src/components/floating-panel.tsx +7 -5
- package/src/components/form-actions.tsx +12 -5
- package/src/components/inline-edit-field.tsx +13 -13
- package/src/components/list.tsx +10 -9
- package/src/components/month-picker.tsx +8 -2
- package/src/components/multi-entity-combobox.tsx +3 -3
- package/src/components/numpad-sheet.tsx +8 -4
- package/src/components/page-header.tsx +15 -1
- package/src/components/popover.tsx +4 -0
- package/src/components/row-actions.tsx +201 -50
- package/src/components/share-card.tsx +67 -13
- package/src/components/status-dot.tsx +7 -7
- package/src/components/swatch-picker.tsx +2 -2
- package/src/components/swipeable-row.tsx +138 -27
- package/src/components/switch.tsx +2 -1
- package/src/components/table.tsx +8 -1
- package/src/components/toggle-group.tsx +5 -3
- package/src/components/translation-review-editor.tsx +8 -3
- package/src/components/translation-review.tsx +18 -22
- package/src/components/ui.tsx +98 -44
- package/src/components/write-lock.tsx +150 -24
- package/src/feedback/feedback-attachment.tsx +13 -9
- package/src/feedback/feedback-inbox.tsx +10 -6
- package/src/feedback/feedback-row-detail.tsx +3 -2
- package/src/feedback/feedback-swipe.tsx +6 -11
- package/src/feedback/feedback-table.tsx +2 -1
- package/src/i18n/defaults.ts +2 -0
- package/src/i18n/german.ts +38 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/es.ts +34 -0
- package/src/i18n/locales/fr.ts +39 -0
- package/src/i18n/locales/hu.ts +37 -0
- package/src/i18n/locales/it.ts +35 -0
- package/src/i18n/locales/zh.ts +33 -0
- package/src/i18n/review.ts +7 -0
- package/src/index.ts +48 -4
- package/src/shell/app-shell.tsx +11 -6
- package/src/testing/css-variables.generated.ts +112 -0
- package/src/testing/css-variables.ts +191 -0
- package/src/testing/index.ts +13 -0
- package/src/theme/contrast-tokens.ts +53 -2
- package/src/theme/palette-derive.ts +87 -10
- package/src/theme/palette-presets.ts +45 -43
- package/src/wizard/stepper-nav.tsx +2 -1
- package/tokens.css +306 -46
package/src/components/ui.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-lab
|
|
|
12
12
|
import type { KitLinkComponent, KitLinkProps } from "../i18n/kit-labels";
|
|
13
13
|
import { pickLinkRenderer, replacingClick, routerLinkNavigation } from "./text-link";
|
|
14
14
|
import { useCommitReason } from "./write-lock";
|
|
15
|
+
import type { CommitScope } from "./write-lock";
|
|
15
16
|
import { mergeDescribedBy } from "./choice-parts";
|
|
16
17
|
import {
|
|
17
18
|
CompactControlsContext,
|
|
@@ -24,6 +25,8 @@ import {
|
|
|
24
25
|
useFieldHint,
|
|
25
26
|
useLockReason,
|
|
26
27
|
FLOATING_LABEL_STATIC,
|
|
28
|
+
LABEL_ABOVE_AT_LARGE,
|
|
29
|
+
LABEL_ABOVE_BOX,
|
|
27
30
|
STATIC_LABEL_TYPE,
|
|
28
31
|
type FieldHintParts,
|
|
29
32
|
} from "./field-parts";
|
|
@@ -46,8 +49,13 @@ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "bran
|
|
|
46
49
|
// The ring is the kit's focus frame (§5): {@link FOCUS_RING_WIDTH} wide, on keyboard focus
|
|
47
50
|
// (`focus-visible`) — a tap or a click no longer leaves a ring on the button — in each
|
|
48
51
|
// variant's own colour below.
|
|
52
|
+
//
|
|
53
|
+
// `[&_svg]:shrink-0` (0.32.2, kastlan's 0.32.1 report): an icon beside a label keeps its
|
|
54
|
+
// size when the label wraps at Large. Without it the svg was a flex item like the text,
|
|
55
|
+
// and a two-line label squeezed it to a sliver ("New invoice" in a page header at 360 px
|
|
56
|
+
// / Extra large).
|
|
49
57
|
const BUTTON_BASE = cn(
|
|
50
|
-
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed",
|
|
58
|
+
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:shrink-0",
|
|
51
59
|
FOCUS_RING_WIDTH,
|
|
52
60
|
);
|
|
53
61
|
|
|
@@ -83,6 +91,18 @@ const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
|
|
|
83
91
|
sm: "large:min-h-[48px] large:py-2.5",
|
|
84
92
|
};
|
|
85
93
|
|
|
94
|
+
/**
|
|
95
|
+
* The hover of a control that may sit on the page, a card or a well (0.33, B′ —
|
|
96
|
+
* docs/colour-roles-harmonization.md §12.1, §12.10): the body ink at 7 %, TRANSLUCENT,
|
|
97
|
+
* so it darkens (in dark, lightens) whatever is under it by the same step — 1.11:1 or
|
|
98
|
+
* more on all three surfaces, both themes, with the label at 7:1 and up. It was
|
|
99
|
+
* `--bg-surface-2`, which since 0.33 is the page itself in light: a ghost button on the
|
|
100
|
+
* page would not have moved at all. Not `--bg-hover` either, which is mixed against the
|
|
101
|
+
* CARD and stands only 1.04:1 from the page. IconButton's `custom` tone uses the same
|
|
102
|
+
* recipe at 12 %.
|
|
103
|
+
*/
|
|
104
|
+
const HOVER_INK = "hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]";
|
|
105
|
+
|
|
86
106
|
// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.
|
|
87
107
|
// Actions default to a warm bordered look (primary = filled warm chip, secondary =
|
|
88
108
|
// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the
|
|
@@ -91,11 +111,11 @@ const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
|
|
|
91
111
|
// variant's own accent: brand for the four neutral ones, danger for `danger`.
|
|
92
112
|
const buttonVariantClasses: Record<ButtonVariant, string> = {
|
|
93
113
|
primary:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"border border-[var(--border)] bg-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
// Hovers onto `--bg-active`, not `--border` (0.33, §12.9): More contrast steps the
|
|
115
|
+
// border until it is a 3:1 LINE, and as a fill under the label that left 2.7:1.
|
|
116
|
+
"border border-[var(--border)] bg-[var(--bg-surface-2)] text-[var(--text-primary)] hover:bg-active focus-visible:ring-[var(--brand)]",
|
|
117
|
+
secondary: `border border-[var(--border)] bg-transparent text-[var(--text-primary)] ${HOVER_INK} focus-visible:ring-[var(--brand)]`,
|
|
118
|
+
ghost: `bg-transparent text-[var(--text-primary)] ${HOVER_INK} focus-visible:ring-[var(--brand)]`,
|
|
99
119
|
danger:
|
|
100
120
|
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
101
121
|
brand:
|
|
@@ -153,6 +173,10 @@ const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
|
|
|
153
173
|
// restates every colour the variant sets (border, fill, text, hover, ring) so none of
|
|
154
174
|
// the neutral ones survives the merge.
|
|
155
175
|
const BUTTON_BOXED_DANGER: Partial<Record<ButtonVariant, string>> = {
|
|
176
|
+
// Not boxed, but its hover is a fill: the danger wash under the danger text, as
|
|
177
|
+
// IconButton's danger tone (0.33). On B′'s translucent ink `--danger` fell to 4.2:1
|
|
178
|
+
// over a light page.
|
|
179
|
+
ghost: "hover:bg-[var(--danger-bg)]",
|
|
156
180
|
secondary:
|
|
157
181
|
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
158
182
|
primary:
|
|
@@ -263,9 +287,10 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
263
287
|
* This button COMMITS — it saves, creates, deletes. Under a locked
|
|
264
288
|
* {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's
|
|
265
289
|
* reason (which wins over a `disabledReason` of its own). No provider, or an
|
|
266
|
-
* unlocked one: no effect. See {@link WriteLockProvider}
|
|
290
|
+
* unlocked one: no effect. See {@link WriteLockProvider}; a {@link CommitScope} since
|
|
291
|
+
* 0.33 (`COMMIT_EXCEPT_BILLING`: every lock but a lapsed plan's).
|
|
267
292
|
*/
|
|
268
|
-
commit?:
|
|
293
|
+
commit?: CommitScope;
|
|
269
294
|
/**
|
|
270
295
|
* Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.
|
|
271
296
|
*
|
|
@@ -722,8 +747,10 @@ const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }>
|
|
|
722
747
|
},
|
|
723
748
|
warning: {
|
|
724
749
|
quiet:
|
|
725
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus-visible:ring-
|
|
726
|
-
|
|
750
|
+
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus-visible:ring-warning-strong",
|
|
751
|
+
// The ring is warning's loud line (0.33, k27): `--warning-border`, amber-300, was a
|
|
752
|
+
// focus ring at 1.1:1 on the light surfaces.
|
|
753
|
+
toned: "text-[var(--warning)] hover:bg-[var(--warning-bg)] focus-visible:ring-warning-strong",
|
|
727
754
|
},
|
|
728
755
|
info: {
|
|
729
756
|
quiet:
|
|
@@ -746,8 +773,8 @@ const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }>
|
|
|
746
773
|
},
|
|
747
774
|
};
|
|
748
775
|
|
|
749
|
-
|
|
750
|
-
|
|
776
|
+
// The translucent ink on hover, as Button's secondary/ghost (B′, see HOVER_INK).
|
|
777
|
+
const ICON_BUTTON_MUTED = `text-[var(--text-placeholder)] ${HOVER_INK} hover:text-[var(--text-primary)]`;
|
|
751
778
|
|
|
752
779
|
/** Which tones sit quiet at rest when `quiet` is left out. */
|
|
753
780
|
const QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {
|
|
@@ -791,9 +818,11 @@ const ICON_BUTTON_QUIET_DISABLED_REST = "disabled:hover:text-[var(--text-placeho
|
|
|
791
818
|
|
|
792
819
|
// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the
|
|
793
820
|
// "selected" look of a Chip or a SegmentedControl option, so an on toggle reads as on
|
|
794
|
-
// beside them. After the tone, so a pressed `muted` button is brand, not grey.
|
|
821
|
+
// beside them. After the tone, so a pressed `muted` button is brand, not grey. On hover
|
|
822
|
+
// the glyph turns brand-muted: `--brand` on `--brand-bg-hover` is 2.69–2.80 in four dark
|
|
823
|
+
// presets, under an icon's 3:1 (colour-roles §12.12).
|
|
795
824
|
const ICON_BUTTON_PRESSED =
|
|
796
|
-
"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-
|
|
825
|
+
"bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)] hover:text-brand-muted disabled:hover:bg-[var(--brand-bg)] disabled:hover:text-[var(--brand)]";
|
|
797
826
|
|
|
798
827
|
// `variant="overlay"`: a round, translucent disc for a control that sits ON a photo
|
|
799
828
|
// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).
|
|
@@ -1071,8 +1100,8 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
1071
1100
|
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
1072
1101
|
/** This action COMMITS — {@link Button}'s `commit`: under a locked
|
|
1073
1102
|
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
1074
|
-
* reason. No effect without a lock. */
|
|
1075
|
-
commit?:
|
|
1103
|
+
* reason. No effect without a lock. A {@link CommitScope} since 0.33. */
|
|
1104
|
+
commit?: CommitScope;
|
|
1076
1105
|
/** The `<button>` element — a prop in React 19, as on {@link Button}. */
|
|
1077
1106
|
ref?: Ref<HTMLButtonElement>;
|
|
1078
1107
|
/** Only on the link form — see {@link IconButtonLinkProps}. */
|
|
@@ -1579,8 +1608,9 @@ export const FIELD_WRITABLE_LOOK =
|
|
|
1579
1608
|
// Extra top padding leaves room for a label that floats INSIDE the field (the
|
|
1580
1609
|
// "filled" pattern) — the label sits in the top strip, the value below it. Used by
|
|
1581
1610
|
// every labelled field (native + custom-dropdown triggers). twMerge lets pt/pb win
|
|
1582
|
-
// over FIELD_BASE's py-2.
|
|
1583
|
-
|
|
1611
|
+
// over FIELD_BASE's py-2. At Large the label stands above the field (0.33, §10.17,
|
|
1612
|
+
// LABEL_ABOVE_AT_LARGE), so the strip goes and the field is FIELD_BASE's `py-2` again.
|
|
1613
|
+
export const FIELD_FLOATING_PAD = "pt-4 pb-1 large:py-2";
|
|
1584
1614
|
|
|
1585
1615
|
// Error/required highlight for a field that is missing a value — a `--danger`
|
|
1586
1616
|
// border and matching focus ring so the control itself shows what's wrong, not just
|
|
@@ -1700,12 +1730,22 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
1700
1730
|
// Animated label that starts centred (as a placeholder) in an empty field and
|
|
1701
1731
|
// floats up INSIDE the top strip on focus or once the field has a value. Sits on
|
|
1702
1732
|
// the field's own surface, so no background chip and nothing to mismatch the card.
|
|
1733
|
+
//
|
|
1734
|
+
// At Large it is a static label ABOVE the field instead (0.33, §10.17): in flow, so it
|
|
1735
|
+
// wraps; always in its floated look, since there is no strip to float into and no
|
|
1736
|
+
// placeholder to stand in for; no transition, since it never moves.
|
|
1737
|
+
const LABEL_FLOATED_AT_LARGE =
|
|
1738
|
+
"large:text-caption large:leading-tight large:text-secondary large:transition-none";
|
|
1739
|
+
|
|
1703
1740
|
export const FLOATING_LABEL_CLASS = cn(
|
|
1704
1741
|
"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all",
|
|
1705
|
-
"max-w-[calc(100%-1.5rem)] truncate",
|
|
1742
|
+
"max-w-[calc(100%-1.5rem)] truncate-until-large",
|
|
1706
1743
|
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1707
1744
|
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
1708
1745
|
"peer-disabled:opacity-50",
|
|
1746
|
+
LABEL_ABOVE_AT_LARGE,
|
|
1747
|
+
"large:block",
|
|
1748
|
+
LABEL_FLOATED_AT_LARGE,
|
|
1709
1749
|
);
|
|
1710
1750
|
|
|
1711
1751
|
// The animated label again, as a ROW that the label and its "?" share — same
|
|
@@ -1718,15 +1758,22 @@ export const FLOATING_LABEL_CLASS = cn(
|
|
|
1718
1758
|
// because the controls that carry an animated label are the ones with something
|
|
1719
1759
|
// at the end edge of the field — NumberInput's calculator is the case this was
|
|
1720
1760
|
// written for. The label truncates a little sooner; the alternative was the "?"
|
|
1721
|
-
// sitting on top of a button (steering-design feedback #48).
|
|
1761
|
+
// sitting on top of a button (steering-design feedback #48). At Large the row stands
|
|
1762
|
+
// above the field, as the label alone does, and nothing at the end edge is under it.
|
|
1722
1763
|
const FLOATING_ROW_CLASS = cn(
|
|
1723
1764
|
"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all",
|
|
1724
1765
|
"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]",
|
|
1725
1766
|
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1726
1767
|
"peer-[:not(:placeholder-shown)]:top-1 peer-[:not(:placeholder-shown)]:text-caption peer-[:not(:placeholder-shown)]:leading-tight peer-[:not(:placeholder-shown)]:text-[var(--text-secondary)]",
|
|
1727
1768
|
"peer-disabled:opacity-50",
|
|
1769
|
+
LABEL_ABOVE_AT_LARGE,
|
|
1770
|
+
LABEL_FLOATED_AT_LARGE,
|
|
1728
1771
|
);
|
|
1729
1772
|
|
|
1773
|
+
/** The static label's row with its "?" at the field's end, in the top strip — and at
|
|
1774
|
+
* Large above the field (LABEL_ABOVE_AT_LARGE). */
|
|
1775
|
+
const STATIC_ROW_CLASS = cn("pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1", LABEL_ABOVE_AT_LARGE);
|
|
1776
|
+
|
|
1730
1777
|
/**
|
|
1731
1778
|
* The one place that assembles a labelled field: a `relative` wrapper around the
|
|
1732
1779
|
* control (`children`) plus a floating label inside the top strip. Pass `staticLabel`
|
|
@@ -1770,16 +1817,22 @@ export function FloatingField({
|
|
|
1770
1817
|
}: FloatingFieldProps) {
|
|
1771
1818
|
const withHint = hint !== undefined && !srOnlyLabel;
|
|
1772
1819
|
const atEnd = withHint && staticLabel;
|
|
1820
|
+
// A label drawn over the field: at Large it stands above it instead (0.33, §10.17),
|
|
1821
|
+
// in the first row of this box (LABEL_ABOVE_BOX), though it FOLLOWS the control in
|
|
1822
|
+
// the DOM — `peer-*` reads the input before it.
|
|
1823
|
+
const above = label !== undefined && !srOnlyLabel;
|
|
1773
1824
|
const labelEl = label !== undefined && (
|
|
1774
1825
|
<label
|
|
1775
1826
|
htmlFor={htmlFor}
|
|
1827
|
+
// In the first row at Large, out of LABEL_ABOVE_BOX's rule for the field's parts.
|
|
1828
|
+
data-field-label={above && !withHint ? "" : undefined}
|
|
1776
1829
|
className={
|
|
1777
1830
|
srOnlyLabel
|
|
1778
1831
|
? "sr-only"
|
|
1779
1832
|
: atEnd
|
|
1780
|
-
? cn("pointer-events-none min-w-0 truncate", STATIC_LABEL_TYPE)
|
|
1833
|
+
? cn("pointer-events-none min-w-0 truncate-until-large", STATIC_LABEL_TYPE)
|
|
1781
1834
|
: withHint
|
|
1782
|
-
? "pointer-events-none min-w-0 truncate"
|
|
1835
|
+
? "pointer-events-none min-w-0 truncate-until-large"
|
|
1783
1836
|
: staticLabel
|
|
1784
1837
|
? FLOATING_LABEL_STATIC
|
|
1785
1838
|
: FLOATING_LABEL_CLASS
|
|
@@ -1792,8 +1845,9 @@ export function FloatingField({
|
|
|
1792
1845
|
// `relative` is the whole contract of this wrapper — the floating label and every
|
|
1793
1846
|
// control that hangs off it (a reveal toggle, a chevron) are positioned against
|
|
1794
1847
|
// this box — so it is merged through `cn` after the spread rather than left where
|
|
1795
|
-
// a caller's stray `className` or `style` could unset it.
|
|
1796
|
-
|
|
1848
|
+
// a caller's stray `className` or `style` could unset it. At Large, with a label
|
|
1849
|
+
// above, a grid: the label's row over the field's, the field's parts in the latter.
|
|
1850
|
+
<div {...rest} className={cn("relative", above && LABEL_ABOVE_BOX, className)}>
|
|
1797
1851
|
{children}
|
|
1798
1852
|
{withHint ? (
|
|
1799
1853
|
// Static: `inset-x-3` rather than `start-3`, so a long label truncates at
|
|
@@ -1801,13 +1855,7 @@ export function FloatingField({
|
|
|
1801
1855
|
// Animated: the row floats with the label and is only as wide as it needs
|
|
1802
1856
|
// to be. Either way the hint keeps its width (`shrink-0`) and the label
|
|
1803
1857
|
// is the one that gives way.
|
|
1804
|
-
<div
|
|
1805
|
-
className={
|
|
1806
|
-
atEnd
|
|
1807
|
-
? "pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1"
|
|
1808
|
-
: FLOATING_ROW_CLASS
|
|
1809
|
-
}
|
|
1810
|
-
>
|
|
1858
|
+
<div data-field-label="" className={atEnd ? STATIC_ROW_CLASS : FLOATING_ROW_CLASS}>
|
|
1811
1859
|
{labelEl}
|
|
1812
1860
|
<span className="pointer-events-auto flex shrink-0 items-center">{hint}</span>
|
|
1813
1861
|
</div>
|
|
@@ -2455,9 +2503,10 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
2455
2503
|
/**
|
|
2456
2504
|
* This select COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is
|
|
2457
2505
|
* locked the `disabledReason` way with the lock's reason (which wins over its own).
|
|
2458
|
-
* No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3
|
|
2506
|
+
* No provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3; a
|
|
2507
|
+
* {@link CommitScope} since 0.33.
|
|
2459
2508
|
*/
|
|
2460
|
-
commit?:
|
|
2509
|
+
commit?: CommitScope;
|
|
2461
2510
|
}
|
|
2462
2511
|
|
|
2463
2512
|
// The compact select: the field's colours, a toolbar button's height. `py-0` and a
|
|
@@ -2663,7 +2712,8 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
2663
2712
|
// The floated label takes the top strip of a list box too, so its first
|
|
2664
2713
|
// row starts under the label rather than behind it. No bottom padding — see
|
|
2665
2714
|
// `dress`.
|
|
2666
|
-
|
|
2715
|
+
// At Large the label stands above the field (§10.17): no strip.
|
|
2716
|
+
listBox ? "pt-5 pb-0 large:pt-2" : FIELD_FLOATING_PAD,
|
|
2667
2717
|
"peer",
|
|
2668
2718
|
dress,
|
|
2669
2719
|
locked && FIELD_LOCKED,
|
|
@@ -2796,7 +2846,9 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
|
|
|
2796
2846
|
*/
|
|
2797
2847
|
const TEXTAREA_LABEL_STRIP = cn(
|
|
2798
2848
|
"pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[calc(0.375rem-1px)] bg-[var(--bg-surface)]",
|
|
2799
|
-
|
|
2849
|
+
// Never at Large (0.33, §10.17): the label stands above the field, so there is no
|
|
2850
|
+
// strip to keep scrolled text out of, and a layer here would hide the first line.
|
|
2851
|
+
"not-large:peer-focus:block not-large:peer-[:not(:placeholder-shown)]:block",
|
|
2800
2852
|
"peer-disabled:bg-[var(--bg-surface-2)] peer-[[readonly]]:bg-[var(--bg-surface-2)]",
|
|
2801
2853
|
TEXTAREA_LABEL_EDGE,
|
|
2802
2854
|
);
|
|
@@ -3035,14 +3087,16 @@ export interface CardProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
3035
3087
|
tone?: CardTone;
|
|
3036
3088
|
/**
|
|
3037
3089
|
* How loud the `tone` frame is. `"strong"`: a 2px border in the tone's strong
|
|
3038
|
-
* colour — `--danger-border-strong` for `danger`,
|
|
3039
|
-
*
|
|
3040
|
-
*
|
|
3041
|
-
*
|
|
3042
|
-
* tile
|
|
3043
|
-
*
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
3090
|
+
* colour — `--danger-border-strong` for `danger`, `--warning-border-strong` for
|
|
3091
|
+
* `warning` (0.33: amber-600 in dark, where `--warning` stood 13:1 off the card and
|
|
3092
|
+
* shouted; docs/colour-roles-harmonization.md §5.2), the tone's own text colour
|
|
3093
|
+
* (`--info`, `--success`) for the two with no `-border-strong` token of their own.
|
|
3094
|
+
* For the one tile on a page that guards something destructive or security-relevant:
|
|
3095
|
+
* keksdose's password / delete-account tile (shared/components/password-danger-card.tsx)
|
|
3096
|
+
* wrote `toneFrameClass(tone)` over the Card by hand for the 2px rule, and `p-[15px]`
|
|
3097
|
+
* under it so the content did not move. Here the extra pixel comes off the padding the
|
|
3098
|
+
* same way (`padding` or the variant's own), so a strong card's content sits exactly
|
|
3099
|
+
* where a soft one's does.
|
|
3046
3100
|
*
|
|
3047
3101
|
* Default `"soft"`: the 1px `-border` it has always been. No effect without `tone`,
|
|
3048
3102
|
* nor on `inset`, which has no border to make louder (its tone is a fill).
|
|
@@ -3130,7 +3184,7 @@ const CARD_TONE_WASH: Record<CardTone, string> = {
|
|
|
3130
3184
|
};
|
|
3131
3185
|
|
|
3132
3186
|
const CARD_TONES_STRONG: Record<CardTone, string> = {
|
|
3133
|
-
warning: "border-2 border-
|
|
3187
|
+
warning: "border-2 border-warning-strong",
|
|
3134
3188
|
danger: "border-2 border-[var(--danger-border-strong)]",
|
|
3135
3189
|
info: "border-2 border-[var(--info)]",
|
|
3136
3190
|
success: "border-2 border-[var(--success)]",
|
|
@@ -12,15 +12,67 @@ export const DEFAULT_WRITE_LOCK_LABELS: WriteLockLabels = {
|
|
|
12
12
|
reason: "You can view this but not change it.",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* What kind of lock a source is (docs/billing-harmonization.md §12.36, ui-kit 0.33).
|
|
17
|
+
* `"demo"` (the read-only demo, `demo.writeLocked`) and `"billing"` (a lapsed plan,
|
|
18
|
+
* `billing.lockReason`) are the kit's; an app names its own — Kurvenschmiede's
|
|
19
|
+
* `"access"` (a curve shared to view) and `"customer"` (an account that owns nothing).
|
|
20
|
+
* A control exempts itself from a kind with {@link CommitScope}; a source without a
|
|
21
|
+
* kind is never exempted.
|
|
22
|
+
*/
|
|
23
|
+
export type WriteLockKind = "demo" | "billing" | (string & {});
|
|
24
|
+
|
|
25
|
+
/** One locked source, as the provider holds it: its kind and its one sentence. */
|
|
26
|
+
export interface WriteLockHold {
|
|
27
|
+
kind: WriteLockKind | undefined;
|
|
28
|
+
reason: ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A control's opt-in to the lock — the type of every `commit` prop in the kit.
|
|
33
|
+
*
|
|
34
|
+
* - `true`: every lock refuses the control (a save, an add, an invite).
|
|
35
|
+
* - `{ except: [kinds] }`: every lock but those kinds. A billing lock leaves the
|
|
36
|
+
* reader able to remove access, revoke sessions, delete the account and change their
|
|
37
|
+
* own settings (§3.3, §12.13), so the kit's parts for those take
|
|
38
|
+
* {@link COMMIT_EXCEPT_BILLING} built in, and an app's own controls of that sort do
|
|
39
|
+
* too.
|
|
40
|
+
* - `false` or left out: no lock reaches it.
|
|
41
|
+
*
|
|
42
|
+
* Under a demo lock and a billing lock together, a control exempt from billing is
|
|
43
|
+
* still refused, with the demo's reason.
|
|
44
|
+
*/
|
|
45
|
+
export type CommitScope = boolean | { readonly except: readonly WriteLockKind[] };
|
|
46
|
+
|
|
47
|
+
/** Every lock but billing's: the scope of an action a lapsed plan still allows — removing
|
|
48
|
+
* access, the account's own settings, an operator's admin route (§12.13). One constant,
|
|
49
|
+
* so the call sites read alike. */
|
|
50
|
+
export const COMMIT_EXCEPT_BILLING = { except: ["billing"] } as const;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What {@link useWriteLock} returns: whether commits are locked here as a control with
|
|
54
|
+
* the scope asked for sees it, and why.
|
|
55
|
+
*/
|
|
16
56
|
export interface WriteLock {
|
|
17
57
|
locked: boolean;
|
|
18
|
-
/** The one sentence a locked commit shows in its tooltip
|
|
19
|
-
* `locked`; `undefined` when not. */
|
|
58
|
+
/** The one sentence a locked commit shows in its tooltip: the first hold in scope's.
|
|
59
|
+
* Always set while `locked`; `undefined` when not. */
|
|
20
60
|
reason: ReactNode | undefined;
|
|
61
|
+
/** The first hold in scope's kind; `undefined` while unlocked, or for a source that
|
|
62
|
+
* names none. */
|
|
63
|
+
kind: WriteLockKind | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* EVERY locked source the provider holds, in the order the server refuses them
|
|
66
|
+
* (§12.3: the demo's 403 before billing's 402) — unfiltered by the scope, for code
|
|
67
|
+
* that must know what else locks (a toast that names the plan, a swipe plan). Empty
|
|
68
|
+
* while the provider is unlocked.
|
|
69
|
+
*/
|
|
70
|
+
holds: readonly WriteLockHold[];
|
|
21
71
|
}
|
|
22
72
|
|
|
23
|
-
const
|
|
73
|
+
const NO_HOLDS: readonly WriteLockHold[] = [];
|
|
74
|
+
|
|
75
|
+
const UNLOCKED: WriteLock = { locked: false, reason: undefined, kind: undefined, holds: NO_HOLDS };
|
|
24
76
|
|
|
25
77
|
const WriteLockContext = createContext<WriteLock>(UNLOCKED);
|
|
26
78
|
|
|
@@ -33,10 +85,27 @@ export interface WriteLockProviderProps {
|
|
|
33
85
|
* already translated. Left out: `labels.reason`.
|
|
34
86
|
*/
|
|
35
87
|
reason?: ReactNode;
|
|
88
|
+
/** What kind of lock this is — `"demo"`, `"billing"` or the app's own (see
|
|
89
|
+
* {@link WriteLockKind}), for a provider of one source. Left out, no control can be
|
|
90
|
+
* exempt from it. */
|
|
91
|
+
kind?: WriteLockKind;
|
|
92
|
+
/**
|
|
93
|
+
* Every locked source, in refusal order — what `combineWriteLocks` answers, so
|
|
94
|
+
* `<WriteLockProvider {...combineWriteLocks([...])}>` carries them. Given while
|
|
95
|
+
* `locked`, it wins over `kind` and `reason`; a hold without a reason takes
|
|
96
|
+
* `labels.reason`.
|
|
97
|
+
*/
|
|
98
|
+
holds?: readonly WriteLockHold[];
|
|
36
99
|
labels?: Partial<WriteLockLabels>;
|
|
37
100
|
children?: ReactNode;
|
|
38
101
|
}
|
|
39
102
|
|
|
103
|
+
// `null`/`""`/`false` count as "no reason given": a lock whose tooltip is empty would be
|
|
104
|
+
// a disabled button that cannot say why, the thing this replaces.
|
|
105
|
+
function given(reason: ReactNode): boolean {
|
|
106
|
+
return reason !== undefined && reason !== null && reason !== false && reason !== "";
|
|
107
|
+
}
|
|
108
|
+
|
|
40
109
|
/**
|
|
41
110
|
* The read-only lock — **everything renders; only the commit is locked.**
|
|
42
111
|
*
|
|
@@ -61,36 +130,93 @@ export interface WriteLockProviderProps {
|
|
|
61
130
|
* {@link useWriteLock} and disables itself. The lock is a courtesy, not a guard: the
|
|
62
131
|
* server still refuses the write.
|
|
63
132
|
*
|
|
133
|
+
* **A lock says its source** (0.33, docs/billing-harmonization.md §12.36). A lapsed
|
|
134
|
+
* plan locks creating and changing, but not removing access or the account's own
|
|
135
|
+
* settings (§3.3, §12.13), and the kit's parts for those hard-coded `commit` — so
|
|
136
|
+
* kastlan's app-wide lock locked a member's own sign-out everywhere. Each source now
|
|
137
|
+
* has a kind, the provider holds every locked one in order (`holds`, from
|
|
138
|
+
* `combineWriteLocks`), and a control's `commit` may name the kinds it is exempt from
|
|
139
|
+
* ({@link CommitScope}). The kit's parts follow §3.3 and §12.13 by themselves.
|
|
140
|
+
*
|
|
64
141
|
* Nested providers: the nearest wins, so an inner `locked={false}` reopens a section
|
|
65
|
-
* the caller may write (a viewer's own notes on a shared curve)
|
|
66
|
-
* with its own reason (Kurvenschmiede's owner lock beside the
|
|
67
|
-
* the outer one's sentence.
|
|
142
|
+
* the caller may write (a viewer's own notes on a shared curve) — every source at
|
|
143
|
+
* once — and an inner lock with its own reason (Kurvenschmiede's owner lock beside the
|
|
144
|
+
* viewer lock) overrides the outer one's sentence and its holds.
|
|
68
145
|
*/
|
|
69
|
-
export function WriteLockProvider({ locked, reason, labels, children }: WriteLockProviderProps) {
|
|
146
|
+
export function WriteLockProvider({ locked, reason, kind, holds, labels, children }: WriteLockProviderProps) {
|
|
70
147
|
// The `writeLock` namespace: prop > `<UiKitProvider labels>` > English.
|
|
71
148
|
const fallback = useKitLabels("writeLock", DEFAULT_WRITE_LOCK_LABELS, labels).reason;
|
|
72
|
-
// `
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
149
|
+
// `holds` wins while it has any; otherwise this provider is one source of its own.
|
|
150
|
+
const several = locked && holds !== undefined && holds.length > 0 ? holds : undefined;
|
|
151
|
+
const firstKind = several ? several[0].kind : kind;
|
|
152
|
+
const firstGiven = several ? several[0].reason : reason;
|
|
153
|
+
const firstReason = given(firstGiven) ? firstGiven : fallback;
|
|
154
|
+
// Memoised on the first hold's kind and sentence — the strings an app passes — so a
|
|
155
|
+
// one-source lock keeps one context value across renders; a list of several is
|
|
156
|
+
// memoised on its own identity.
|
|
157
|
+
const many = several && several.length > 1 ? several : undefined;
|
|
158
|
+
const value = useMemo<WriteLock>(() => {
|
|
159
|
+
if (!locked) return UNLOCKED;
|
|
160
|
+
const all = many
|
|
161
|
+
? many.map((hold) => ({ kind: hold.kind, reason: given(hold.reason) ? hold.reason : fallback }))
|
|
162
|
+
: [{ kind: firstKind, reason: firstReason }];
|
|
163
|
+
return { locked: true, reason: firstReason, kind: firstKind, holds: all };
|
|
164
|
+
}, [locked, many, firstKind, firstReason, fallback]);
|
|
80
165
|
return <WriteLockContext.Provider value={value}>{children}</WriteLockContext.Provider>;
|
|
81
166
|
}
|
|
82
167
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Whether a hold refuses a control with this scope: `true` refuses with every hold,
|
|
170
|
+
* `{ except }` with every hold whose kind it does not name. A hold with no kind is
|
|
171
|
+
* never exempted — an app that forgot to name its source must not reopen by accident.
|
|
172
|
+
*/
|
|
173
|
+
function inScope(hold: WriteLockHold, scope: CommitScope): boolean {
|
|
174
|
+
if (scope === true) return true;
|
|
175
|
+
if (scope === false) return false;
|
|
176
|
+
return hold.kind === undefined || !scope.except.includes(hold.kind);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The lock as a control with `scope` sees it — {@link useWriteLock}'s answer, pure, for
|
|
181
|
+
* a scope known only at call time: `useConfirm` reads the CALLER's lock when it is
|
|
182
|
+
* called, and applies each `confirm({ commit })`'s own scope to it when it runs.
|
|
183
|
+
*
|
|
184
|
+
* `undefined` and `false` see no lock (a control without `commit`); `true` sees the
|
|
185
|
+
* lock as it is; `{ except }` sees the first hold of another kind, with its reason
|
|
186
|
+
* and kind. `holds` is passed on unfiltered.
|
|
187
|
+
*/
|
|
188
|
+
export function writeLockFor(lock: WriteLock, scope: CommitScope | undefined): WriteLock {
|
|
189
|
+
if (!lock.locked || scope === true) return lock;
|
|
190
|
+
// A lock built by hand (a test's, an app's own context) may carry no holds: it is
|
|
191
|
+
// then the one source it describes.
|
|
192
|
+
const holds = lock.holds.length > 0 ? lock.holds : [{ kind: lock.kind, reason: lock.reason }];
|
|
193
|
+
const first = scope === undefined ? undefined : holds.find((hold) => inScope(hold, scope));
|
|
194
|
+
if (!first) return { ...UNLOCKED, holds: lock.holds };
|
|
195
|
+
// The first in scope is the provider's own first: the lock as it is, same object.
|
|
196
|
+
if (first === holds[0]) return lock;
|
|
197
|
+
return { locked: true, reason: first.reason, kind: first.kind, holds: lock.holds };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The nearest {@link WriteLockProvider}'s lock, as a control with `scope` sees it —
|
|
202
|
+
* unlocked with no provider. Left out, the scope is `true`: every lock.
|
|
203
|
+
*
|
|
204
|
+
* For code that must KNOW rather than a control that opts in: a toast that says why,
|
|
205
|
+
* a swipe plan that leaves an action out, a hand-rolled toggle that disables itself.
|
|
206
|
+
* `useWriteLock(COMMIT_EXCEPT_BILLING)` answers "would a lapsed plan's lock refuse me?"
|
|
207
|
+
* as a removal sees it — what keksdose's `useWriteLockKind() !== "billing"` asked.
|
|
208
|
+
*/
|
|
209
|
+
export function useWriteLock(scope: CommitScope = true): WriteLock {
|
|
210
|
+
return writeLockFor(useContext(WriteLockContext), scope);
|
|
86
211
|
}
|
|
87
212
|
|
|
88
213
|
/**
|
|
89
214
|
* The `disabledReason` a control with `commit` should use: the lock's reason while
|
|
90
|
-
*
|
|
91
|
-
* hover is asking — else the control's own. Internal to the kit's
|
|
215
|
+
* the lock refuses this scope — it wins over the control's own, since it is the answer
|
|
216
|
+
* to the question the hover is asking — else the control's own. Internal to the kit's
|
|
217
|
+
* commit controls.
|
|
92
218
|
*/
|
|
93
|
-
export function useCommitReason(commit:
|
|
94
|
-
const lock =
|
|
95
|
-
return
|
|
219
|
+
export function useCommitReason(commit: CommitScope | undefined, own: ReactNode): ReactNode {
|
|
220
|
+
const lock = writeLockFor(useContext(WriteLockContext), commit);
|
|
221
|
+
return lock.locked ? lock.reason : own;
|
|
96
222
|
}
|
|
@@ -12,7 +12,7 @@ import { Camera, FileText, Image as ImageIcon, Paperclip, X } from "lucide-react
|
|
|
12
12
|
import { Button, IconButton, Spinner } from "../components/ui";
|
|
13
13
|
import type { ButtonSize, ButtonVariant } from "../components/ui";
|
|
14
14
|
import { DEFAULT_FILE_PICKER_LABELS, formatAccept } from "../components/file-button";
|
|
15
|
-
import { useCommitReason } from "../components/write-lock";
|
|
15
|
+
import { useCommitReason, type CommitScope } from "../components/write-lock";
|
|
16
16
|
import { useKitFileLabels, useKitLabels } from "../i18n/kit-labels";
|
|
17
17
|
import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
18
18
|
|
|
@@ -231,6 +231,10 @@ interface FeedbackAttachmentFieldBaseProps {
|
|
|
231
231
|
* 592px, the page 614px wide). keksdose passed `className="min-w-0 max-w-full"` at both
|
|
232
232
|
* its callers to stop it; here once, for every host. In normal flow — a dialog, a note
|
|
233
233
|
* editor — both are no-ops.
|
|
234
|
+
*
|
|
235
|
+
* The names and sizes under it are `truncate-until-large` (0.33, docs/text-size-harmonization.md
|
|
236
|
+
* §10.17): "…" at Normal, and from Large up they wrap, breaking a long file name
|
|
237
|
+
* anywhere, so the bound above still holds and nothing is cut.
|
|
234
238
|
*/
|
|
235
239
|
const FIELD_ROOT = "relative min-w-0 max-w-full";
|
|
236
240
|
|
|
@@ -334,7 +338,7 @@ export interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFiel
|
|
|
334
338
|
* with the lock's reason, which wins over one of its own. No provider, or an unlocked
|
|
335
339
|
* one: no effect.
|
|
336
340
|
*/
|
|
337
|
-
commit?:
|
|
341
|
+
commit?: CommitScope;
|
|
338
342
|
/**
|
|
339
343
|
* Why no file can be added or removed — the write lock's sentence, a quota, a thread
|
|
340
344
|
* closed to replies. Unlike `disabled`, it SAYS so, the kit's commit-control way: the
|
|
@@ -478,7 +482,7 @@ function SingleField({
|
|
|
478
482
|
</div>
|
|
479
483
|
)}
|
|
480
484
|
<div className="min-w-0 flex-1">
|
|
481
|
-
<div className="truncate text-sm text-[var(--text-secondary)]">{value.name}</div>
|
|
485
|
+
<div className="truncate-until-large text-sm text-[var(--text-secondary)]">{value.name}</div>
|
|
482
486
|
<div className="text-xs text-[var(--text-muted)]">
|
|
483
487
|
{fileText.size(value.size)}
|
|
484
488
|
</div>
|
|
@@ -662,8 +666,8 @@ function MultipleField({
|
|
|
662
666
|
>
|
|
663
667
|
<ChipPreview file={chip.file} screenshot={chip.screenshot} />
|
|
664
668
|
<div className="min-w-0 flex-1">
|
|
665
|
-
<div className="truncate text-sm text-[var(--text-secondary)]">{chip.name}</div>
|
|
666
|
-
<div className="truncate text-xs text-[var(--text-muted)]">
|
|
669
|
+
<div className="truncate-until-large text-sm text-[var(--text-secondary)]">{chip.name}</div>
|
|
670
|
+
<div className="truncate-until-large text-xs text-[var(--text-muted)]">
|
|
667
671
|
{/* The screenshot's file name moves down here: still there for the
|
|
668
672
|
reporter who wants to know what will be sent, not its title. */}
|
|
669
673
|
{chip.screenshot ? `${chip.file.name} · ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size)}
|
|
@@ -899,9 +903,9 @@ function RefsField({
|
|
|
899
903
|
>
|
|
900
904
|
<RefGlyph type={ref.type} />
|
|
901
905
|
<div className="min-w-0 flex-1">
|
|
902
|
-
<div className="truncate text-sm text-[var(--text-secondary)]">{ref.name}</div>
|
|
906
|
+
<div className="truncate-until-large text-sm text-[var(--text-secondary)]">{ref.name}</div>
|
|
903
907
|
{ref.size !== undefined && (
|
|
904
|
-
<div className="truncate text-xs text-[var(--text-muted)]">{fileText.size(ref.size)}</div>
|
|
908
|
+
<div className="truncate-until-large text-xs text-[var(--text-muted)]">{fileText.size(ref.size)}</div>
|
|
905
909
|
)}
|
|
906
910
|
</div>
|
|
907
911
|
<RemoveButton
|
|
@@ -923,8 +927,8 @@ function RefsField({
|
|
|
923
927
|
<Spinner label={null} className="size-5" />
|
|
924
928
|
</span>
|
|
925
929
|
<div className="min-w-0 flex-1">
|
|
926
|
-
<div className="truncate text-sm text-[var(--text-secondary)]">{upload.file.name}</div>
|
|
927
|
-
<div className="truncate text-xs text-[var(--text-muted)]">{uploadingLabel}</div>
|
|
930
|
+
<div className="truncate-until-large text-sm text-[var(--text-secondary)]">{upload.file.name}</div>
|
|
931
|
+
<div className="truncate-until-large text-xs text-[var(--text-muted)]">{uploadingLabel}</div>
|
|
928
932
|
</div>
|
|
929
933
|
</li>
|
|
930
934
|
))}
|