@kelpie/ui 0.11.0 → 0.13.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/dist/api/resources/attendances.d.ts +13 -0
- package/dist/api/resources/attendances.d.ts.map +1 -0
- package/dist/api/resources/attendances.js +34 -0
- package/dist/api/resources/attendances.js.map +1 -0
- package/dist/api/resources/eventAssociations.d.ts +12 -0
- package/dist/api/resources/eventAssociations.d.ts.map +1 -0
- package/dist/api/resources/eventAssociations.js +25 -0
- package/dist/api/resources/eventAssociations.js.map +1 -0
- package/dist/api/resources/events.d.ts +17 -0
- package/dist/api/resources/events.d.ts.map +1 -0
- package/dist/api/resources/events.js +40 -0
- package/dist/api/resources/events.js.map +1 -0
- package/dist/api/resources/handbookSeed.d.ts +11 -0
- package/dist/api/resources/handbookSeed.d.ts.map +1 -0
- package/dist/api/resources/handbookSeed.js +43 -0
- package/dist/api/resources/handbookSeed.js.map +1 -0
- package/dist/api/resources/moduleSettings.d.ts +10 -0
- package/dist/api/resources/moduleSettings.d.ts.map +1 -1
- package/dist/api/resources/moduleSettings.js +21 -0
- package/dist/api/resources/moduleSettings.js.map +1 -1
- package/dist/api/resources/planItems.d.ts +4 -4
- package/dist/api/resources/planItems.d.ts.map +1 -1
- package/dist/api/resources/planItems.js +1 -1
- package/dist/api/resources/planItems.js.map +1 -1
- package/dist/api/resources/publicConfig.js +3 -3
- package/dist/api/resources/sampleData.d.ts.map +1 -1
- package/dist/api/resources/sampleData.js +2 -0
- package/dist/api/resources/sampleData.js.map +1 -1
- package/dist/api/resources/session.d.ts +22 -1
- package/dist/api/resources/session.d.ts.map +1 -1
- package/dist/api/resources/session.js +43 -1
- package/dist/api/resources/session.js.map +1 -1
- package/dist/api/resources/workspace.d.ts.map +1 -1
- package/dist/api/resources/workspace.js +2 -3
- package/dist/api/resources/workspace.js.map +1 -1
- package/dist/app/KelpieApp.d.ts.map +1 -1
- package/dist/app/KelpieApp.js +5 -1
- package/dist/app/KelpieApp.js.map +1 -1
- package/dist/components/ActivitiesPanel.d.ts.map +1 -1
- package/dist/components/ActivitiesPanel.js +2 -0
- package/dist/components/ActivitiesPanel.js.map +1 -1
- package/dist/components/AddressesField.d.ts +23 -0
- package/dist/components/AddressesField.d.ts.map +1 -0
- package/dist/components/AddressesField.js +215 -0
- package/dist/components/AddressesField.js.map +1 -0
- package/dist/components/CountrySearch.d.ts +16 -0
- package/dist/components/CountrySearch.d.ts.map +1 -0
- package/dist/components/CountrySearch.js +30 -0
- package/dist/components/CountrySearch.js.map +1 -0
- package/dist/components/EnvironmentBanner.d.ts +0 -12
- package/dist/components/EnvironmentBanner.d.ts.map +1 -1
- package/dist/components/EnvironmentBanner.js +21 -6
- package/dist/components/EnvironmentBanner.js.map +1 -1
- package/dist/components/InlineEdit.d.ts +7 -1
- package/dist/components/InlineEdit.d.ts.map +1 -1
- package/dist/components/InlineEdit.js +8 -1
- package/dist/components/InlineEdit.js.map +1 -1
- package/dist/components/LinkedEventsPanel.d.ts +11 -0
- package/dist/components/LinkedEventsPanel.d.ts.map +1 -0
- package/dist/components/LinkedEventsPanel.js +18 -0
- package/dist/components/LinkedEventsPanel.js.map +1 -0
- package/dist/components/NotesPanel.d.ts +4 -6
- package/dist/components/NotesPanel.d.ts.map +1 -1
- package/dist/components/NotesPanel.js +39 -3
- package/dist/components/NotesPanel.js.map +1 -1
- package/dist/components/PlanAttention.d.ts.map +1 -1
- package/dist/components/PlanAttention.js +3 -2
- package/dist/components/PlanAttention.js.map +1 -1
- package/dist/components/PlanPanel.d.ts +3 -3
- package/dist/components/PlanPanel.d.ts.map +1 -1
- package/dist/components/Shell.d.ts.map +1 -1
- package/dist/components/Shell.js +8 -2
- package/dist/components/Shell.js.map +1 -1
- package/dist/components/SocialNetworkIcon.d.ts +17 -0
- package/dist/components/SocialNetworkIcon.d.ts.map +1 -0
- package/dist/components/SocialNetworkIcon.js +39 -0
- package/dist/components/SocialNetworkIcon.js.map +1 -0
- package/dist/components/SocialProfilesField.d.ts +8 -0
- package/dist/components/SocialProfilesField.d.ts.map +1 -1
- package/dist/components/SocialProfilesField.js +19 -5
- package/dist/components/SocialProfilesField.js.map +1 -1
- package/dist/components/TimezoneSearch.d.ts +21 -0
- package/dist/components/TimezoneSearch.d.ts.map +1 -0
- package/dist/components/TimezoneSearch.js +27 -0
- package/dist/components/TimezoneSearch.js.map +1 -0
- package/dist/components/WorkspaceSwitcher.d.ts +8 -0
- package/dist/components/WorkspaceSwitcher.d.ts.map +1 -0
- package/dist/components/WorkspaceSwitcher.js +67 -0
- package/dist/components/WorkspaceSwitcher.js.map +1 -0
- package/dist/components/environmentTone.d.ts +10 -0
- package/dist/components/environmentTone.d.ts.map +1 -0
- package/dist/components/environmentTone.js +35 -0
- package/dist/components/environmentTone.js.map +1 -0
- package/dist/index.d.ts +25 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/lib/timezones.d.ts +17 -0
- package/dist/lib/timezones.d.ts.map +1 -0
- package/dist/lib/timezones.js +39 -0
- package/dist/lib/timezones.js.map +1 -0
- package/dist/pages/CompaniesPage.d.ts.map +1 -1
- package/dist/pages/CompaniesPage.js +12 -11
- package/dist/pages/CompaniesPage.js.map +1 -1
- package/dist/pages/CompanyDetail.d.ts.map +1 -1
- package/dist/pages/CompanyDetail.js +23 -12
- package/dist/pages/CompanyDetail.js.map +1 -1
- package/dist/pages/DealDetail.d.ts.map +1 -1
- package/dist/pages/DealDetail.js +5 -1
- package/dist/pages/DealDetail.js.map +1 -1
- package/dist/pages/DecisionsPage.d.ts.map +1 -1
- package/dist/pages/DecisionsPage.js +7 -0
- package/dist/pages/DecisionsPage.js.map +1 -1
- package/dist/pages/EnquiryDetail.d.ts.map +1 -1
- package/dist/pages/EnquiryDetail.js +5 -1
- package/dist/pages/EnquiryDetail.js.map +1 -1
- package/dist/pages/EventDetail.d.ts +2 -0
- package/dist/pages/EventDetail.d.ts.map +1 -0
- package/dist/pages/EventDetail.js +332 -0
- package/dist/pages/EventDetail.js.map +1 -0
- package/dist/pages/EventsPage.d.ts +2 -0
- package/dist/pages/EventsPage.d.ts.map +1 -0
- package/dist/pages/EventsPage.js +176 -0
- package/dist/pages/EventsPage.js.map +1 -0
- package/dist/pages/ListDetail.d.ts.map +1 -1
- package/dist/pages/ListDetail.js +17 -0
- package/dist/pages/ListDetail.js.map +1 -1
- package/dist/pages/OpportunityDetail.d.ts.map +1 -1
- package/dist/pages/OpportunityDetail.js +5 -1
- package/dist/pages/OpportunityDetail.js.map +1 -1
- package/dist/pages/PartnershipDetail.d.ts.map +1 -1
- package/dist/pages/PartnershipDetail.js +5 -1
- package/dist/pages/PartnershipDetail.js.map +1 -1
- package/dist/pages/PeoplePage.d.ts.map +1 -1
- package/dist/pages/PeoplePage.js +13 -5
- package/dist/pages/PeoplePage.js.map +1 -1
- package/dist/pages/PersonDetail.d.ts.map +1 -1
- package/dist/pages/PersonDetail.js +94 -28
- package/dist/pages/PersonDetail.js.map +1 -1
- package/dist/pages/PlanningPage.d.ts.map +1 -1
- package/dist/pages/PlanningPage.js +6 -3
- package/dist/pages/PlanningPage.js.map +1 -1
- package/dist/pages/RaiseDetail.d.ts.map +1 -1
- package/dist/pages/RaiseDetail.js +5 -1
- package/dist/pages/RaiseDetail.js.map +1 -1
- package/dist/pages/RoleDetail.d.ts.map +1 -1
- package/dist/pages/RoleDetail.js +4 -1
- package/dist/pages/RoleDetail.js.map +1 -1
- package/dist/pages/SearchPage.d.ts.map +1 -1
- package/dist/pages/SearchPage.js +2 -1
- package/dist/pages/SearchPage.js.map +1 -1
- package/dist/pages/account/PreferencesPage.d.ts.map +1 -1
- package/dist/pages/account/PreferencesPage.js +2 -28
- package/dist/pages/account/PreferencesPage.js.map +1 -1
- package/dist/pages/account/SecurityPage.js +2 -2
- package/dist/pages/account/SecurityPage.js.map +1 -1
- package/dist/pages/admin/DataPage.js +10 -9
- package/dist/pages/admin/DataPage.js.map +1 -1
- package/dist/pages/admin/FieldsPage.js +1 -1
- package/dist/pages/admin/FieldsPage.js.map +1 -1
- package/dist/pages/admin/ModulesPage.d.ts.map +1 -1
- package/dist/pages/admin/ModulesPage.js +1 -0
- package/dist/pages/admin/ModulesPage.js.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts +12 -0
- package/dist/pages/admin/WorkspaceSettingsPage.d.ts.map +1 -1
- package/dist/pages/admin/WorkspaceSettingsPage.js +2 -21
- package/dist/pages/admin/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/auth/AuthForm.d.ts +18 -0
- package/dist/pages/auth/AuthForm.d.ts.map +1 -1
- package/dist/pages/auth/AuthForm.js +17 -0
- package/dist/pages/auth/AuthForm.js.map +1 -1
- package/dist/pages/auth/AuthLayout.d.ts +5 -4
- package/dist/pages/auth/AuthLayout.d.ts.map +1 -1
- package/dist/pages/auth/AuthLayout.js +7 -5
- package/dist/pages/auth/AuthLayout.js.map +1 -1
- package/dist/pages/auth/AuthMethods.d.ts +12 -0
- package/dist/pages/auth/AuthMethods.d.ts.map +1 -0
- package/dist/pages/auth/AuthMethods.js +20 -0
- package/dist/pages/auth/AuthMethods.js.map +1 -0
- package/dist/pages/auth/RegionSwitcher.d.ts +10 -0
- package/dist/pages/auth/RegionSwitcher.d.ts.map +1 -0
- package/dist/pages/auth/RegionSwitcher.js +28 -0
- package/dist/pages/auth/RegionSwitcher.js.map +1 -0
- package/dist/pages/auth/SignInPage.d.ts +7 -0
- package/dist/pages/auth/SignInPage.d.ts.map +1 -1
- package/dist/pages/auth/SignInPage.js +5 -8
- package/dist/pages/auth/SignInPage.js.map +1 -1
- package/dist/pages/auth/SignUpPage.d.ts.map +1 -1
- package/dist/pages/auth/SignUpPage.js +9 -1
- package/dist/pages/auth/SignUpPage.js.map +1 -1
- package/dist/pages/auth/nextPath.d.ts +15 -0
- package/dist/pages/auth/nextPath.d.ts.map +1 -0
- package/dist/pages/auth/nextPath.js +17 -0
- package/dist/pages/auth/nextPath.js.map +1 -0
- package/dist/pages/auth/regionHref.d.ts +6 -0
- package/dist/pages/auth/regionHref.d.ts.map +1 -0
- package/dist/pages/auth/regionHref.js +8 -0
- package/dist/pages/auth/regionHref.js.map +1 -0
- package/dist/pages/dashboard/DashboardPage.d.ts +8 -0
- package/dist/pages/dashboard/DashboardPage.d.ts.map +1 -1
- package/dist/pages/dashboard/DashboardPage.js +5 -13
- package/dist/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/pages/dashboard/attention.d.ts +1 -8
- package/dist/pages/dashboard/attention.d.ts.map +1 -1
- package/dist/pages/dashboard/attention.js +21 -54
- package/dist/pages/dashboard/attention.js.map +1 -1
- package/dist/pages/dashboard/sections.d.ts.map +1 -1
- package/dist/pages/dashboard/sections.js +3 -2
- package/dist/pages/dashboard/sections.js.map +1 -1
- package/dist/pages/forms/FormActions.d.ts.map +1 -1
- package/dist/pages/forms/FormActions.js +7 -3
- package/dist/pages/forms/FormActions.js.map +1 -1
- package/dist/pages/onboarding/HandbookStepPage.d.ts +4 -8
- package/dist/pages/onboarding/HandbookStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/HandbookStepPage.js +13 -13
- package/dist/pages/onboarding/HandbookStepPage.js.map +1 -1
- package/dist/pages/onboarding/InvitesStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/InvitesStepPage.js +13 -7
- package/dist/pages/onboarding/InvitesStepPage.js.map +1 -1
- package/dist/pages/onboarding/ModulesStepPage.d.ts +11 -0
- package/dist/pages/onboarding/ModulesStepPage.d.ts.map +1 -0
- package/dist/pages/onboarding/ModulesStepPage.js +80 -0
- package/dist/pages/onboarding/ModulesStepPage.js.map +1 -0
- package/dist/pages/onboarding/OnboardingNav.d.ts +17 -0
- package/dist/pages/onboarding/OnboardingNav.d.ts.map +1 -0
- package/dist/pages/onboarding/OnboardingNav.js +37 -0
- package/dist/pages/onboarding/OnboardingNav.js.map +1 -0
- package/dist/pages/onboarding/OrganisationStepPage.d.ts +13 -0
- package/dist/pages/onboarding/OrganisationStepPage.d.ts.map +1 -0
- package/dist/pages/onboarding/OrganisationStepPage.js +59 -0
- package/dist/pages/onboarding/OrganisationStepPage.js.map +1 -0
- package/dist/pages/onboarding/WorkspaceStepPage.d.ts.map +1 -1
- package/dist/pages/onboarding/WorkspaceStepPage.js +25 -20
- package/dist/pages/onboarding/WorkspaceStepPage.js.map +1 -1
- package/dist/pages/onboarding/handbookTemplates.d.ts +19 -0
- package/dist/pages/onboarding/handbookTemplates.d.ts.map +1 -0
- package/dist/pages/onboarding/handbookTemplates.js +57 -0
- package/dist/pages/onboarding/handbookTemplates.js.map +1 -0
- package/dist/pages/onboarding/onboardingModules.d.ts +23 -0
- package/dist/pages/onboarding/onboardingModules.d.ts.map +1 -0
- package/dist/pages/onboarding/onboardingModules.js +153 -0
- package/dist/pages/onboarding/onboardingModules.js.map +1 -0
- package/dist/pages/onboarding/onboardingRerun.d.ts +21 -0
- package/dist/pages/onboarding/onboardingRerun.d.ts.map +1 -0
- package/dist/pages/onboarding/onboardingRerun.js +33 -0
- package/dist/pages/onboarding/onboardingRerun.js.map +1 -0
- package/dist/registry/context.d.ts +3 -1
- package/dist/registry/context.d.ts.map +1 -1
- package/dist/registry/context.js +4 -0
- package/dist/registry/context.js.map +1 -1
- package/dist/registry/contributions.d.ts +24 -0
- package/dist/registry/contributions.d.ts.map +1 -1
- package/dist/registry/contributions.js.map +1 -1
- package/dist/registry/registry.d.ts +4 -1
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +6 -0
- package/dist/registry/registry.js.map +1 -1
- package/package.json +4 -3
- package/src/api/resources/attendances.ts +54 -0
- package/src/api/resources/eventAssociations.ts +43 -0
- package/src/api/resources/events.ts +70 -0
- package/src/api/resources/handbookSeed.ts +63 -0
- package/src/api/resources/moduleSettings.ts +25 -0
- package/src/api/resources/planItems.ts +5 -5
- package/src/api/resources/publicConfig.ts +3 -3
- package/src/api/resources/sampleData.ts +2 -0
- package/src/api/resources/session.ts +59 -0
- package/src/api/resources/workspace.ts +2 -3
- package/src/app/KelpieApp.tsx +11 -3
- package/src/components/ActivitiesPanel.tsx +2 -0
- package/src/components/AddressesField.tsx +502 -0
- package/src/components/CountrySearch.tsx +71 -0
- package/src/components/EnvironmentBanner.tsx +23 -6
- package/src/components/InlineEdit.tsx +35 -1
- package/src/components/LinkedEventsPanel.tsx +50 -0
- package/src/components/NotesPanel.tsx +132 -15
- package/src/components/PlanAttention.tsx +5 -4
- package/src/components/PlanPanel.tsx +3 -3
- package/src/components/Shell.tsx +16 -1
- package/src/components/SocialNetworkIcon.tsx +88 -0
- package/src/components/SocialProfilesField.tsx +57 -10
- package/src/components/TimezoneSearch.tsx +67 -0
- package/src/components/WorkspaceSwitcher.tsx +136 -0
- package/src/components/environmentTone.ts +40 -0
- package/src/index.ts +49 -3
- package/src/lib/timezones.ts +43 -0
- package/src/pages/CompaniesPage.tsx +14 -13
- package/src/pages/CompanyDetail.tsx +59 -27
- package/src/pages/DealDetail.tsx +5 -0
- package/src/pages/DecisionsPage.tsx +7 -0
- package/src/pages/EnquiryDetail.tsx +7 -0
- package/src/pages/EventDetail.tsx +794 -0
- package/src/pages/EventsPage.tsx +382 -0
- package/src/pages/ListDetail.tsx +24 -0
- package/src/pages/OpportunityDetail.tsx +7 -0
- package/src/pages/PartnershipDetail.tsx +7 -0
- package/src/pages/PeoplePage.tsx +18 -5
- package/src/pages/PersonDetail.tsx +300 -100
- package/src/pages/PlanningPage.tsx +17 -14
- package/src/pages/RaiseDetail.tsx +5 -0
- package/src/pages/RoleDetail.tsx +8 -0
- package/src/pages/SearchPage.tsx +2 -1
- package/src/pages/account/PreferencesPage.tsx +2 -37
- package/src/pages/account/SecurityPage.tsx +6 -1
- package/src/pages/admin/DataPage.tsx +63 -21
- package/src/pages/admin/FieldsPage.tsx +1 -1
- package/src/pages/admin/ModulesPage.tsx +1 -0
- package/src/pages/admin/WorkspaceSettingsPage.tsx +10 -33
- package/src/pages/auth/AuthForm.tsx +38 -0
- package/src/pages/auth/AuthLayout.tsx +8 -4
- package/src/pages/auth/AuthMethods.tsx +29 -0
- package/src/pages/auth/RegionSwitcher.tsx +56 -0
- package/src/pages/auth/SignInPage.tsx +13 -14
- package/src/pages/auth/SignUpPage.tsx +32 -0
- package/src/pages/auth/nextPath.ts +17 -0
- package/src/pages/auth/regionHref.ts +10 -0
- package/src/pages/dashboard/DashboardPage.tsx +16 -48
- package/src/pages/dashboard/attention.ts +24 -78
- package/src/pages/dashboard/sections.tsx +8 -2
- package/src/pages/forms/FormActions.tsx +14 -8
- package/src/pages/onboarding/HandbookStepPage.tsx +21 -24
- package/src/pages/onboarding/InvitesStepPage.tsx +27 -19
- package/src/pages/onboarding/ModulesStepPage.tsx +145 -0
- package/src/pages/onboarding/OnboardingNav.tsx +97 -0
- package/src/pages/onboarding/OrganisationStepPage.tsx +130 -0
- package/src/pages/onboarding/WorkspaceStepPage.tsx +53 -38
- package/src/pages/onboarding/handbookTemplates.ts +77 -0
- package/src/pages/onboarding/onboardingModules.ts +202 -0
- package/src/pages/onboarding/onboardingRerun.ts +41 -0
- package/src/registry/context.ts +6 -0
- package/src/registry/contributions.ts +26 -0
- package/src/registry/registry.ts +12 -0
- package/src/styles.css +30 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { formatAddress, primaryAddress } from '@kelpie/schemas'
|
|
2
|
+
import type { PostalAddress } from '@kelpie/schemas'
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Chip } from './Chip.tsx'
|
|
6
|
+
import { CountrySearch } from './CountrySearch.tsx'
|
|
7
|
+
import { SidebarField } from './SidebarField.tsx'
|
|
8
|
+
|
|
9
|
+
export interface AddressesFieldProps<Kind extends string> {
|
|
10
|
+
readonly value: readonly PostalAddress<Kind>[]
|
|
11
|
+
readonly kinds: readonly Kind[]
|
|
12
|
+
readonly kindLabels: Readonly<Record<Kind, string>>
|
|
13
|
+
readonly onChange: (addresses: readonly PostalAddress<Kind>[]) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AddressSidebarLinkProps {
|
|
17
|
+
readonly addresses: readonly PostalAddress[]
|
|
18
|
+
readonly onOpen: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The primary address as a single sidebar link. Clicking it opens the Addresses
|
|
23
|
+
* tab; the editor itself does not live in the aside.
|
|
24
|
+
*/
|
|
25
|
+
export function AddressSidebarLink({
|
|
26
|
+
addresses,
|
|
27
|
+
onOpen,
|
|
28
|
+
}: AddressSidebarLinkProps): React.JSX.Element {
|
|
29
|
+
const address = primaryAddress(addresses)
|
|
30
|
+
const text = address === undefined ? 'Add address…' : formatAddress(address)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<SidebarField label="Address">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={onOpen}
|
|
37
|
+
className="text-left text-[12px] font-medium text-accent transition hover:text-accent-hover hover:underline"
|
|
38
|
+
>
|
|
39
|
+
{text}
|
|
40
|
+
</button>
|
|
41
|
+
</SidebarField>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface AddressDraft {
|
|
46
|
+
readonly line1: string
|
|
47
|
+
readonly line2: string
|
|
48
|
+
readonly city: string
|
|
49
|
+
readonly region: string
|
|
50
|
+
readonly postalCode: string
|
|
51
|
+
readonly country: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function emptyDraft(): AddressDraft {
|
|
55
|
+
return { line1: '', line2: '', city: '', region: '', postalCode: '', country: '' }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fromAddress(address: PostalAddress): AddressDraft {
|
|
59
|
+
return {
|
|
60
|
+
line1: address.line1 ?? '',
|
|
61
|
+
line2: address.line2 ?? '',
|
|
62
|
+
city: address.city ?? '',
|
|
63
|
+
region: address.region ?? '',
|
|
64
|
+
postalCode: address.postalCode ?? '',
|
|
65
|
+
country: address.country ?? '',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function blankToNull(value: string): string | null {
|
|
70
|
+
const trimmed = value.trim()
|
|
71
|
+
|
|
72
|
+
return trimmed.length === 0 ? null : trimmed
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function partsFromDraft(draft: AddressDraft): {
|
|
76
|
+
readonly line1: string | null
|
|
77
|
+
readonly line2: string | null
|
|
78
|
+
readonly city: string | null
|
|
79
|
+
readonly region: string | null
|
|
80
|
+
readonly postalCode: string | null
|
|
81
|
+
readonly country: string | null
|
|
82
|
+
} {
|
|
83
|
+
return {
|
|
84
|
+
line1: blankToNull(draft.line1),
|
|
85
|
+
line2: blankToNull(draft.line2),
|
|
86
|
+
city: blankToNull(draft.city),
|
|
87
|
+
region: blankToNull(draft.region),
|
|
88
|
+
postalCode: blankToNull(draft.postalCode),
|
|
89
|
+
country: blankToNull(draft.country),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function draftHasLocation(draft: AddressDraft): boolean {
|
|
94
|
+
return Object.values(partsFromDraft(draft)).some((part) => part !== null)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withPrimary<Kind extends string>(
|
|
98
|
+
addresses: readonly PostalAddress<Kind>[],
|
|
99
|
+
kind: Kind,
|
|
100
|
+
): readonly PostalAddress<Kind>[] {
|
|
101
|
+
return addresses.map((address) => ({ ...address, primary: address.kind === kind }))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function withoutKind<Kind extends string>(
|
|
105
|
+
addresses: readonly PostalAddress<Kind>[],
|
|
106
|
+
kind: Kind,
|
|
107
|
+
): readonly PostalAddress<Kind>[] {
|
|
108
|
+
const next = addresses.filter((address) => address.kind !== kind)
|
|
109
|
+
|
|
110
|
+
if (next.length === 0 || next.some((address) => address.primary)) {
|
|
111
|
+
return next
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return next.map((address, index) => ({ ...address, primary: index === 0 }))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function AddressPartInputs({
|
|
118
|
+
draft,
|
|
119
|
+
onChange,
|
|
120
|
+
}: {
|
|
121
|
+
readonly draft: AddressDraft
|
|
122
|
+
readonly onChange: (draft: AddressDraft) => void
|
|
123
|
+
}): React.JSX.Element {
|
|
124
|
+
const inputClass =
|
|
125
|
+
'w-full rounded-md border border-border bg-surface-raised px-2 py-1 text-[12px] outline-none focus:border-accent focus:ring-2 focus:ring-accent/20'
|
|
126
|
+
|
|
127
|
+
function set(field: keyof AddressDraft, value: string): void {
|
|
128
|
+
onChange({ ...draft, [field]: value })
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="space-y-1.5">
|
|
133
|
+
<input
|
|
134
|
+
value={draft.line1}
|
|
135
|
+
onChange={(event) => {
|
|
136
|
+
set('line1', event.target.value)
|
|
137
|
+
}}
|
|
138
|
+
placeholder="Line 1"
|
|
139
|
+
className={inputClass}
|
|
140
|
+
/>
|
|
141
|
+
<input
|
|
142
|
+
value={draft.line2}
|
|
143
|
+
onChange={(event) => {
|
|
144
|
+
set('line2', event.target.value)
|
|
145
|
+
}}
|
|
146
|
+
placeholder="Line 2"
|
|
147
|
+
className={inputClass}
|
|
148
|
+
/>
|
|
149
|
+
<input
|
|
150
|
+
value={draft.city}
|
|
151
|
+
onChange={(event) => {
|
|
152
|
+
set('city', event.target.value)
|
|
153
|
+
}}
|
|
154
|
+
placeholder="City"
|
|
155
|
+
className={inputClass}
|
|
156
|
+
/>
|
|
157
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
158
|
+
<input
|
|
159
|
+
value={draft.region}
|
|
160
|
+
onChange={(event) => {
|
|
161
|
+
set('region', event.target.value)
|
|
162
|
+
}}
|
|
163
|
+
placeholder="Region"
|
|
164
|
+
className={inputClass}
|
|
165
|
+
/>
|
|
166
|
+
<input
|
|
167
|
+
value={draft.postalCode}
|
|
168
|
+
onChange={(event) => {
|
|
169
|
+
set('postalCode', event.target.value)
|
|
170
|
+
}}
|
|
171
|
+
placeholder="Postal code"
|
|
172
|
+
className={inputClass}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<CountrySearch
|
|
176
|
+
value={draft.country}
|
|
177
|
+
onChange={(country) => {
|
|
178
|
+
set('country', country)
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Add, edit, and remove labelled postal addresses. One entry per kind. The first
|
|
187
|
+
* added address is primary; marking another one primary clears the old mark;
|
|
188
|
+
* removing the primary promotes the first remaining entry.
|
|
189
|
+
*/
|
|
190
|
+
export function AddressesField<Kind extends string>({
|
|
191
|
+
value,
|
|
192
|
+
kinds,
|
|
193
|
+
kindLabels,
|
|
194
|
+
onChange,
|
|
195
|
+
}: AddressesFieldProps<Kind>): React.JSX.Element {
|
|
196
|
+
const listId = useId()
|
|
197
|
+
const rootRef = useRef<HTMLDivElement>(null)
|
|
198
|
+
const [open, setOpen] = useState(false)
|
|
199
|
+
const [query, setQuery] = useState('')
|
|
200
|
+
const [highlight, setHighlight] = useState(0)
|
|
201
|
+
const [pendingKind, setPendingKind] = useState<Kind | null>(null)
|
|
202
|
+
const [pendingDraft, setPendingDraft] = useState<AddressDraft>(emptyDraft)
|
|
203
|
+
const [editingKind, setEditingKind] = useState<Kind | null>(null)
|
|
204
|
+
const [editDraft, setEditDraft] = useState<AddressDraft>(emptyDraft)
|
|
205
|
+
const [editPrimary, setEditPrimary] = useState(false)
|
|
206
|
+
|
|
207
|
+
const available = useMemo(() => {
|
|
208
|
+
const used = new Set(value.map((address) => address.kind))
|
|
209
|
+
|
|
210
|
+
return kinds.filter((kind) => !used.has(kind))
|
|
211
|
+
}, [kinds, value])
|
|
212
|
+
|
|
213
|
+
const filtered = useMemo(() => {
|
|
214
|
+
const term = query.trim().toLowerCase()
|
|
215
|
+
|
|
216
|
+
return term.length === 0
|
|
217
|
+
? available
|
|
218
|
+
: available.filter((kind) => kindLabels[kind].toLowerCase().includes(term))
|
|
219
|
+
}, [available, kindLabels, query])
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
if (!open) {
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function onPointerDown(event: MouseEvent): void {
|
|
227
|
+
if (rootRef.current?.contains(event.target as Node) !== true) {
|
|
228
|
+
setOpen(false)
|
|
229
|
+
setQuery('')
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
document.addEventListener('mousedown', onPointerDown)
|
|
234
|
+
|
|
235
|
+
return () => {
|
|
236
|
+
document.removeEventListener('mousedown', onPointerDown)
|
|
237
|
+
}
|
|
238
|
+
}, [open])
|
|
239
|
+
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
setHighlight(0)
|
|
242
|
+
}, [query, open])
|
|
243
|
+
|
|
244
|
+
function pickKind(kind: Kind): void {
|
|
245
|
+
setEditingKind(null)
|
|
246
|
+
setPendingKind(kind)
|
|
247
|
+
setPendingDraft(emptyDraft())
|
|
248
|
+
setOpen(false)
|
|
249
|
+
setQuery('')
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function cancelPending(): void {
|
|
253
|
+
setPendingKind(null)
|
|
254
|
+
setPendingDraft(emptyDraft())
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function commitPending(): void {
|
|
258
|
+
if (pendingKind === null || !draftHasLocation(pendingDraft)) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const next: PostalAddress<Kind> = {
|
|
263
|
+
kind: pendingKind,
|
|
264
|
+
...partsFromDraft(pendingDraft),
|
|
265
|
+
primary: value.length === 0,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onChange(value.length === 0 ? [next] : [...value.filter((address) => address.kind !== pendingKind), next])
|
|
269
|
+
cancelPending()
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cancelEdit(): void {
|
|
273
|
+
setEditingKind(null)
|
|
274
|
+
setEditDraft(emptyDraft())
|
|
275
|
+
setEditPrimary(false)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function commitEdit(): void {
|
|
279
|
+
if (editingKind === null) {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!draftHasLocation(editDraft)) {
|
|
284
|
+
onChange(withoutKind(value, editingKind))
|
|
285
|
+
cancelEdit()
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const next = value.map((address) =>
|
|
290
|
+
address.kind === editingKind
|
|
291
|
+
? { ...address, ...partsFromDraft(editDraft) }
|
|
292
|
+
: address,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
onChange(editPrimary ? withPrimary(next, editingKind) : next)
|
|
296
|
+
cancelEdit()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="space-y-0.5">
|
|
301
|
+
<ul>
|
|
302
|
+
{value.map((address) => {
|
|
303
|
+
const label = kindLabels[address.kind]
|
|
304
|
+
|
|
305
|
+
if (editingKind === address.kind) {
|
|
306
|
+
return (
|
|
307
|
+
<li key={address.kind} className="py-1">
|
|
308
|
+
<div className="mb-1 flex items-center gap-1.5">
|
|
309
|
+
<span className="text-[11px] font-medium text-ink-muted">{label}</span>
|
|
310
|
+
{editPrimary && <Chip tone="accent">Primary</Chip>}
|
|
311
|
+
</div>
|
|
312
|
+
<AddressPartInputs draft={editDraft} onChange={setEditDraft} />
|
|
313
|
+
<div className="mt-1.5 flex items-center justify-end gap-1.5">
|
|
314
|
+
{!editPrimary && (
|
|
315
|
+
<button
|
|
316
|
+
type="button"
|
|
317
|
+
onClick={() => {
|
|
318
|
+
setEditPrimary(true)
|
|
319
|
+
}}
|
|
320
|
+
className="mr-auto rounded-md px-2 py-1 text-[11px] font-medium text-ink-muted hover:text-ink"
|
|
321
|
+
>
|
|
322
|
+
Make Primary
|
|
323
|
+
</button>
|
|
324
|
+
)}
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
onClick={cancelEdit}
|
|
328
|
+
className="rounded-md px-2 py-1 text-[11px] font-medium text-ink-muted hover:text-ink"
|
|
329
|
+
>
|
|
330
|
+
Cancel
|
|
331
|
+
</button>
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
onClick={commitEdit}
|
|
335
|
+
className="rounded-md bg-accent px-2.5 py-1 text-[11px] font-semibold text-accent-fg hover:bg-accent-hover"
|
|
336
|
+
>
|
|
337
|
+
Save
|
|
338
|
+
</button>
|
|
339
|
+
</div>
|
|
340
|
+
</li>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const summary = formatAddress(address)
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<li
|
|
348
|
+
key={address.kind}
|
|
349
|
+
className="-mx-1 rounded-md px-1 py-1 hover:bg-surface"
|
|
350
|
+
>
|
|
351
|
+
<div className="flex items-start gap-1">
|
|
352
|
+
<div className="min-w-0 flex-1">
|
|
353
|
+
<div className="flex items-center gap-1.5 text-[12px] leading-snug text-ink">
|
|
354
|
+
<span className="font-medium">{label}</span>
|
|
355
|
+
{address.primary && <Chip tone="accent">Primary</Chip>}
|
|
356
|
+
</div>
|
|
357
|
+
{summary.length > 0 && (
|
|
358
|
+
<div className="text-[12px] leading-snug text-ink-muted">{summary}</div>
|
|
359
|
+
)}
|
|
360
|
+
</div>
|
|
361
|
+
<div className="flex shrink-0 items-center">
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => {
|
|
365
|
+
cancelPending()
|
|
366
|
+
setEditingKind(address.kind)
|
|
367
|
+
setEditDraft(fromAddress(address))
|
|
368
|
+
setEditPrimary(address.primary)
|
|
369
|
+
}}
|
|
370
|
+
className="rounded px-1.5 py-0.5 text-[11px] font-medium text-ink-faint hover:bg-surface-raised hover:text-ink"
|
|
371
|
+
>
|
|
372
|
+
Edit
|
|
373
|
+
</button>
|
|
374
|
+
<button
|
|
375
|
+
type="button"
|
|
376
|
+
onClick={() => {
|
|
377
|
+
if (editingKind === address.kind) {
|
|
378
|
+
cancelEdit()
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
onChange(withoutKind(value, address.kind))
|
|
382
|
+
}}
|
|
383
|
+
className="rounded px-1 py-0.5 text-[13px] leading-none text-ink-faint hover:bg-danger-soft hover:text-danger"
|
|
384
|
+
aria-label={`Remove ${label}`}
|
|
385
|
+
>
|
|
386
|
+
×
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</li>
|
|
391
|
+
)
|
|
392
|
+
})}
|
|
393
|
+
</ul>
|
|
394
|
+
|
|
395
|
+
{value.length === 0 && pendingKind === null && (
|
|
396
|
+
<p className="py-0.5 text-[12px] text-ink-faint">None yet</p>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{pendingKind !== null ? (
|
|
400
|
+
<div className="mt-1 space-y-1.5 rounded-md border border-border bg-surface px-2 py-2">
|
|
401
|
+
<div className="text-[11px] font-medium text-ink">{kindLabels[pendingKind]}</div>
|
|
402
|
+
<AddressPartInputs draft={pendingDraft} onChange={setPendingDraft} />
|
|
403
|
+
<div className="flex justify-end gap-1.5">
|
|
404
|
+
<button
|
|
405
|
+
type="button"
|
|
406
|
+
onClick={cancelPending}
|
|
407
|
+
className="rounded-md px-2 py-1 text-[11px] font-medium text-ink-muted hover:text-ink"
|
|
408
|
+
>
|
|
409
|
+
Cancel
|
|
410
|
+
</button>
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
onClick={commitPending}
|
|
414
|
+
disabled={!draftHasLocation(pendingDraft)}
|
|
415
|
+
className="rounded-md bg-accent px-2.5 py-1 text-[11px] font-semibold text-accent-fg hover:bg-accent-hover disabled:opacity-40"
|
|
416
|
+
>
|
|
417
|
+
Add
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
) : (
|
|
422
|
+
available.length > 0 && (
|
|
423
|
+
<div ref={rootRef} className="relative">
|
|
424
|
+
<button
|
|
425
|
+
type="button"
|
|
426
|
+
onClick={() => {
|
|
427
|
+
setOpen((current) => !current)
|
|
428
|
+
setQuery('')
|
|
429
|
+
}}
|
|
430
|
+
className="mt-0.5 -mx-1 rounded-md px-1 py-1 text-[12px] font-medium text-ink-muted transition hover:bg-surface hover:text-accent"
|
|
431
|
+
>
|
|
432
|
+
+ Add
|
|
433
|
+
</button>
|
|
434
|
+
|
|
435
|
+
{open && (
|
|
436
|
+
<div className="absolute z-20 mt-0.5 w-full overflow-hidden rounded-md border border-border bg-surface-raised">
|
|
437
|
+
<input
|
|
438
|
+
value={query}
|
|
439
|
+
onChange={(event) => {
|
|
440
|
+
setQuery(event.target.value)
|
|
441
|
+
}}
|
|
442
|
+
onKeyDown={(event) => {
|
|
443
|
+
if (event.key === 'ArrowDown') {
|
|
444
|
+
event.preventDefault()
|
|
445
|
+
setHighlight((current) =>
|
|
446
|
+
Math.min(current + 1, Math.max(filtered.length - 1, 0)),
|
|
447
|
+
)
|
|
448
|
+
} else if (event.key === 'ArrowUp') {
|
|
449
|
+
event.preventDefault()
|
|
450
|
+
setHighlight((current) => Math.max(current - 1, 0))
|
|
451
|
+
} else if (event.key === 'Enter') {
|
|
452
|
+
event.preventDefault()
|
|
453
|
+
const hit = filtered[highlight]
|
|
454
|
+
|
|
455
|
+
if (hit !== undefined) {
|
|
456
|
+
pickKind(hit)
|
|
457
|
+
}
|
|
458
|
+
} else if (event.key === 'Escape') {
|
|
459
|
+
setOpen(false)
|
|
460
|
+
setQuery('')
|
|
461
|
+
}
|
|
462
|
+
}}
|
|
463
|
+
placeholder="Search kinds…"
|
|
464
|
+
role="combobox"
|
|
465
|
+
aria-expanded={open}
|
|
466
|
+
aria-controls={listId}
|
|
467
|
+
aria-autocomplete="list"
|
|
468
|
+
className="w-full border-b border-border px-2.5 py-1.5 text-[12px] outline-none focus:bg-accent-soft/30"
|
|
469
|
+
/>
|
|
470
|
+
<ul id={listId} role="listbox" className="max-h-48 overflow-auto py-1">
|
|
471
|
+
{filtered.length === 0 ? (
|
|
472
|
+
<li className="px-3 py-2 text-[12px] text-ink-faint">No kinds match</li>
|
|
473
|
+
) : (
|
|
474
|
+
filtered.map((kind, index) => (
|
|
475
|
+
<li key={kind} role="option" aria-selected={index === highlight}>
|
|
476
|
+
<button
|
|
477
|
+
type="button"
|
|
478
|
+
onMouseEnter={() => {
|
|
479
|
+
setHighlight(index)
|
|
480
|
+
}}
|
|
481
|
+
onClick={() => {
|
|
482
|
+
pickKind(kind)
|
|
483
|
+
}}
|
|
484
|
+
className={[
|
|
485
|
+
'flex w-full px-3 py-1.5 text-left text-[12px] text-ink',
|
|
486
|
+
index === highlight ? 'bg-accent-soft' : 'hover:bg-surface',
|
|
487
|
+
].join(' ')}
|
|
488
|
+
>
|
|
489
|
+
{kindLabels[kind]}
|
|
490
|
+
</button>
|
|
491
|
+
</li>
|
|
492
|
+
))
|
|
493
|
+
)}
|
|
494
|
+
</ul>
|
|
495
|
+
</div>
|
|
496
|
+
)}
|
|
497
|
+
</div>
|
|
498
|
+
)
|
|
499
|
+
)}
|
|
500
|
+
</div>
|
|
501
|
+
)
|
|
502
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { COUNTRIES, countryName } from '@kelpie/schemas'
|
|
2
|
+
import type { JSX } from 'react'
|
|
3
|
+
|
|
4
|
+
import { EntitySearch } from './EntitySearch.tsx'
|
|
5
|
+
|
|
6
|
+
export interface CountrySearchProps {
|
|
7
|
+
readonly value: string
|
|
8
|
+
readonly onChange: (country: string) => void
|
|
9
|
+
readonly disabled?: boolean | undefined
|
|
10
|
+
readonly size?: 'sm' | 'md' | undefined
|
|
11
|
+
readonly className?: string | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function countryOptions(
|
|
15
|
+
current: string,
|
|
16
|
+
): readonly { readonly id: string; readonly label: string; readonly meta?: string }[] {
|
|
17
|
+
const options = COUNTRIES.map((country) => ({
|
|
18
|
+
id: country.code,
|
|
19
|
+
label: `${country.name} (${country.code})`,
|
|
20
|
+
...(country.aliases === undefined ? {} : { meta: country.aliases.join(', ') }),
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
if (current.length === 0 || COUNTRIES.some((country) => country.code === current)) {
|
|
24
|
+
return options
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const name = countryName(current) ?? current
|
|
28
|
+
|
|
29
|
+
return [{ id: current, label: `${name} (${current})` }, ...options]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Searchable ISO 3166-1 alpha-2 picker.
|
|
34
|
+
*
|
|
35
|
+
* Accepts a stored code. The list searches English names, codes, and aliases.
|
|
36
|
+
* Clearing writes an empty string so the parent can store null.
|
|
37
|
+
*/
|
|
38
|
+
export function CountrySearch({
|
|
39
|
+
value,
|
|
40
|
+
onChange,
|
|
41
|
+
disabled,
|
|
42
|
+
size = 'sm',
|
|
43
|
+
className = '',
|
|
44
|
+
}: CountrySearchProps): JSX.Element {
|
|
45
|
+
if (disabled === true) {
|
|
46
|
+
const padding = size === 'md' ? 'px-2.5 py-1.5 text-[13px]' : 'px-2 py-1.5 text-[12px]'
|
|
47
|
+
const label =
|
|
48
|
+
value.length === 0 ? '—' : `${countryName(value) ?? value}${value.length === 2 ? ` (${value})` : ''}`
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={`w-full rounded-md border border-border bg-surface-raised text-ink opacity-60 ${padding} ${className}`}
|
|
53
|
+
>
|
|
54
|
+
{label}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<EntitySearch
|
|
61
|
+
options={countryOptions(value)}
|
|
62
|
+
value={value}
|
|
63
|
+
onChange={onChange}
|
|
64
|
+
placeholder="Search countries…"
|
|
65
|
+
emptyMessage="No matching countries"
|
|
66
|
+
limit={20}
|
|
67
|
+
size={size}
|
|
68
|
+
className={className}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { usePublicConfig } from '../api/resources/publicConfig.ts'
|
|
2
|
+
import { environmentTone, type EnvironmentTone } from './environmentTone.ts'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* A thin strip above the app that names the site
|
|
5
|
+
* A thin strip above the app that names the site and paints it in a colour
|
|
6
|
+
* that belongs to that name.
|
|
5
7
|
*
|
|
6
8
|
* Renders nothing on production, whether or not a site name was set: the
|
|
7
|
-
* banner exists to tell dev, demo, and any other non-production
|
|
8
|
-
* apart.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* banner exists to tell dev, demo, cloud, and any other non-production
|
|
10
|
+
* install apart. Colour does the first half of that work; the label does
|
|
11
|
+
* the rest. Renders nothing while the metadata is loading or on error
|
|
12
|
+
* either, so a slow config request never pushes the app down half a
|
|
13
|
+
* second after boot.
|
|
11
14
|
*
|
|
12
15
|
* Mounted at the top of `KelpieApp`, above the router, so it shows on the
|
|
13
16
|
* sign-in and onboarding pages as well as the main app.
|
|
14
17
|
*/
|
|
15
18
|
|
|
19
|
+
const TONE_CLASSES: Readonly<Record<EnvironmentTone, string>> = {
|
|
20
|
+
dev: 'bg-env-dev text-env-dev-fg',
|
|
21
|
+
demo: 'bg-env-demo text-env-demo-fg',
|
|
22
|
+
staging: 'bg-env-staging text-env-staging-fg',
|
|
23
|
+
test: 'bg-env-test text-env-test-fg',
|
|
24
|
+
cloud: 'bg-env-cloud text-env-cloud-fg',
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export function EnvironmentBanner(): React.JSX.Element | null {
|
|
17
28
|
const { config } = usePublicConfig()
|
|
18
29
|
|
|
@@ -21,9 +32,15 @@ export function EnvironmentBanner(): React.JSX.Element | null {
|
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
const label = config.siteName ?? config.runtimeMode
|
|
35
|
+
const tone = environmentTone(label)
|
|
36
|
+
|
|
37
|
+
const className = [
|
|
38
|
+
'flex h-5 shrink-0 items-center justify-center px-2 text-[11px] font-medium tracking-wide uppercase',
|
|
39
|
+
TONE_CLASSES[tone],
|
|
40
|
+
].join(' ')
|
|
24
41
|
|
|
25
42
|
return (
|
|
26
|
-
<div className=
|
|
43
|
+
<div data-tone={tone} className={className}>
|
|
27
44
|
{label}
|
|
28
45
|
</div>
|
|
29
46
|
)
|
|
@@ -22,7 +22,12 @@ interface TextProps extends CommonProps {
|
|
|
22
22
|
readonly value: string
|
|
23
23
|
readonly onChange: (value: string) => void
|
|
24
24
|
readonly multiline?: boolean
|
|
25
|
-
readonly type?: 'text' | 'number' | 'date' | 'url' | 'email'
|
|
25
|
+
readonly type?: 'text' | 'number' | 'date' | 'datetime-local' | 'url' | 'email'
|
|
26
|
+
/**
|
|
27
|
+
* When the value is set, hover shows Visit (opens the address) and Edit
|
|
28
|
+
* instead of making the whole value a click-to-edit control.
|
|
29
|
+
*/
|
|
30
|
+
readonly visitable?: boolean
|
|
26
31
|
readonly options?: never
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -32,10 +37,18 @@ interface SelectProps extends CommonProps {
|
|
|
32
37
|
readonly options: readonly { readonly value: string; readonly label: string }[]
|
|
33
38
|
readonly multiline?: never
|
|
34
39
|
readonly type?: never
|
|
40
|
+
readonly visitable?: never
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
export type InlineEditProps = TextProps | SelectProps
|
|
38
44
|
|
|
45
|
+
const ACTION_CLASS =
|
|
46
|
+
'rounded-md px-2 py-0.5 text-[11px] font-medium text-ink-muted hover:bg-surface-sunken hover:text-ink'
|
|
47
|
+
|
|
48
|
+
function hrefForVisit(value: string): string {
|
|
49
|
+
return /^https?:\/\//iu.test(value) ? value : `https://${value}`
|
|
50
|
+
}
|
|
51
|
+
|
|
39
52
|
export function InlineEdit(props: InlineEditProps): React.JSX.Element {
|
|
40
53
|
const {
|
|
41
54
|
value,
|
|
@@ -158,6 +171,27 @@ export function InlineEdit(props: InlineEditProps): React.JSX.Element {
|
|
|
158
171
|
|
|
159
172
|
const isEmpty = value.length === 0 || value === '—'
|
|
160
173
|
|
|
174
|
+
if (props.options === undefined && props.visitable === true && !isEmpty) {
|
|
175
|
+
return (
|
|
176
|
+
<div className={`group relative inline-flex max-w-full items-baseline ${className}`.trim()}>
|
|
177
|
+
<span className={`text-[13px] leading-snug ${displayClassName}`.trim()}>{display ?? value}</span>
|
|
178
|
+
<span className="absolute left-full top-1/2 flex -translate-y-1/2 gap-0.5 pl-1 whitespace-nowrap opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100">
|
|
179
|
+
<a
|
|
180
|
+
href={hrefForVisit(value)}
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
className={`${ACTION_CLASS} hover:text-accent`}
|
|
184
|
+
>
|
|
185
|
+
Visit
|
|
186
|
+
</a>
|
|
187
|
+
<button type="button" onClick={() => setEditing(true)} className={ACTION_CLASS}>
|
|
188
|
+
Edit
|
|
189
|
+
</button>
|
|
190
|
+
</span>
|
|
191
|
+
</div>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
161
195
|
return (
|
|
162
196
|
<button
|
|
163
197
|
type="button"
|