@eifi1/ui-kit 0.32.2 → 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/phone-input.d.ts +10 -10
- package/dist/components/row-actions.d.ts +5 -5
- package/dist/components/row-actions.js +130 -32
- 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 +47 -27
- 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-Dy4kJgUm.d.ts → delete-account-setting-DiPX4Vue.d.ts} +355 -41
- 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/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/row-actions.tsx +208 -58
- 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 +92 -43
- 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/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";
|
|
@@ -88,6 +91,18 @@ const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
|
|
|
88
91
|
sm: "large:min-h-[48px] large:py-2.5",
|
|
89
92
|
};
|
|
90
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
|
+
|
|
91
106
|
// Warm, palette-token-driven so buttons blend with the fields + cards in every theme.
|
|
92
107
|
// Actions default to a warm bordered look (primary = filled warm chip, secondary =
|
|
93
108
|
// outline); `brand` stays the solid accent for the rare strong CTA; `danger` takes the
|
|
@@ -96,11 +111,11 @@ const BUTTON_SIZES_LARGE: Record<ButtonSize, string> = {
|
|
|
96
111
|
// variant's own accent: brand for the four neutral ones, danger for `danger`.
|
|
97
112
|
const buttonVariantClasses: Record<ButtonVariant, string> = {
|
|
98
113
|
primary:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"border border-[var(--border)] bg-
|
|
102
|
-
|
|
103
|
-
|
|
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)]`,
|
|
104
119
|
danger:
|
|
105
120
|
"bg-[var(--danger)] text-[var(--danger-contrast)] hover:bg-[var(--danger-hover)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
106
121
|
brand:
|
|
@@ -158,6 +173,10 @@ const TONED_VARIANTS = new Set<ButtonVariant>(["link", "ghost"]);
|
|
|
158
173
|
// restates every colour the variant sets (border, fill, text, hover, ring) so none of
|
|
159
174
|
// the neutral ones survives the merge.
|
|
160
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)]",
|
|
161
180
|
secondary:
|
|
162
181
|
"border-[var(--danger-border)] bg-transparent text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:ring-[var(--danger-border-strong)]",
|
|
163
182
|
primary:
|
|
@@ -268,9 +287,10 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
268
287
|
* This button COMMITS — it saves, creates, deletes. Under a locked
|
|
269
288
|
* {@link WriteLockProvider} it is disabled the `disabledReason` way, with the lock's
|
|
270
289
|
* reason (which wins over a `disabledReason` of its own). No provider, or an
|
|
271
|
-
* 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).
|
|
272
292
|
*/
|
|
273
|
-
commit?:
|
|
293
|
+
commit?: CommitScope;
|
|
274
294
|
/**
|
|
275
295
|
* Busy — the save is in flight: a spinner, `aria-busy`, and no second submit.
|
|
276
296
|
*
|
|
@@ -727,8 +747,10 @@ const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }>
|
|
|
727
747
|
},
|
|
728
748
|
warning: {
|
|
729
749
|
quiet:
|
|
730
|
-
"text-[var(--text-placeholder)] hover:bg-[var(--warning-bg)] hover:text-[var(--warning)] focus-visible:ring-
|
|
731
|
-
|
|
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",
|
|
732
754
|
},
|
|
733
755
|
info: {
|
|
734
756
|
quiet:
|
|
@@ -751,8 +773,8 @@ const ICON_BUTTON_TONES: Record<ColouredTone, { quiet: string; toned: string }>
|
|
|
751
773
|
},
|
|
752
774
|
};
|
|
753
775
|
|
|
754
|
-
|
|
755
|
-
|
|
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)]`;
|
|
756
778
|
|
|
757
779
|
/** Which tones sit quiet at rest when `quiet` is left out. */
|
|
758
780
|
const QUIET_BY_DEFAULT: Record<ColouredTone, boolean> = {
|
|
@@ -796,9 +818,11 @@ const ICON_BUTTON_QUIET_DISABLED_REST = "disabled:hover:text-[var(--text-placeho
|
|
|
796
818
|
|
|
797
819
|
// `pressed`: a toggle that is on. The brand glyph on the quiet brand fill — the
|
|
798
820
|
// "selected" look of a Chip or a SegmentedControl option, so an on toggle reads as on
|
|
799
|
-
// 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).
|
|
800
824
|
const ICON_BUTTON_PRESSED =
|
|
801
|
-
"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)]";
|
|
802
826
|
|
|
803
827
|
// `variant="overlay"`: a round, translucent disc for a control that sits ON a photo
|
|
804
828
|
// (keksdose's receipt-scan preview: close, rotate, retake over the camera image).
|
|
@@ -1076,8 +1100,8 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
1076
1100
|
disabledReasonDisplay?: DisabledReasonDisplay;
|
|
1077
1101
|
/** This action COMMITS — {@link Button}'s `commit`: under a locked
|
|
1078
1102
|
* {@link WriteLockProvider} it takes the `disabledReason` path with the lock's
|
|
1079
|
-
* reason. No effect without a lock. */
|
|
1080
|
-
commit?:
|
|
1103
|
+
* reason. No effect without a lock. A {@link CommitScope} since 0.33. */
|
|
1104
|
+
commit?: CommitScope;
|
|
1081
1105
|
/** The `<button>` element — a prop in React 19, as on {@link Button}. */
|
|
1082
1106
|
ref?: Ref<HTMLButtonElement>;
|
|
1083
1107
|
/** Only on the link form — see {@link IconButtonLinkProps}. */
|
|
@@ -1584,8 +1608,9 @@ export const FIELD_WRITABLE_LOOK =
|
|
|
1584
1608
|
// Extra top padding leaves room for a label that floats INSIDE the field (the
|
|
1585
1609
|
// "filled" pattern) — the label sits in the top strip, the value below it. Used by
|
|
1586
1610
|
// every labelled field (native + custom-dropdown triggers). twMerge lets pt/pb win
|
|
1587
|
-
// over FIELD_BASE's py-2.
|
|
1588
|
-
|
|
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";
|
|
1589
1614
|
|
|
1590
1615
|
// Error/required highlight for a field that is missing a value — a `--danger`
|
|
1591
1616
|
// border and matching focus ring so the control itself shows what's wrong, not just
|
|
@@ -1705,12 +1730,22 @@ export function FieldChevron({ className, ...rest }: FieldChevronProps) {
|
|
|
1705
1730
|
// Animated label that starts centred (as a placeholder) in an empty field and
|
|
1706
1731
|
// floats up INSIDE the top strip on focus or once the field has a value. Sits on
|
|
1707
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
|
+
|
|
1708
1740
|
export const FLOATING_LABEL_CLASS = cn(
|
|
1709
1741
|
"pointer-events-none absolute start-3 top-2.5 text-sm text-[var(--text-placeholder)] transition-all",
|
|
1710
|
-
"max-w-[calc(100%-1.5rem)] truncate",
|
|
1742
|
+
"max-w-[calc(100%-1.5rem)] truncate-until-large",
|
|
1711
1743
|
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1712
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)]",
|
|
1713
1745
|
"peer-disabled:opacity-50",
|
|
1746
|
+
LABEL_ABOVE_AT_LARGE,
|
|
1747
|
+
"large:block",
|
|
1748
|
+
LABEL_FLOATED_AT_LARGE,
|
|
1714
1749
|
);
|
|
1715
1750
|
|
|
1716
1751
|
// The animated label again, as a ROW that the label and its "?" share — same
|
|
@@ -1723,15 +1758,22 @@ export const FLOATING_LABEL_CLASS = cn(
|
|
|
1723
1758
|
// because the controls that carry an animated label are the ones with something
|
|
1724
1759
|
// at the end edge of the field — NumberInput's calculator is the case this was
|
|
1725
1760
|
// written for. The label truncates a little sooner; the alternative was the "?"
|
|
1726
|
-
// 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.
|
|
1727
1763
|
const FLOATING_ROW_CLASS = cn(
|
|
1728
1764
|
"pointer-events-none absolute start-3 top-2.5 flex items-center gap-1 transition-all",
|
|
1729
1765
|
"max-w-[calc(100%-3rem)] text-sm text-[var(--text-placeholder)]",
|
|
1730
1766
|
"peer-focus:top-1 peer-focus:text-caption peer-focus:leading-tight peer-focus:text-[var(--text-secondary)]",
|
|
1731
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)]",
|
|
1732
1768
|
"peer-disabled:opacity-50",
|
|
1769
|
+
LABEL_ABOVE_AT_LARGE,
|
|
1770
|
+
LABEL_FLOATED_AT_LARGE,
|
|
1733
1771
|
);
|
|
1734
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
|
+
|
|
1735
1777
|
/**
|
|
1736
1778
|
* The one place that assembles a labelled field: a `relative` wrapper around the
|
|
1737
1779
|
* control (`children`) plus a floating label inside the top strip. Pass `staticLabel`
|
|
@@ -1775,16 +1817,22 @@ export function FloatingField({
|
|
|
1775
1817
|
}: FloatingFieldProps) {
|
|
1776
1818
|
const withHint = hint !== undefined && !srOnlyLabel;
|
|
1777
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;
|
|
1778
1824
|
const labelEl = label !== undefined && (
|
|
1779
1825
|
<label
|
|
1780
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}
|
|
1781
1829
|
className={
|
|
1782
1830
|
srOnlyLabel
|
|
1783
1831
|
? "sr-only"
|
|
1784
1832
|
: atEnd
|
|
1785
|
-
? 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)
|
|
1786
1834
|
: withHint
|
|
1787
|
-
? "pointer-events-none min-w-0 truncate"
|
|
1835
|
+
? "pointer-events-none min-w-0 truncate-until-large"
|
|
1788
1836
|
: staticLabel
|
|
1789
1837
|
? FLOATING_LABEL_STATIC
|
|
1790
1838
|
: FLOATING_LABEL_CLASS
|
|
@@ -1797,8 +1845,9 @@ export function FloatingField({
|
|
|
1797
1845
|
// `relative` is the whole contract of this wrapper — the floating label and every
|
|
1798
1846
|
// control that hangs off it (a reveal toggle, a chevron) are positioned against
|
|
1799
1847
|
// this box — so it is merged through `cn` after the spread rather than left where
|
|
1800
|
-
// a caller's stray `className` or `style` could unset it.
|
|
1801
|
-
|
|
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)}>
|
|
1802
1851
|
{children}
|
|
1803
1852
|
{withHint ? (
|
|
1804
1853
|
// Static: `inset-x-3` rather than `start-3`, so a long label truncates at
|
|
@@ -1806,13 +1855,7 @@ export function FloatingField({
|
|
|
1806
1855
|
// Animated: the row floats with the label and is only as wide as it needs
|
|
1807
1856
|
// to be. Either way the hint keeps its width (`shrink-0`) and the label
|
|
1808
1857
|
// is the one that gives way.
|
|
1809
|
-
<div
|
|
1810
|
-
className={
|
|
1811
|
-
atEnd
|
|
1812
|
-
? "pointer-events-none absolute inset-x-3 top-1 flex items-center gap-1"
|
|
1813
|
-
: FLOATING_ROW_CLASS
|
|
1814
|
-
}
|
|
1815
|
-
>
|
|
1858
|
+
<div data-field-label="" className={atEnd ? STATIC_ROW_CLASS : FLOATING_ROW_CLASS}>
|
|
1816
1859
|
{labelEl}
|
|
1817
1860
|
<span className="pointer-events-auto flex shrink-0 items-center">{hint}</span>
|
|
1818
1861
|
</div>
|
|
@@ -2460,9 +2503,10 @@ export interface SelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement
|
|
|
2460
2503
|
/**
|
|
2461
2504
|
* This select COMMITS — choosing saves. Under a locked {@link WriteLockProvider} it is
|
|
2462
2505
|
* locked the `disabledReason` way with the lock's reason (which wins over its own).
|
|
2463
|
-
* 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.
|
|
2464
2508
|
*/
|
|
2465
|
-
commit?:
|
|
2509
|
+
commit?: CommitScope;
|
|
2466
2510
|
}
|
|
2467
2511
|
|
|
2468
2512
|
// The compact select: the field's colours, a toolbar button's height. `py-0` and a
|
|
@@ -2668,7 +2712,8 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
|
|
2668
2712
|
// The floated label takes the top strip of a list box too, so its first
|
|
2669
2713
|
// row starts under the label rather than behind it. No bottom padding — see
|
|
2670
2714
|
// `dress`.
|
|
2671
|
-
|
|
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,
|
|
2672
2717
|
"peer",
|
|
2673
2718
|
dress,
|
|
2674
2719
|
locked && FIELD_LOCKED,
|
|
@@ -2801,7 +2846,9 @@ function labelEdge(scrollTop: number, lineHeight: number): { height: number; sol
|
|
|
2801
2846
|
*/
|
|
2802
2847
|
const TEXTAREA_LABEL_STRIP = cn(
|
|
2803
2848
|
"pointer-events-none absolute inset-x-px top-px hidden h-4 rounded-t-[calc(0.375rem-1px)] bg-[var(--bg-surface)]",
|
|
2804
|
-
|
|
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",
|
|
2805
2852
|
"peer-disabled:bg-[var(--bg-surface-2)] peer-[[readonly]]:bg-[var(--bg-surface-2)]",
|
|
2806
2853
|
TEXTAREA_LABEL_EDGE,
|
|
2807
2854
|
);
|
|
@@ -3040,14 +3087,16 @@ export interface CardProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
3040
3087
|
tone?: CardTone;
|
|
3041
3088
|
/**
|
|
3042
3089
|
* How loud the `tone` frame is. `"strong"`: a 2px border in the tone's strong
|
|
3043
|
-
* colour — `--danger-border-strong` for `danger`,
|
|
3044
|
-
*
|
|
3045
|
-
*
|
|
3046
|
-
*
|
|
3047
|
-
* tile
|
|
3048
|
-
*
|
|
3049
|
-
*
|
|
3050
|
-
*
|
|
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.
|
|
3051
3100
|
*
|
|
3052
3101
|
* Default `"soft"`: the 1px `-border` it has always been. No effect without `tone`,
|
|
3053
3102
|
* nor on `inset`, which has no border to make louder (its tone is a fill).
|
|
@@ -3135,7 +3184,7 @@ const CARD_TONE_WASH: Record<CardTone, string> = {
|
|
|
3135
3184
|
};
|
|
3136
3185
|
|
|
3137
3186
|
const CARD_TONES_STRONG: Record<CardTone, string> = {
|
|
3138
|
-
warning: "border-2 border-
|
|
3187
|
+
warning: "border-2 border-warning-strong",
|
|
3139
3188
|
danger: "border-2 border-[var(--danger-border-strong)]",
|
|
3140
3189
|
info: "border-2 border-[var(--info)]",
|
|
3141
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
|
))}
|
|
@@ -20,7 +20,7 @@ import { cn } from "../lib/cn";
|
|
|
20
20
|
import { Button, Textarea } from "../components/ui";
|
|
21
21
|
import type { ButtonSize, ButtonVariant } from "../components/ui";
|
|
22
22
|
import { Tooltip } from "../components/tooltip";
|
|
23
|
-
import { useCommitReason, useWriteLock } from "../components/write-lock";
|
|
23
|
+
import { useCommitReason, useWriteLock, type CommitScope } from "../components/write-lock";
|
|
24
24
|
import { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from "./feedback-attachment";
|
|
25
25
|
import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
26
26
|
import { useFeedbackCategoryLabels, useFeedbackStatusLabels } from "./feedback-labels";
|
|
@@ -401,7 +401,7 @@ export function FeedbackStatusTransitions({
|
|
|
401
401
|
* every app's lock is about (keksdose's shell lock is a read-only demo BUDGET, and a
|
|
402
402
|
* report is not budget data).
|
|
403
403
|
*/
|
|
404
|
-
commit?:
|
|
404
|
+
commit?: CommitScope;
|
|
405
405
|
/** Why the statuses cannot be changed right now, the same way (a lock's own reason
|
|
406
406
|
* wins over it). Only read while `canEdit`: a read-only row is a picture of the
|
|
407
407
|
* workflow, not a refused action. */
|
|
@@ -461,7 +461,9 @@ export function FeedbackStatusTransitions({
|
|
|
461
461
|
"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium",
|
|
462
462
|
active
|
|
463
463
|
? cn(meta.activeBg, meta.activeText, "border-transparent")
|
|
464
|
-
:
|
|
464
|
+
: // The translucent ink on hover (0.33, B′), as Button's secondary:
|
|
465
|
+
// the well is the page in light, and these pills sit on either.
|
|
466
|
+
"border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]",
|
|
465
467
|
),
|
|
466
468
|
// Both stay unclickable, but only the read-only case is DIMMED: the
|
|
467
469
|
// current status is the one thing in the row that has to be legible
|
|
@@ -587,7 +589,7 @@ export function FeedbackNoteEditor({
|
|
|
587
589
|
/** Save COMMITS (0.27.0): under a locked `WriteLockProvider` it is disabled the
|
|
588
590
|
* `disabledReason` way, with the lock's reason, and Ctrl/⌘+Enter saves nothing. The
|
|
589
591
|
* text box stays editable — nothing in it reaches the server until the save. */
|
|
590
|
-
commit?:
|
|
592
|
+
commit?: CommitScope;
|
|
591
593
|
}) {
|
|
592
594
|
const [draft, setDraft] = useState(initial);
|
|
593
595
|
const [file, setFile] = useState<File | null>(null);
|
|
@@ -607,11 +609,13 @@ export function FeedbackNoteEditor({
|
|
|
607
609
|
// (Steering Design feedback #140).
|
|
608
610
|
const root = useRef<HTMLDivElement>(null);
|
|
609
611
|
const hintId = useId();
|
|
610
|
-
|
|
612
|
+
// The lock as the save button sees it, with its scope (§12.36): `commit &&
|
|
613
|
+
// lock.locked` read a `{ except }` scope as `true`.
|
|
614
|
+
const lock = useWriteLock(commit ?? false);
|
|
611
615
|
const blank = required && !draft.trim();
|
|
612
616
|
// The shortcut obeys what the button shows: no save while one is in flight, while the
|
|
613
617
|
// note a `required` editor needs is missing, or while a `commit` save is locked.
|
|
614
|
-
const canSubmit = !pending && !blank && !
|
|
618
|
+
const canSubmit = !pending && !blank && !lock.locked;
|
|
615
619
|
const submit = () => onSave(draft, file);
|
|
616
620
|
return (
|
|
617
621
|
// Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,
|