mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 17:19:52 +07:00
initial create admission, fix view detail admission
This commit is contained in:
parent
a4c2e0ec5f
commit
aee9ea4b8f
@ -8,8 +8,17 @@ const optionsMode = {
|
||||
const res = await fetch(field.optionsEndpoint);
|
||||
const json = await res.json();
|
||||
|
||||
selectOptions[field.key] = json?.data ?? [];
|
||||
// selectOptions[field.key] = json?.data ?? [];
|
||||
|
||||
const data = json?.data ?? [];
|
||||
const valueKey = field.valueKey ?? 'value';
|
||||
const labelKey = field.labelKey ?? 'label';
|
||||
|
||||
selectOptions[field.key] = data.map((item) => ({
|
||||
value: item[valueKey],
|
||||
label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey],
|
||||
}));
|
||||
console.log(selectOptions);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch options for", field.key, err);
|
||||
selectOptions[field.key] = [];
|
||||
@ -52,8 +61,17 @@ const optionsMode = {
|
||||
const res = await fetch(endpoint);
|
||||
const json = await res.json();
|
||||
|
||||
selectOptions[field.key] = json?.data ?? [];
|
||||
// selectOptions[field.key] = json?.data ?? [];
|
||||
|
||||
const data = json?.data ?? [];
|
||||
const valueKey = field.valueKey ?? 'value';
|
||||
const labelKey = field.labelKey ?? 'label';
|
||||
|
||||
selectOptions[field.key] = data.map((item) => ({
|
||||
value: item[valueKey],
|
||||
label: typeof labelKey === 'function' ? labelKey(item) : item[labelKey],
|
||||
}));
|
||||
console.log(selectOptions);
|
||||
// Track last fetched parent value for dependent fields
|
||||
if (field.dependsOn) {
|
||||
lastFetched[field.key] = parentValue;
|
||||
|
||||
@ -13,17 +13,10 @@ export async function getVisit(searchQuery) {
|
||||
return await getById(API.PATVISIT, searchQuery)
|
||||
}
|
||||
|
||||
export async function getPatient(searchQuery) {
|
||||
const { data: patient, error } = await getById(API.PATIENTS, searchQuery)
|
||||
return { patient };
|
||||
export async function createAdmission(newAdmissionForm) {
|
||||
return await create(API.PATVISIT, newAdmissionForm)
|
||||
}
|
||||
|
||||
export async function createPatient(newContactForm) {
|
||||
// console.log(JSON.stringify(newContactForm));
|
||||
return await create(API.PATIENTS, newContactForm)
|
||||
}
|
||||
|
||||
export async function editPatient(editContactForm) {
|
||||
// console.log(JSON.stringify(editContactForm));
|
||||
return await update(API.PATIENTS, editContactForm)
|
||||
export async function editAdmission(editAdmissionForm) {
|
||||
return await update(API.PATVISIT, editAdmissionForm)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import Settings2Icon from "@lucide/svelte/icons/settings-2";
|
||||
import PencilIcon from "@lucide/svelte/icons/pencil";
|
||||
|
||||
export const searchFields = [
|
||||
{
|
||||
@ -38,6 +39,66 @@ export const searchFields = [
|
||||
},
|
||||
];
|
||||
|
||||
// export const detailSections = [
|
||||
// {
|
||||
// class: "grid grid-cols-1 sm:grid-cols-2 gap-3",
|
||||
// fields: [
|
||||
// { key: "PVID", label: "Visit ID" },
|
||||
// { key: "EpisodeID", label: "Episode ID" },
|
||||
// { key: "", label: "Visit Class" },
|
||||
// { key: "", label: "Service Class" },
|
||||
// { key: "LocationID", label: "Location" },
|
||||
// { key: "AttDoc", label: "Attending Doctor" },
|
||||
// { key: "RefDoc", label: "Reffering Doctor" },
|
||||
// { key: "AdmDoc", label: "Admitting Doctor" },
|
||||
// { key: "CnsDoc", label: "Consulting Doctor" },
|
||||
// { key: "", label: "Admission Date", isUTCDate: true },
|
||||
// { key: "", label: "Discharge Date", isUTCDate: true },
|
||||
// { key: "Diagnosis", label: "Clinical Diagnosis" },
|
||||
// ]
|
||||
// },
|
||||
// ]
|
||||
|
||||
export const detailSections = [
|
||||
{
|
||||
title: "", // No title for top row
|
||||
class: "grid grid-cols-1 md:grid-cols-2 gap-4",
|
||||
groups: [
|
||||
{
|
||||
class: "space-y-3",
|
||||
fields: [
|
||||
{ key: "PVID", label: "Visit ID" },
|
||||
{ key: "EpisodeID", label: "Episode ID" },
|
||||
{ key: "VisitClass", label: "Visit Class" },
|
||||
{ key: "ServiceClass", label: "Service Class" },
|
||||
]
|
||||
},
|
||||
{
|
||||
class: "space-y-3",
|
||||
fields: [
|
||||
{ key: "LocationID", label: "Location" },
|
||||
{ key: "AttDoc", label: "Attending Doctor" },
|
||||
{ key: "RefDoc", label: "Referring Doctor" },
|
||||
{ key: "AdmDoc", label: "Admitting Doctor" },
|
||||
{ key: "CnsDoc", label: "Consulting Doctor" },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
class: "grid grid-cols-2 gap-4 items-center",
|
||||
fields: [
|
||||
{ key: "AdmissionDate", label: "Admission Date", isUTCDate: true },
|
||||
{ key: "DischargeDate", label: "Discharge Date", isUTCDate: true },
|
||||
]
|
||||
},
|
||||
{
|
||||
fields: [
|
||||
{ key: "Diagnosis", label: "Clinical Diagnosis" },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export function admissionActions(masterDetail) {
|
||||
return [
|
||||
{
|
||||
@ -51,4 +112,15 @@ export function admissionActions(masterDetail) {
|
||||
popoverWidth: "w-256",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function viewActions(handlers){
|
||||
return [
|
||||
{
|
||||
Icon: PencilIcon,
|
||||
label: 'Edit Patient',
|
||||
onClick: handlers.editPatient,
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,209 @@
|
||||
import { API } from "$lib/config/api";
|
||||
import EraserIcon from "@lucide/svelte/icons/eraser";
|
||||
import { z } from "zod";
|
||||
|
||||
export const admissionSchema = z.object({});
|
||||
|
||||
export const admissionInitialForm = {
|
||||
InternalPVID: "",
|
||||
InternalPID: "",
|
||||
PVID: "",
|
||||
EpisodeID: "",
|
||||
DiagCode: "",
|
||||
Diagnosis: "",
|
||||
ADTCode: "",
|
||||
LocationID: "",
|
||||
AttDoc: "",
|
||||
RefDoc: "",
|
||||
AdmDoc: "",
|
||||
CnsDoc: "",
|
||||
};
|
||||
|
||||
export const admissionDefaultErrors = {};
|
||||
|
||||
export const admissionFormFields = [
|
||||
{
|
||||
title: "Visit Information",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "PVID",
|
||||
label: "Visit ID",
|
||||
required: false,
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
key: "EpisodeID",
|
||||
label: "Episode ID",
|
||||
required: false,
|
||||
type: "text",
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Medical Team",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "AttDoc",
|
||||
label: "Attended Doctor",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||
valueKey: "ContactID",
|
||||
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||
},
|
||||
{
|
||||
key: "RefDoc",
|
||||
label: "Reference Doctor",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||
valueKey: "ContactID",
|
||||
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "AdmDoc",
|
||||
label: "Admitted Doctor",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||
valueKey: "ContactID",
|
||||
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||
},
|
||||
{
|
||||
key: "CnsDoc",
|
||||
label: "Consulte Doctor",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.CONTACT}`,
|
||||
valueKey: "ContactID",
|
||||
labelKey: (item) => `${item.Initial} - ${item.NameFirst} ${item.NameLast}`,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Visit Classification",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "LocationID",
|
||||
label: "Location",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.LOCATION}`,
|
||||
valueKey: "LocationID",
|
||||
labelKey: (item) => `${item.LocCode} - ${item.LocFull}`,
|
||||
},
|
||||
{
|
||||
key: "VisitClass",
|
||||
label: "Visit Class",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/visit_classes`,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "ServiceClass",
|
||||
label: "Service Class",
|
||||
required: false,
|
||||
type: "select",
|
||||
optionsEndpoint: `${API.BASE_URL}${API.VALUESET}/service_classes`,
|
||||
},
|
||||
{
|
||||
key: "Discharge",
|
||||
label: "Discharge Status",
|
||||
required: false,
|
||||
type: "toggle",
|
||||
defaultValue: false,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Clinical Information",
|
||||
rows: [
|
||||
{
|
||||
type: "row",
|
||||
columns: [
|
||||
{
|
||||
key: "Diagnosis",
|
||||
label: "Diagnosis",
|
||||
required: false,
|
||||
type: "textarea",
|
||||
rows: 4,
|
||||
maxLength: 1000,
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export function getAdmissionFormActions(handlers) {
|
||||
return [
|
||||
{
|
||||
Icon: EraserIcon,
|
||||
label: 'Clear Form',
|
||||
onClick: handlers.clearForm,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const admissionTemplate = {
|
||||
PVID: 'PVID',
|
||||
InternalPID: 'InternalPID',
|
||||
EpisodeID: 'EpisodeID',
|
||||
PatDiag: {
|
||||
DiagCode: 'DiagCode',
|
||||
Diagnosis: 'Diagnosis',
|
||||
},
|
||||
PatVisitADT: {
|
||||
ADTCode: () => 'A04',
|
||||
LocationID: 'LocationID',
|
||||
AttDoc: 'AttDoc',
|
||||
RefDoc: 'RefDoc',
|
||||
AdmDoc: 'AdmDoc',
|
||||
CnsDoc: 'CnsDoc',
|
||||
},
|
||||
};
|
||||
|
||||
export function buildPayload(form, schema = admissionTemplate) {
|
||||
const payload = {};
|
||||
|
||||
for (const [key, config] of Object.entries(schema)) {
|
||||
if (typeof config === 'string') {
|
||||
// Kirim nilai dari form, atau null jika tidak ada (agar key tetap ada)
|
||||
payload[key] = form[config] ?? null;
|
||||
}
|
||||
else if (typeof config === 'function') {
|
||||
payload[key] = config(form);
|
||||
}
|
||||
else if (typeof config === 'object' && config !== null) {
|
||||
// Rekursif tanpa pengecekan panjang keys, agar objek nested selalu dibuat
|
||||
payload[key] = buildPayload(form, config);
|
||||
}
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
@ -1 +1,60 @@
|
||||
cr
|
||||
<script>
|
||||
import { useForm } from "$lib/components/composable/use-form.svelte";
|
||||
import { admissionSchema, admissionInitialForm, admissionDefaultErrors, admissionFormFields, getAdmissionFormActions, buildPayload } from "$lib/components/patient/admission/config/admission-form-config";
|
||||
import { createAdmission } from "$lib/components/patient/admission/api/patient-admission-api";
|
||||
import { usePatientForm } from "$lib/components/composable/use-patient-form.svelte";
|
||||
import FormPageContainer from "$lib/components/patient/reusable/form-page-container.svelte";
|
||||
import PatientFormRenderer from "$lib/components/patient/reusable/patient-form-renderer.svelte";
|
||||
|
||||
let props = $props();
|
||||
|
||||
let formState = useForm({
|
||||
schema: admissionSchema,
|
||||
initialForm: admissionInitialForm,
|
||||
defaultErrors: admissionDefaultErrors,
|
||||
mode: 'create',
|
||||
modeOpt: 'default',
|
||||
saveEndpoint: createAdmission,
|
||||
editEndpoint: null,
|
||||
});
|
||||
|
||||
const helpers = usePatientForm(formState, admissionSchema);
|
||||
|
||||
const handlers = {
|
||||
clearForm: () => {
|
||||
formState.reset();
|
||||
}
|
||||
};
|
||||
|
||||
const actions = getAdmissionFormActions(handlers);
|
||||
|
||||
async function handleSave() {
|
||||
const payload = buildPayload(formState.form);
|
||||
|
||||
console.log("Payload siap kirim:", payload);
|
||||
|
||||
}
|
||||
|
||||
const primaryAction = $derived({
|
||||
label: 'Save',
|
||||
onClick: handleSave,
|
||||
disabled: helpers.hasErrors || formState.isSaving.current,
|
||||
loading: formState.isSaving.current
|
||||
});
|
||||
|
||||
const secondaryActions = [];
|
||||
|
||||
</script>
|
||||
|
||||
<FormPageContainer title="Create Admission" {primaryAction} {secondaryActions} {actions}>
|
||||
<PatientFormRenderer
|
||||
{formState}
|
||||
formFields={admissionFormFields}
|
||||
uploadErrors={helpers.uploadErrors}
|
||||
isChecking={helpers.isChecking}
|
||||
linkToDisplay={helpers.linkToDisplay}
|
||||
validateIdentifier={helpers.validateIdentifier}
|
||||
validateFieldAsync={helpers.validateFieldAsync}
|
||||
mode="create"
|
||||
/>
|
||||
</FormPageContainer>
|
||||
@ -1 +1,112 @@
|
||||
vw
|
||||
<script>
|
||||
import { formatUTCDate } from "$lib/utils/formatUTCDate";
|
||||
import { detailSections, viewActions } from "$lib/components/patient/admission/config/admission-config";
|
||||
import TopbarWrapper from "$lib/components/topbar/topbar-wrapper.svelte";
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
|
||||
let props = $props();
|
||||
|
||||
let visit = $derived(props.masterDetail?.selectedItem?.data);
|
||||
|
||||
const handlers = {
|
||||
editPatient: () => props.masterDetail.enterEdit(),
|
||||
};
|
||||
|
||||
const actions = viewActions(handlers);
|
||||
|
||||
function getFieldValue(field) {
|
||||
if (!visit) return "-";
|
||||
|
||||
if (field.keys) {
|
||||
return field.keys
|
||||
.map(k => field.parentKey ? visit[field.parentKey]?.[k] : visit[k])
|
||||
.filter(val => val && val.trim() !== "")
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
return field.parentKey ? visit[field.parentKey]?.[field.key] : visit[field.key];
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#snippet Fieldset({ value, label, isUTCDate = false })}
|
||||
<div class="space-y-1.5">
|
||||
<dt class="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{label}
|
||||
</dt>
|
||||
<dd class="text-sm font-medium">
|
||||
{#if isUTCDate}
|
||||
{formatUTCDate(value)}
|
||||
{:else}
|
||||
{value ?? "-"}
|
||||
{/if}
|
||||
</dd>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if props.masterDetail.selectedItem}
|
||||
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||
<TopbarWrapper
|
||||
title={props.masterDetail.selectedItem.data.PVID}
|
||||
{actions}
|
||||
/>
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
|
||||
{#each detailSections as section}
|
||||
<div class="p-4">
|
||||
{#if section.groups}
|
||||
<div class={section.class}>
|
||||
{#each section.groups as group}
|
||||
<div>
|
||||
<div class={group.class}>
|
||||
{#each group.fields as field}
|
||||
{@render Fieldset({
|
||||
label: field.label,
|
||||
value: getFieldValue(field),
|
||||
isUTCDate: field.isUTCDate
|
||||
})}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class={section.class}>
|
||||
{#each section.fields as field}
|
||||
{@render Fieldset({
|
||||
label: field.label,
|
||||
value: getFieldValue(field),
|
||||
isUTCDate: field.isUTCDate
|
||||
})}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<ReusableEmpty desc="Select a visit to see details"/>
|
||||
{/if}
|
||||
|
||||
<!-- {#if props.masterDetail.selectedItem}
|
||||
<div class="flex flex-col px-2 py-1 gap-2 h-full w-full">
|
||||
<TopbarWrapper title={props.masterDetail.selectedItem.data.PVID} {actions} />
|
||||
<div class="flex-1 min-h-0 overflow-y-auto space-y-4">
|
||||
{#each detailSections as section}
|
||||
<div class="p-4">
|
||||
<div class={section.class}>
|
||||
{#each section.fields as field}
|
||||
{@render Fieldset({
|
||||
label: field.label,
|
||||
value: getFieldValue(field),
|
||||
isUTCDate: field.isUTCDate,
|
||||
})}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<ReusableEmpty desc="Select a visit to see details"/>
|
||||
{/if} -->
|
||||
@ -6,7 +6,6 @@
|
||||
import ReusableEmpty from "$lib/components/reusable/reusable-empty.svelte";
|
||||
|
||||
let props = $props();
|
||||
|
||||
let patient = $derived(props.masterDetail?.selectedItem?.patient);
|
||||
|
||||
const handlers = {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import * as Select from "$lib/components/ui/select/index.js";
|
||||
import * as ToggleGroup from "$lib/components/ui/toggle-group/index.js";
|
||||
import { Button } from "$lib/components/ui/button/index.js";
|
||||
import { Input } from "$lib/components/ui/input/index.js";
|
||||
import { Label } from "$lib/components/ui/label/index.js";
|
||||
@ -11,6 +12,8 @@
|
||||
import LinktoModal from "$lib/components/patient/list/modal/linkto-modal.svelte";
|
||||
import ReusableUpload from "$lib/components/reusable/reusable-upload.svelte";
|
||||
import ChevronUpIcon from "@lucide/svelte/icons/chevron-up";
|
||||
import CheckIcon from "@lucide/svelte/icons/check";
|
||||
import XIcon from "@lucide/svelte/icons/x";
|
||||
|
||||
let {
|
||||
formState,
|
||||
@ -75,7 +78,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey })}
|
||||
{#snippet Fieldset({ key, label, required, type, optionsEndpoint, options, validateOn, dependsOn, endpointParamKey, valueKey, labelKey })}
|
||||
<div class="flex w-full flex-col gap-1.5">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<Label>{label}</Label>
|
||||
@ -184,7 +187,7 @@
|
||||
onOpenChange={(open) => {
|
||||
if (open && optionsEndpoint) {
|
||||
formState.fetchOptions(
|
||||
{ key, optionsEndpoint, dependsOn, endpointParamKey },
|
||||
{ key, optionsEndpoint, dependsOn, endpointParamKey, valueKey, labelKey },
|
||||
formState.form
|
||||
);
|
||||
}
|
||||
@ -279,6 +282,26 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if type === "toggle"}
|
||||
<div class="flex items-center w-full">
|
||||
<ToggleGroup.Root variant="outline" type="single" class="w-full" >
|
||||
<ToggleGroup.Item
|
||||
value="yes"
|
||||
aria-label="Toggle Yes"
|
||||
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-green-50 data-[state=on]:text-green-700 data-[state=on]:border-green-200 data-[state=on]:*:[svg]:stroke-[5px]"
|
||||
>
|
||||
<CheckIcon class="h-4 w-4" />
|
||||
</ToggleGroup.Item>
|
||||
|
||||
<ToggleGroup.Item
|
||||
value="no"
|
||||
aria-label="Toggle No"
|
||||
class="flex gap-2 px-4 w-1/2 transition-all data-[state=on]:bg-red-50 data-[state=on]:text-red-700 data-[state=on]:border-red-200 data-[state=on]:*:[svg]:stroke-[5px]"
|
||||
>
|
||||
<XIcon class="h-6 w-6" />
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
</div>
|
||||
{:else}
|
||||
<Input
|
||||
type="text"
|
||||
|
||||
10
src/lib/components/ui/toggle-group/index.js
Normal file
10
src/lib/components/ui/toggle-group/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Root from "./toggle-group.svelte";
|
||||
import Item from "./toggle-group-item.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
Item,
|
||||
//
|
||||
Root as ToggleGroup,
|
||||
Item as ToggleGroupItem,
|
||||
};
|
||||
35
src/lib/components/ui/toggle-group/toggle-group-item.svelte
Normal file
35
src/lib/components/ui/toggle-group/toggle-group-item.svelte
Normal file
@ -0,0 +1,35 @@
|
||||
<script>
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
|
||||
import { getToggleGroupCtx } from "./toggle-group.svelte";
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { toggleVariants } from "$lib/components/ui/toggle/index.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
size,
|
||||
variant,
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
const ctx = getToggleGroupCtx();
|
||||
</script>
|
||||
|
||||
<ToggleGroupPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={ctx.variant || variant}
|
||||
data-size={ctx.size || size}
|
||||
data-spacing={ctx.spacing}
|
||||
class={cn(
|
||||
toggleVariants({
|
||||
variant: ctx.variant || variant,
|
||||
size: ctx.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10 data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{value}
|
||||
{...restProps}
|
||||
/>
|
||||
52
src/lib/components/ui/toggle-group/toggle-group.svelte
Normal file
52
src/lib/components/ui/toggle-group/toggle-group.svelte
Normal file
@ -0,0 +1,52 @@
|
||||
<script module>
|
||||
import { getContext, setContext } from "svelte";
|
||||
import { toggleVariants } from "$lib/components/ui/toggle/index.js";
|
||||
|
||||
export function setToggleGroupCtx(props) {
|
||||
setContext("toggleGroup", props);
|
||||
}
|
||||
|
||||
export function getToggleGroupCtx() {
|
||||
return getContext("toggleGroup");
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
class: className,
|
||||
size = "default",
|
||||
spacing = 0,
|
||||
variant = "default",
|
||||
...restProps
|
||||
} = $props();
|
||||
|
||||
setToggleGroupCtx({
|
||||
variant,
|
||||
size,
|
||||
spacing,
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Discriminated Unions + Destructing (required for bindable) do not
|
||||
get along, so we shut typescript up by casting `value` to `never`.
|
||||
-->
|
||||
<ToggleGroupPrimitive.Root
|
||||
bind:value={value}
|
||||
bind:ref
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
data-spacing={spacing}
|
||||
style={`--gap: ${spacing}`}
|
||||
class={cn(
|
||||
"group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
11
src/lib/components/ui/toggle/index.js
Normal file
11
src/lib/components/ui/toggle/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Root from "./toggle.svelte";
|
||||
export {
|
||||
toggleVariants,
|
||||
|
||||
} from "./toggle.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Toggle,
|
||||
};
|
||||
46
src/lib/components/ui/toggle/toggle.svelte
Normal file
46
src/lib/components/ui/toggle/toggle.svelte
Normal file
@ -0,0 +1,46 @@
|
||||
<script module>
|
||||
import { tv } from "tailwind-variants";
|
||||
|
||||
export const toggleVariants = tv({
|
||||
base: "hover:bg-muted hover:text-muted-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-xs",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 min-w-9 px-2",
|
||||
sm: "h-8 min-w-8 px-1.5",
|
||||
lg: "h-10 min-w-10 px-2.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { Toggle as TogglePrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
pressed = $bindable(false),
|
||||
class: className,
|
||||
size = "default",
|
||||
variant = "default",
|
||||
...restProps
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<TogglePrimitive.Root
|
||||
bind:ref
|
||||
bind:pressed
|
||||
data-slot="toggle"
|
||||
class={cn(toggleVariants({ variant, size }), className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@ -12,7 +12,6 @@
|
||||
return await getVisit(row.PVID);
|
||||
},
|
||||
});
|
||||
$inspect(masterDetail.selectedItem)
|
||||
</script>
|
||||
|
||||
<div class="flex w-full h-full overflow-hidden">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user