@mohasinac/appkit 4.64.1 → 4.65.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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.js +18 -1
- package/dist/_internal/server/features/search/index-build/sources.js +27 -12
- package/dist/_internal/server/features/seo/sitemap.js +78 -12
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +16 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/features/stores/tabs.d.ts +19 -15
- package/dist/_internal/server/features/stores/tabs.js +20 -21
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +9 -4
- package/dist/_internal/server/jobs/core/categoryTaxonomyBackfill.js +22 -0
- package/dist/_internal/server/jobs/core/countersReconcile.js +0 -1
- package/dist/_internal/server/jobs/handlers/onCatalogueSubmittedForApproval.js +1 -1
- package/dist/_internal/shared/features/site-settings/types.d.ts +10 -2
- package/dist/_internal/shared/features/sticker-trace/trace/segment.js +13 -2
- package/dist/_internal/shared/listing-types/feature-flags.d.ts +54 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +72 -0
- package/dist/client.d.ts +5 -2
- package/dist/client.js +8 -1
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +14 -0
- package/dist/features/account/api/[userId]/addresses/route.js +1 -5
- package/dist/features/addresses/repository/addresses.repository.js +1 -1
- package/dist/features/admin/api/bids/route.js +1 -5
- package/dist/features/admin/api/coupons/route.js +1 -5
- package/dist/features/admin/api/products/route.js +1 -5
- package/dist/features/admin/api/reviews/route.js +1 -5
- package/dist/features/admin/components/AdminCarouselEditorView.js +0 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -2
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +4 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +19 -3
- package/dist/features/admin/components/OwnerQuickActions.d.ts +7 -1
- package/dist/features/admin/components/OwnerQuickActions.js +1 -1
- package/dist/features/admin/schemas/admin-editor-forms.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +52 -0
- package/dist/features/admin/schemas/firestore.js +20 -0
- package/dist/features/auctions/api/route.js +1 -8
- package/dist/features/auctions/components/AuctionDetailPageView.js +1 -7
- package/dist/features/auth/hooks/useAuth.js +1 -1
- package/dist/features/before-after/api/route.js +1 -5
- package/dist/features/blog/api/route.js +1 -8
- package/dist/features/blog/components/BlogIndexPageView.d.ts +2 -3
- package/dist/features/blog/components/BlogIndexPageView.js +6 -9
- package/dist/features/cart/actions/cart-actions.js +1 -1
- package/dist/features/cart/api/route.js +1 -5
- package/dist/features/cart/utils/guest-cart.js +1 -5
- package/dist/features/catalogue/repository/catalogue.repository.js +1 -1
- package/dist/features/categories/api/route.js +5 -23
- package/dist/features/categories/components/BundlesListView.d.ts +2 -3
- package/dist/features/categories/components/BundlesListView.js +2 -5
- package/dist/features/categories/components/CategoriesIndexListing.js +47 -5
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +2 -3
- package/dist/features/categories/components/CategoriesIndexPageView.js +0 -32
- package/dist/features/categories/constants/category-tabs.d.ts +1 -1
- package/dist/features/categories/constants/category-tabs.js +7 -0
- package/dist/features/categories/hooks/useCategories.d.ts +6 -0
- package/dist/features/categories/hooks/useCategories.js +73 -2
- package/dist/features/categories/types/index.d.ts +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +2 -8
- package/dist/features/collections/api/route.js +1 -5
- package/dist/features/consultation/api/route.js +1 -5
- package/dist/features/contact/email.js +2 -5
- package/dist/features/corporate/api/route.js +1 -5
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +1 -7
- package/dist/features/events/components/AdminEventEditorView.js +1 -1
- package/dist/features/events/components/EventsListPageView.d.ts +2 -3
- package/dist/features/events/components/EventsListPageView.js +8 -11
- package/dist/features/faq/constants/help-guides.d.ts +8 -5
- package/dist/features/faq/constants/help-guides.js +9 -5
- package/dist/features/filters/AsyncFacetSection.js +2 -2
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +2 -2
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/filters/filterUtils.d.ts +0 -2
- package/dist/features/filters/filterUtils.js +0 -4
- package/dist/features/filters/index.d.ts +1 -1
- package/dist/features/filters/index.js +1 -1
- package/dist/features/forms/Checkbox.js +4 -3
- package/dist/features/forms/Input.js +5 -4
- package/dist/features/forms/Radio.js +6 -5
- package/dist/features/forms/Select.js +3 -2
- package/dist/features/forms/Slider.js +3 -2
- package/dist/features/forms/Textarea.js +3 -2
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/index.d.ts +1 -1
- package/dist/features/forms/index.js +1 -1
- package/dist/features/forms/utils.d.ts +0 -2
- package/dist/features/forms/utils.js +0 -4
- package/dist/features/grouped/schemas/grouped-listing-form.d.ts +6 -6
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- package/dist/features/history/utils/guest-history.js +1 -5
- package/dist/features/homepage/components/CustomCardsSection.js +8 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +6 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.d.ts +23 -1
- package/dist/features/layout/AppLayoutShell.js +13 -12
- package/dist/features/layout/BottomNavItem.d.ts +9 -1
- package/dist/features/layout/BottomNavItem.js +18 -4
- package/dist/features/layout/index.d.ts +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +1 -7
- package/dist/features/media/MediaLightbox.js +3 -5
- package/dist/features/orders/components/OrderCompletionPanel.d.ts +1 -1
- package/dist/features/orders/components/OrderCompletionPanel.js +1 -1
- package/dist/features/orders/components/RaiseDisputeForm.js +1 -1
- package/dist/features/orders/components/RefundRequestView.js +1 -1
- package/dist/features/parts/schemas/firestore.d.ts +274 -0
- package/dist/features/parts/schemas/firestore.js +19 -0
- package/dist/features/payments/repository/saved-payment-methods.repository.js +1 -1
- package/dist/features/pre-orders/api/admin/route.js +1 -5
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -7
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/CustomSectionTabContent.js +0 -5
- package/dist/features/products/components/GroupMemberPicker.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -7
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +1 -7
- package/dist/features/products/repository/products.repository.js +0 -1
- package/dist/features/promotions/api/route.js +1 -8
- package/dist/features/reviews/components/ReviewsIndexPageView.d.ts +2 -3
- package/dist/features/reviews/components/ReviewsIndexPageView.js +9 -12
- package/dist/features/seller/api/coupons/route.js +1 -5
- package/dist/features/seller/api/products/route.js +1 -5
- package/dist/features/seller/components/SellerProductShell.js +1 -2
- package/dist/features/seller/hooks/useSellerListingData.js +0 -30
- package/dist/features/stores/api/[storeSlug]/auctions/route.js +1 -10
- package/dist/features/stores/api/[storeSlug]/products/route.js +1 -10
- package/dist/features/stores/components/InteractiveStoreCard.js +9 -1
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoresIndexPageView.d.ts +2 -3
- package/dist/features/stores/components/StoresIndexPageView.js +6 -9
- package/dist/features/stores/schemas/firestore.d.ts +27 -0
- package/dist/features/stores/schemas/firestore.js +2 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +1 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/providers/db-firebase/admin-app-lite.js +14 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/react/hooks/useBrowsePageFlags.d.ts +26 -0
- package/dist/react/hooks/useBrowsePageFlags.js +18 -0
- package/dist/schemas/registry.d.ts +4 -4
- package/dist/security/csp.d.ts +32 -2
- package/dist/security/csp.js +32 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.js +1 -1
- package/dist/ui/components/Chart.js +0 -1
- package/dist/ui/rich-text/RichText.js +1 -3
- package/dist/ui/rich-text/RichTextRenderer.js +1 -3
- package/dist/utils/browser-storage.d.ts +14 -0
- package/dist/utils/browser-storage.js +18 -0
- package/dist/utils/classnames.d.ts +19 -0
- package/dist/utils/classnames.js +21 -0
- package/dist/utils/search-params.d.ts +44 -0
- package/dist/utils/search-params.js +46 -0
- package/dist/utils/sieve-builder.d.ts +13 -0
- package/dist/utils/sieve-builder.js +22 -0
- package/dist/utils/string.formatter.d.ts +12 -0
- package/dist/utils/string.formatter.js +17 -0
- package/package.json +2 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
type
|
|
2
|
+
import type { SearchParamsRecord } from "../../../utils/search-params";
|
|
3
3
|
export interface EventsListPageViewProps {
|
|
4
|
-
searchParams?:
|
|
4
|
+
searchParams?: SearchParamsRecord;
|
|
5
5
|
}
|
|
6
6
|
export declare function EventsListPageView({ searchParams }: EventsListPageViewProps): Promise<React.JSX.Element>;
|
|
7
|
-
export {};
|
|
@@ -9,12 +9,9 @@ import { sortBy } from "../../../constants/sort.js";
|
|
|
9
9
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
10
10
|
import { hidePublicTestData } from "../../../_internal/server/features/tester/visibility.js";
|
|
11
11
|
import { sortPinnedFirst } from "../utils/sort-pinned.js";
|
|
12
|
-
|
|
13
|
-
const v = params[key];
|
|
14
|
-
return Array.isArray(v) ? v[0] ?? "" : v ?? "";
|
|
15
|
-
}
|
|
12
|
+
import { firstParam } from "../../../utils/search-params.js";
|
|
16
13
|
function buildEventFilters(params) {
|
|
17
|
-
const statusRaw =
|
|
14
|
+
const statusRaw = firstParam(params, "status");
|
|
18
15
|
const parts = [];
|
|
19
16
|
if (statusRaw) {
|
|
20
17
|
const values = statusRaw.split("|").filter(Boolean);
|
|
@@ -27,7 +24,7 @@ function buildEventFilters(params) {
|
|
|
27
24
|
else {
|
|
28
25
|
parts.push(sieveFilter(EVENT_FIELDS.STATUS, SIEVE_OP.EQ, EVENT_FIELDS.STATUS_VALUES.ACTIVE));
|
|
29
26
|
}
|
|
30
|
-
const type =
|
|
27
|
+
const type = firstParam(params, "type");
|
|
31
28
|
if (type) {
|
|
32
29
|
const values = type.split("|").filter(Boolean);
|
|
33
30
|
if (values.length === 1)
|
|
@@ -35,8 +32,8 @@ function buildEventFilters(params) {
|
|
|
35
32
|
else if (values.length > 1)
|
|
36
33
|
parts.push(sieveMultiEq(EVENT_FIELDS.TYPE, values));
|
|
37
34
|
}
|
|
38
|
-
const dateFrom =
|
|
39
|
-
const dateTo =
|
|
35
|
+
const dateFrom = firstParam(params, "dateFrom");
|
|
36
|
+
const dateTo = firstParam(params, "dateTo");
|
|
40
37
|
if (dateFrom)
|
|
41
38
|
parts.push(sieveFilter(EVENT_FIELDS.STARTS_AT, SIEVE_OP.GTE, dateFrom));
|
|
42
39
|
if (dateTo)
|
|
@@ -44,9 +41,9 @@ function buildEventFilters(params) {
|
|
|
44
41
|
return sieveAnd(...parts);
|
|
45
42
|
}
|
|
46
43
|
export async function EventsListPageView({ searchParams = {} }) {
|
|
47
|
-
const sort =
|
|
48
|
-
const page = Number(
|
|
49
|
-
const pageSize = Number(
|
|
44
|
+
const sort = firstParam(searchParams, "sort") || sortBy(EVENT_FIELDS.STARTS_AT, "ASC");
|
|
45
|
+
const page = Number(firstParam(searchParams, "page")) || 1;
|
|
46
|
+
const pageSize = Number(firstParam(searchParams, "pageSize")) || 24;
|
|
50
47
|
const filters = buildEventFilters(searchParams);
|
|
51
48
|
const [result, facetCounts] = await Promise.all([
|
|
52
49
|
safeRead(() => eventRepository.list({ filters, sorts: sort, page, pageSize }), {
|
|
@@ -19,12 +19,15 @@ import type { FAQCategory } from "../types/index";
|
|
|
19
19
|
* they get the Help Centre index instead, via `HELP_INDEX_PATH`. Writing a
|
|
20
20
|
* guide to point them at later is a content decision, not a missing entry.
|
|
21
21
|
*
|
|
22
|
-
* 🛑 Paths, not `ROUTES`.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* 🛑 CORRECTED 2026-10-24 — this said *"Paths, not `ROUTES`. … `ROUTES` is
|
|
23
|
+
* appkit's own map — adding them there would make appkit declare pages it does
|
|
24
|
+
* not own."* The premise was already false when it was written: appkit's route
|
|
25
|
+
* map has declared `PUBLIC.HELP` and six `HELP_*` children for a long time
|
|
26
|
+
* (`route-map.ts:97-110`). So the file was hand-writing a literal to avoid
|
|
27
|
+
* adding something that was already there, and a renamed segment would have
|
|
28
|
+
* moved the real pages while leaving these guide links on the old path.
|
|
26
29
|
*/
|
|
27
|
-
export declare const HELP_INDEX_PATH:
|
|
30
|
+
export declare const HELP_INDEX_PATH: string;
|
|
28
31
|
export declare const FAQ_CATEGORY_HELP_GUIDE: Record<FAQCategory, {
|
|
29
32
|
path: string;
|
|
30
33
|
label: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
1
2
|
/**
|
|
2
3
|
* The Help Centre guide that answers a FAQ category at length.
|
|
3
4
|
*
|
|
@@ -18,12 +19,15 @@
|
|
|
18
19
|
* they get the Help Centre index instead, via `HELP_INDEX_PATH`. Writing a
|
|
19
20
|
* guide to point them at later is a content decision, not a missing entry.
|
|
20
21
|
*
|
|
21
|
-
* 🛑 Paths, not `ROUTES`.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* 🛑 CORRECTED 2026-10-24 — this said *"Paths, not `ROUTES`. … `ROUTES` is
|
|
23
|
+
* appkit's own map — adding them there would make appkit declare pages it does
|
|
24
|
+
* not own."* The premise was already false when it was written: appkit's route
|
|
25
|
+
* map has declared `PUBLIC.HELP` and six `HELP_*` children for a long time
|
|
26
|
+
* (`route-map.ts:97-110`). So the file was hand-writing a literal to avoid
|
|
27
|
+
* adding something that was already there, and a renamed segment would have
|
|
28
|
+
* moved the real pages while leaving these guide links on the old path.
|
|
25
29
|
*/
|
|
26
|
-
export const HELP_INDEX_PATH =
|
|
30
|
+
export const HELP_INDEX_PATH = String(ROUTES.PUBLIC.HELP);
|
|
27
31
|
/**
|
|
28
32
|
* The orders guide, named once because THREE categories share it.
|
|
29
33
|
*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Button, Checkbox, Div, Input, Row, Span, Stack, Text } from "../../ui/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
6
|
const CLS_BADGE_COUNT = "inline-flex items-center justify-center w-5 h-5 rounded-full bg-success-surface dark:bg-success-surface text-success dark:text-success ring-1 ring-success/20";
|
|
7
7
|
const CLS_CLEAR_ICON = "inline-flex items-center justify-center w-5 h-5 p-[var(--appkit-space-0)] text-[var(--appkit-color-text-muted)] hover:text-error dark:hover:text-error hover:bg-[var(--appkit-color-surface)] dark:hover:bg-slate-700 transition-colors rounded-full";
|
|
8
8
|
/**
|
|
@@ -60,7 +60,7 @@ export function AsyncFacetSection({ title, selected, onChange, loadOptions, sele
|
|
|
60
60
|
const selectedLabels = selected
|
|
61
61
|
.map((v) => labelMap.current.get(v) ?? v)
|
|
62
62
|
.slice(0, 3);
|
|
63
|
-
return (_jsxs(Div, { className:
|
|
63
|
+
return (_jsxs(Div, { className: joinClasses("p-[var(--appkit-space-4)] border-b border-[var(--appkit-color-border)] last:border-b-0", className), children: [_jsxs(Row, { gap: "xs", children: [_jsxs(Button, { type: "button", onClick: () => setCollapsed((c) => !c), variant: "ghost", size: "sm", className: "flex-1 justify-between text-[length:var(--appkit-text-sm)] font-semibold text-[var(--appkit-color-text)] py-[var(--appkit-space-1)] hover:opacity-80 transition-opacity", "aria-expanded": !collapsed, children: [_jsxs(Row, { as: Span, gap: "xs", children: [title, hasValue && (_jsx(Span, { size: "xs", className: CLS_BADGE_COUNT, children: selected.length }))] }), _jsx("svg", { className: joinClasses("w-4 h-4 shrink-0 text-[var(--appkit-color-text-muted)] transition-transform duration-200", collapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), onClear && hasValue && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: CLS_CLEAR_ICON, "aria-label": "Clear filter", children: _jsx("svg", { className: "w-3 h-3 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), collapsed && hasValue && (_jsxs(Row, { wrap: true, gap: "xs", className: "mt-2", children: [selectedLabels.map((label) => (_jsx(Span, { layout: "inline-flex", size: "xs", border: "default", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: label }, label))), selected.length > 3 && (_jsxs(Span, { layout: "inline-flex", size: "xs", rounded: "full", padding: "pill-xs", color: "faint", children: ["+", selected.length - 3, " more"] }))] })), _jsx(Div, { className: "mt-2", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: query, onChange: (e) => handleQueryChange(e.target.value), className: "w-full rounded-md border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text)] focus:outline-none focus:ring-2 focus:ring-primary-500/20" }) }), !collapsed && (_jsxs(Stack, { className: "mt-3", gap: "xs", children: [options.map((opt) => {
|
|
64
64
|
const isSelected = selected.includes(opt.value);
|
|
65
65
|
return (_jsxs(Row, { gap: "2.5", className: "cursor-pointer group", padding: "y-2xs", children: [_jsx(Checkbox, { bare: true, type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(opt.value), "aria-label": opt.label, className: "flex-shrink-0 w-4 h-4 rounded border-[var(--appkit-color-border)] cursor-pointer text-primary-600 dark:text-secondary-500" }), _jsx(Span, { size: "sm", className: "flex-1 group-hover:text-[var(--appkit-color-text)] transition-colors truncate", color: "muted", children: opt.label }), opt.count !== undefined && (_jsx(Span, { size: "xs", className: "tabular-nums flex-shrink-0", color: "faint", children: opt.count }))] }, opt.value));
|
|
66
66
|
}), options.length === 0 && !loading && (_jsx(Text, { paddingY: "2xs", color: "faint", size: "xs", children: "No results" })), loading && (_jsx(Text, { paddingY: "2xs", className: "animate-pulse", color: "faint", size: "xs", children: "Loading\u2026" })), hasMore && !loading && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", textSize: "xs", textColor: "faint", className: "w-full text-left hover:text-[var(--appkit-color-text)] py-[0.25rem]", onClick: () => void load(query, page + 1), children: "Load more\u2026" }))] }))] }));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Button, Checkbox, Div, Input, Row, Span, Stack, Text } from "../../ui/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
6
|
const __O = {
|
|
7
7
|
yAuto: "overflow-y-auto",
|
|
8
8
|
};
|
|
@@ -75,16 +75,16 @@ export function FilterFacetSection({ title, options, selected, onChange, selecti
|
|
|
75
75
|
.slice(0, 3);
|
|
76
76
|
// ── Dropdown variant ────────────────────────────────────────────────────
|
|
77
77
|
if (displayAs === "dropdown") {
|
|
78
|
-
return (_jsxs(Div, { ref: dropdownRef, className:
|
|
78
|
+
return (_jsxs(Div, { ref: dropdownRef, className: joinClasses("relative inline-block", className), children: [_jsxs(Button, { type: "button", onClick: () => setDropdownOpen((o) => !o), variant: "ghost", className: joinClasses("inline-flex items-center gap-[var(--appkit-space-1-5)] rounded-full border px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-sm)] font-medium transition-colors", hasValue
|
|
79
79
|
? CLS_OPTION_SELECTED
|
|
80
|
-
: "hover:border-zinc-400 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] hover:border-[var(--appkit-color-border)]"), children: [title, hasValue && (_jsx(Span, { weight: "bold", className: CLS_OPTION_DOT, children: selected.length })), _jsx("svg", { className:
|
|
80
|
+
: "hover:border-zinc-400 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] hover:border-[var(--appkit-color-border)]"), children: [title, hasValue && (_jsx(Span, { weight: "bold", className: CLS_OPTION_DOT, children: selected.length })), _jsx("svg", { className: joinClasses("h-3.5 w-3.5 text-zinc-400 transition-transform", dropdownOpen ? "rotate-180" : ""), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), dropdownOpen && (_jsxs(Div, { className: "absolute left-0 top-full z-[var(--appkit-z-dropdown,50)] mt-1 min-w-[180px] max-w-[260px]", surface: "default", rounded: "xl", shadow: "lg", border: "default", children: [searchable && (_jsx(Div, { border: "subtle", className: "border-b", padding: "xs", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border px-[var(--appkit-space-2)] py-[var(--appkit-space-1)] text-[length:var(--appkit-text-xs)] focus:outline-none border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] text-[var(--appkit-color-text)]" }) })), _jsxs(Div, { className: `max-h-56 ${__O.yAuto}`, padding: "y-2xs", children: [filtered.map((option) => {
|
|
81
81
|
const isSelected = selected.includes(option.value);
|
|
82
82
|
return (_jsxs(Row, { gap: "xs", className: "cursor-pointer py-[0.375rem] hover:bg-[var(--appkit-color-bg)] mx-1", padding: "x-sm", rounded: "lg", onClick: () => toggle(option.value), children: [_jsx(Checkbox, { bare: true, type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: "h-3.5 w-3.5 flex-shrink-0 cursor-pointer rounded text-primary-600 border-[var(--appkit-color-border)] dark:text-secondary-500" }), _jsx(Span, { size: "xs", className: "flex-1 truncate", color: "muted", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "flex-shrink-0 text-[10px] tabular-nums", color: "faint", children: option.count }))] }, option.value));
|
|
83
83
|
}), filtered.length === 0 && (_jsx(Text, { paddingY: "xs", paddingX: "x-sm", color: "faint", size: "xs", children: "No results" }))] }), hasValue && (_jsx(Div, { border: "subtle", className: "border-t p-[0.25rem]", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: CLS_CLEAR_BTN, onClick: () => { onChange([]); onClear?.(); setDropdownOpen(false); }, children: "Clear" }) }))] }))] }));
|
|
84
84
|
}
|
|
85
85
|
// ── Accordion variant (default) ─────────────────────────────────────────
|
|
86
|
-
return (_jsxs(Div, { className:
|
|
86
|
+
return (_jsxs(Div, { className: joinClasses("p-[var(--appkit-space-4)] border-b border-[var(--appkit-color-border)] last:border-b-0", className), children: [_jsxs(Row, { gap: "xs", children: [_jsxs(Button, { type: "button", onClick: handleToggle, variant: "ghost", size: "sm", justify: "between", className: "flex-1 text-[0.875rem] font-[600] text-[var(--appkit-color-text)] py-[0.25rem] hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsxs(Row, { as: Span, gap: "xs", children: [title, hasValue && (_jsx(Span, { size: "xs", className: CLS_BADGE_COUNT, children: selected.length }))] }), _jsx("svg", { className: joinClasses("w-4 h-4 shrink-0 text-[var(--appkit-color-text-muted)] transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), onClear && hasValue && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: CLS_CLEAR_ICON, "aria-label": "Clear filter", children: _jsx("svg", { className: "w-3 h-3 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), isCollapsed && hasValue && (_jsxs(Row, { wrap: true, gap: "xs", className: "mt-2", children: [selectedLabels.map((label) => (_jsx(Span, { layout: "inline-flex", size: "xs", className: "border border-[var(--appkit-color-border)]", rounded: "full", padding: "pill-xs", surface: "subtle", color: "muted", children: label }, label))), selected.length > 3 && (_jsxs(Span, { layout: "inline-flex", size: "xs", rounded: "full", padding: "pill-xs", color: "faint", children: ["+", selected.length - 3, " more"] }))] })), searchable && (_jsx(Div, { className: "mt-2", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text)] focus:outline-none focus:ring-2 focus:ring-primary-500/20 " }) })), !isCollapsed && (_jsxs(Stack, { className: "mt-3", gap: "xs", children: [filtered.map((option) => {
|
|
87
87
|
const isSelected = selected.includes(option.value);
|
|
88
|
-
return (_jsxs(Row, { gap: "2.5", className: "cursor-pointer group", padding: "y-2xs", children: [selectionMode === "single" ? (_jsx(Checkbox, { bare: true, type: "radio", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className:
|
|
88
|
+
return (_jsxs(Row, { gap: "2.5", className: "cursor-pointer group", padding: "y-2xs", children: [selectionMode === "single" ? (_jsx(Checkbox, { bare: true, type: "radio", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: joinClasses("flex-shrink-0 border-[var(--appkit-color-border)] cursor-pointer", "w-4 h-4 rounded-full text-primary-600 dark:text-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })) : (_jsx(Checkbox, { bare: true, type: "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: joinClasses("flex-shrink-0 border-[var(--appkit-color-border)] cursor-pointer", "w-4 h-4 rounded text-primary-600 dark:text-secondary-500 checked:bg-primary-600 dark:checked:bg-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })), _jsx(Span, { size: "sm", className: "flex-1 group-hover:text-[var(--appkit-color-text)] transition-colors truncate", color: "muted", children: option.label }), option.count !== undefined && (_jsx(Span, { size: "xs", className: "tabular-nums flex-shrink-0", color: "faint", children: option.count }))] }, option.value));
|
|
89
89
|
}), filtered.length === 0 && (_jsx(Text, { paddingY: "2xs", color: "faint", size: "xs", children: "No results" }))] }))] }));
|
|
90
90
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Div, Input, Row, Slider, Span, Stack, Text } from "../../ui/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
6
|
const CLS_BADGE_COUNT = "inline-flex items-center justify-center w-5 h-5 rounded-full bg-success-surface dark:bg-success-surface text-success dark:text-success ring-1 ring-success/20";
|
|
7
7
|
const CLS_CLEAR_BTN = "inline-flex items-center justify-center w-5 h-5 text-[var(--appkit-color-text-muted)] hover:text-error dark:hover:text-error rounded-full transition-colors";
|
|
8
8
|
/**
|
|
@@ -63,5 +63,5 @@ export function RangeFilter({ title, minLabel, maxLabel, minValue, maxValue, onM
|
|
|
63
63
|
onMaxChange("");
|
|
64
64
|
});
|
|
65
65
|
const inputClass = "w-full rounded-md border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] px-[var(--appkit-space-2-5)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text)] focus:outline-none focus:ring-2 focus:ring-primary-500/20 ";
|
|
66
|
-
return (_jsxs(Div, { role: "group", "aria-labelledby": `rf-${title}`, className:
|
|
66
|
+
return (_jsxs(Div, { role: "group", "aria-labelledby": `rf-${title}`, className: joinClasses("p-[var(--appkit-space-4)] border-b border-[var(--appkit-color-border)] last:border-b-0", className), children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsxs(Button, { type: "button", id: `rf-${title}`, onClick: handleToggle, variant: "ghost", size: "sm", justify: "between", className: "flex-1 text-[0.875rem] font-[600] text-[var(--appkit-color-text)] py-[0.25rem] hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsxs(Span, { layout: "flex", gap: "md", children: [title, hasValue && (_jsx(Span, { size: "xs", className: CLS_BADGE_COUNT, children: "1" }))] }), _jsx("svg", { className: joinClasses("w-4 h-4 shrink-0 text-[var(--appkit-color-text-muted)] transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), hasValue && (_jsx(Button, { type: "button", onClick: handleClear, variant: "ghost", size: "sm", className: CLS_CLEAR_BTN, "aria-label": "Clear", children: _jsx("svg", { className: "w-3 h-3 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), !isCollapsed && (_jsxs(Stack, { gap: "3", className: "mt-3", children: [canShowSlider && (_jsx(DualSlider, { minValue: minValue, maxValue: maxValue, onMinChange: onMinChange, onMaxChange: onMaxChange, minBound: minBound, maxBound: maxBound, step: step, prefix: prefix })), _jsxs(Row, { align: "end", gap: "sm", children: [_jsxs(Div, { className: "flex-1 min-w-0", children: [minLabel && (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: minLabel })), _jsxs(Div, { className: "relative", children: [prefix && (_jsx(Span, { size: "xs", className: "absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none", color: "faint", children: prefix })), _jsx(Input, { type: type, value: minValue, onChange: (e) => onMinChange(e.target.value), placeholder: minPlaceholder ?? (type === "date" ? "From" : "Min"), className: joinClasses(inputClass, prefix ? "pl-5" : "") })] })] }), _jsx(Span, { size: "xs", className: "flex-shrink-0 pb-[0.375rem]", color: "faint", children: "-" }), _jsxs(Div, { className: "flex-1 min-w-0", children: [maxLabel && (_jsx(Text, { className: "mb-1", color: "muted", size: "xs", children: maxLabel })), _jsxs(Div, { className: "relative", children: [prefix && (_jsx(Span, { size: "xs", className: "absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none", color: "faint", children: prefix })), _jsx(Input, { type: type, value: maxValue, onChange: (e) => onMaxChange(e.target.value), placeholder: maxPlaceholder ?? (type === "date" ? "To" : "Max"), className: joinClasses(inputClass, prefix ? "pl-5" : "") })] })] })] })] }))] }));
|
|
67
67
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Button, Div, Row, Span, Text, Toggle } from "../../ui/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
6
|
const CLS_CLEAR_BTN = "inline-flex items-center justify-center w-5 h-5 text-[var(--appkit-color-text-muted)] hover:text-error dark:hover:text-error rounded-full transition-colors";
|
|
7
7
|
export function SwitchFilter({ title, label, checked, onChange, defaultCollapsed = false, className = "", isOpen: controlledOpen, onToggle, onClear, }) {
|
|
8
8
|
const isControlled = controlledOpen !== undefined;
|
|
@@ -14,5 +14,5 @@ export function SwitchFilter({ title, label, checked, onChange, defaultCollapsed
|
|
|
14
14
|
else
|
|
15
15
|
setInternalCollapsed((c) => !c);
|
|
16
16
|
};
|
|
17
|
-
return (_jsxs(Div, { role: "group", "aria-labelledby": `sf-${title}`, className:
|
|
17
|
+
return (_jsxs(Div, { role: "group", "aria-labelledby": `sf-${title}`, className: joinClasses("p-[var(--appkit-space-4)] border-b border-[var(--appkit-color-border)] last:border-b-0", className), children: [_jsxs(Row, { gap: "sm", children: [_jsxs(Button, { type: "button", id: `sf-${title}`, onClick: handleToggle, variant: "ghost", size: "sm", justify: "between", className: "flex-1 text-[0.875rem] font-[600] text-[var(--appkit-color-text)] py-[0.25rem] hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: title }), _jsx("svg", { className: joinClasses("w-4 h-4 shrink-0 text-[var(--appkit-color-text-muted)] transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), checked && onClear && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: CLS_CLEAR_BTN, "aria-label": "Clear", children: _jsx("svg", { className: "w-3 h-3 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), !isCollapsed && (_jsxs(Row, { className: "mt-3", align: "center", justify: "between", gap: "3", children: [_jsx(Span, { size: "sm", color: "muted", children: label }), _jsx(Toggle, { checked: checked, onChange: onChange, size: "sm", "aria-label": label })] }))] }));
|
|
18
18
|
}
|
|
@@ -7,5 +7,3 @@ export type FilterOption = {
|
|
|
7
7
|
};
|
|
8
8
|
export declare function getFilterLabel(options: ReadonlyArray<FilterOption>, value: string): string | undefined;
|
|
9
9
|
export declare function getFilterValue(options: ReadonlyArray<FilterOption>, label: string): string | undefined;
|
|
10
|
-
/** Simple class-name concatenation. */
|
|
11
|
-
export declare function cn(...classes: (string | undefined | null | false)[]): string;
|
|
@@ -7,7 +7,3 @@ export function getFilterLabel(options, value) {
|
|
|
7
7
|
export function getFilterValue(options, label) {
|
|
8
8
|
return options.find((o) => o.label === label)?.value;
|
|
9
9
|
}
|
|
10
|
-
/** Simple class-name concatenation. */
|
|
11
|
-
export function cn(...classes) {
|
|
12
|
-
return classes.filter(Boolean).join(" ");
|
|
13
|
-
}
|
|
@@ -8,7 +8,7 @@ export { SwitchFilter } from "./SwitchFilter";
|
|
|
8
8
|
export type { SwitchFilterProps } from "./SwitchFilter";
|
|
9
9
|
export { FilterPanel } from "./FilterPanel";
|
|
10
10
|
export type { FilterPanelProps, FilterConfig, UrlTable, FacetSingleConfig, FacetMultiConfig, SwitchConfig, RangeNumberConfig, RangeDateConfig, } from "./FilterPanel";
|
|
11
|
-
export { getFilterLabel, getFilterValue
|
|
11
|
+
export { getFilterLabel, getFilterValue } from "./filterUtils";
|
|
12
12
|
export type { FilterOption } from "./filterUtils";
|
|
13
13
|
export { usePendingFilters } from "./usePendingFilters";
|
|
14
14
|
export type { UsePendingFiltersOptions, UsePendingFiltersResult, } from "./usePendingFilters";
|
|
@@ -4,6 +4,6 @@ export { AsyncFacetSection } from "./AsyncFacetSection.js";
|
|
|
4
4
|
export { RangeFilter } from "./RangeFilter.js";
|
|
5
5
|
export { SwitchFilter } from "./SwitchFilter.js";
|
|
6
6
|
export { FilterPanel } from "./FilterPanel.js";
|
|
7
|
-
export { getFilterLabel, getFilterValue
|
|
7
|
+
export { getFilterLabel, getFilterValue } from "./filterUtils.js";
|
|
8
8
|
export { usePendingFilters } from "./usePendingFilters.js";
|
|
9
9
|
export { parseRouteFilterSegments, serializeRouteFilterSegments, buildFilterUrl, extractFilterStateFromParams, mergeFilterUpdate, } from "./routeFilterState.js";
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Div, Row, Span, Text } from "../../ui/index.js";
|
|
4
4
|
import { Checkbox as UiCheckbox } from "../../ui/components/Checkbox.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ERROR_BASE } from "./utils.js";
|
|
6
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
7
|
export function Checkbox({ label, suffix, error, indeterminate, className = "", checked, ...props }) {
|
|
7
|
-
return (_jsxs(Div, { className: "w-full", children: [_jsxs("label", { className: "flex flex-row items-center gap-3 cursor-pointer group", children: [_jsxs(Row, { className: "relative flex-shrink-0", align: "center", justify: "center", children: [_jsx(UiCheckbox, { bare: true, type: "checkbox", checked: checked, indeterminate: indeterminate, className:
|
|
8
|
+
return (_jsxs(Div, { className: "w-full", children: [_jsxs("label", { className: "flex flex-row items-center gap-3 cursor-pointer group", children: [_jsxs(Row, { className: "relative flex-shrink-0", align: "center", justify: "center", children: [_jsx(UiCheckbox, { bare: true, type: "checkbox", checked: checked, indeterminate: indeterminate, className: joinClasses("peer w-5 h-5 rounded-md border-2 cursor-pointer", "transition-all duration-200 appearance-none", error
|
|
8
9
|
? "border-error dark:border-error"
|
|
9
|
-
: "border-[var(--appkit-color-border)]", "checked:bg-primary-600 checked:border-primary-600", "dark:checked:bg-secondary-500 dark:checked:border-secondary-500", "hover:border-primary-400 dark:hover:border-secondary-400", "focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-1", "dark:focus:ring-secondary-400/30 dark:focus:ring-offset-slate-900", "disabled:opacity-50 disabled:cursor-not-allowed", className), ...props }), _jsx("svg", { className: "absolute w-3 h-3 text-white pointer-events-none opacity-0 peer-checked:opacity-100 transition-opacity duration-150", viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: indeterminate ? (_jsx("line", { x1: "2", y1: "6", x2: "10", y2: "6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })) : (_jsx("path", { d: "M2 6L5 9L10 3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })) })] }), (label || suffix) && (_jsxs(Span, { layout: "flex", gap: "md", size: "sm", className: "group-hover:text-[var(--appkit-color-text)] transition-colors", color: "muted", children: [label, suffix] }))] }), error && (_jsx(Text, { className:
|
|
10
|
+
: "border-[var(--appkit-color-border)]", "checked:bg-primary-600 checked:border-primary-600", "dark:checked:bg-secondary-500 dark:checked:border-secondary-500", "hover:border-primary-400 dark:hover:border-secondary-400", "focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-1", "dark:focus:ring-secondary-400/30 dark:focus:ring-offset-slate-900", "disabled:opacity-50 disabled:cursor-not-allowed", className), ...props }), _jsx("svg", { className: "absolute w-3 h-3 text-white pointer-events-none opacity-0 peer-checked:opacity-100 transition-opacity duration-150", viewBox: "0 0 12 12", fill: "none", "aria-hidden": "true", children: indeterminate ? (_jsx("line", { x1: "2", y1: "6", x2: "10", y2: "6", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" })) : (_jsx("path", { d: "M2 6L5 9L10 3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })) })] }), (label || suffix) && (_jsxs(Span, { layout: "flex", gap: "md", size: "sm", className: "group-hover:text-[var(--appkit-color-text)] transition-colors", color: "muted", children: [label, suffix] }))] }), error && (_jsx(Text, { className: joinClasses(ERROR_BASE, "mt-1.5"), role: "alert", children: error }))] }));
|
|
10
11
|
}
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Div, Span, Text } from "../../ui/index.js";
|
|
4
4
|
import { Input as UiInput } from "../../ui/components/Input.js";
|
|
5
|
-
import {
|
|
5
|
+
import { INPUT_BASE, INPUT_ERROR, INPUT_SUCCESS, INPUT_DISABLED, INPUT_WITH_ICON, LABEL_BASE, HELPER_BASE, ERROR_BASE, } from "./utils.js";
|
|
6
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
7
|
export const Input = React.forwardRef(function Input({ label, error, helperText, icon, rightIcon, success, className = "", required, disabled, ...props }, ref) {
|
|
7
8
|
const stateClasses = error
|
|
8
9
|
? INPUT_ERROR
|
|
@@ -11,13 +12,13 @@ export const Input = React.forwardRef(function Input({ label, error, helperText,
|
|
|
11
12
|
: disabled
|
|
12
13
|
? INPUT_DISABLED
|
|
13
14
|
: "";
|
|
14
|
-
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsxs(Div, { className: "relative group", children: [icon && (_jsx(Div, { className:
|
|
15
|
+
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsxs(Div, { className: "relative group", children: [icon && (_jsx(Div, { className: joinClasses("absolute left-3 top-1/2 -translate-y-1/2 transition-colors duration-150", error
|
|
15
16
|
? "text-error"
|
|
16
|
-
: "text-[var(--appkit-color-text-faint)]", "group-focus-within:text-primary-500 dark:group-focus-within:text-secondary-400"), children: icon })), _jsx(UiInput, { bare: true, ref: ref, className:
|
|
17
|
+
: "text-[var(--appkit-color-text-faint)]", "group-focus-within:text-primary-500 dark:group-focus-within:text-secondary-400"), children: icon })), _jsx(UiInput, { bare: true, ref: ref, className: joinClasses("w-full", INPUT_BASE, icon ? INPUT_WITH_ICON : "", rightIcon ? "pr-10" : "", stateClasses, className), disabled: disabled, "aria-invalid": error ? "true" : undefined, "aria-describedby": error
|
|
17
18
|
? `${props.id}-error`
|
|
18
19
|
: helperText
|
|
19
20
|
? `${props.id}-helper`
|
|
20
|
-
: undefined, ...props }), rightIcon && (_jsx(Div, { className:
|
|
21
|
+
: undefined, ...props }), rightIcon && (_jsx(Div, { className: joinClasses("absolute right-3 top-1/2 -translate-y-1/2 transition-colors duration-150", error
|
|
21
22
|
? "text-error"
|
|
22
23
|
: "text-[var(--appkit-color-text-faint)]"), children: rightIcon }))] }), error && (_jsx(Text, { id: `${props.id}-error`, className: ERROR_BASE, role: "alert", children: error })), !error && helperText && (_jsx(Text, { id: `${props.id}-helper`, className: HELPER_BASE, children: helperText }))] }));
|
|
23
24
|
});
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Checkbox, Div, Span, Text } from "../../ui/index.js";
|
|
3
|
-
import {
|
|
3
|
+
import { LABEL_BASE, ERROR_BASE } from "./utils.js";
|
|
4
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
4
5
|
export function RadioGroup({ name, options, value, onChange, label, error, orientation = "vertical", variant = "toggle", }) {
|
|
5
6
|
if (variant === "classic") {
|
|
6
|
-
return (_jsxs(Div, { className: "w-full", children: [label && _jsx(Text, { className: LABEL_BASE, children: label }), _jsx(Div, { className:
|
|
7
|
+
return (_jsxs(Div, { className: "w-full", children: [label && _jsx(Text, { className: LABEL_BASE, children: label }), _jsx(Div, { className: joinClasses("flex gap-3", orientation === "vertical" ? "flex-col" : "flex-row flex-wrap"), children: options.map((option) => {
|
|
7
8
|
const isSelected = value === option.value;
|
|
8
|
-
return (_jsxs("label", { className:
|
|
9
|
+
return (_jsxs("label", { className: joinClasses("flex items-center gap-2.5 cursor-pointer", option.disabled && "opacity-50 cursor-not-allowed"), children: [_jsx(Checkbox, { bare: true, type: "radio", name: name, value: option.value, checked: isSelected, disabled: option.disabled, onChange: () => !option.disabled && onChange?.(option.value), className: "w-4 h-4 text-primary-600 dark:text-secondary-500 border-[var(--appkit-color-border)] focus:ring-primary-500/30 dark:focus:ring-secondary-400/30" }), _jsx(Span, { size: "sm", color: "muted", children: option.label })] }, option.value));
|
|
9
10
|
}) }), error && (_jsx(Text, { className: ERROR_BASE, role: "alert", children: error }))] }));
|
|
10
11
|
}
|
|
11
12
|
// Toggle variant — pill style
|
|
12
|
-
return (_jsxs(Div, { className: "w-full", children: [label && _jsx(Text, { className: LABEL_BASE, children: label }), _jsx(Div, { className:
|
|
13
|
+
return (_jsxs(Div, { className: "w-full", children: [label && _jsx(Text, { className: LABEL_BASE, children: label }), _jsx(Div, { className: joinClasses("flex gap-2", orientation === "vertical" ? "flex-col" : "flex-row flex-wrap"), role: "radiogroup", "aria-label": label, children: options.map((option) => {
|
|
13
14
|
const isSelected = value === option.value;
|
|
14
|
-
return (_jsxs("label", { className:
|
|
15
|
+
return (_jsxs("label", { className: joinClasses("flex items-center justify-center px-4 py-2.5 rounded-xl border text-sm font-medium cursor-pointer transition-all duration-200", isSelected
|
|
15
16
|
? "bg-primary-600 dark:bg-secondary-500 border-primary-600 dark:border-secondary-500 text-white shadow-sm"
|
|
16
17
|
: "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:border-primary-400 dark:hover:border-secondary-400 hover:bg-primary-50/50 dark:hover:bg-secondary-900/20", option.disabled && "opacity-50 cursor-not-allowed"), children: [_jsx(Checkbox, { bare: true, type: "radio", name: name, value: option.value, checked: isSelected, disabled: option.disabled, onChange: () => !option.disabled && onChange?.(option.value), className: "sr-only" }), option.label] }, option.value));
|
|
17
18
|
}) }), error && (_jsx(Text, { className: ERROR_BASE, role: "alert", children: error }))] }));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Div, Span, Text } from "../../ui/index.js";
|
|
3
3
|
import { Select as UiSelect } from "../../ui/components/Select.js";
|
|
4
|
-
import {
|
|
4
|
+
import { INPUT_BASE, INPUT_ERROR, LABEL_BASE, HELPER_BASE, ERROR_BASE, } from "./utils.js";
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
5
6
|
export function Select({ label, error, helperText, options, placeholder, className = "", required, value, ...props }) {
|
|
6
|
-
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsxs(Div, { className: "relative group", children: [_jsx(UiSelect, { bare: true, options: options, placeholder: placeholder, value: typeof value === "string" ? value : undefined, className:
|
|
7
|
+
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsxs(Div, { className: "relative group", children: [_jsx(UiSelect, { bare: true, options: options, placeholder: placeholder, value: typeof value === "string" ? value : undefined, className: joinClasses(INPUT_BASE, "pr-10 appearance-none cursor-pointer", error ? INPUT_ERROR : "", className), "aria-invalid": error ? "true" : undefined, ...props }), _jsx(Div, { className: "absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none transition-colors duration-150", children: _jsx("svg", { className: joinClasses("w-5 h-5 transition-transform duration-200", error
|
|
7
8
|
? "text-error"
|
|
8
9
|
: "text-[var(--appkit-color-text-faint)]"), viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", clipRule: "evenodd" }) }) })] }), error && (_jsx(Text, { className: ERROR_BASE, role: "alert", children: error })), !error && helperText && _jsx(Text, { className: HELPER_BASE, children: helperText })] }));
|
|
9
10
|
}
|
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Div, Row, Span, Text } from "../../ui/index.js";
|
|
5
5
|
import { Slider as UiSlider } from "../../ui/components/Slider.js";
|
|
6
|
-
import {
|
|
6
|
+
import { ERROR_BASE } from "./utils.js";
|
|
7
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
7
8
|
/**
|
|
8
9
|
* Slider — single-thumb range slider.
|
|
9
10
|
*/
|
|
@@ -20,5 +21,5 @@ export function Slider({ value: controlledValue, defaultValue = 0, min = 0, max
|
|
|
20
21
|
onChange?.(newVal);
|
|
21
22
|
};
|
|
22
23
|
const displayValue = formatValue ? formatValue(value) : String(value);
|
|
23
|
-
return (_jsxs(Div, { className:
|
|
24
|
+
return (_jsxs(Div, { className: joinClasses("w-full space-y-2", className), children: [(label || showValue) && (_jsxs(Row, { justify: "between", children: [label && (_jsx(Span, { size: "sm", weight: "medium", children: _jsx("label", { htmlFor: sliderId, children: label }) })), showValue && (_jsx(Span, { size: "sm", weight: "semibold", className: "tabular-nums", color: "primary", children: displayValue }))] })), _jsx(UiSlider, { id: sliderId, value: value, min: min, max: max, step: step, disabled: disabled, onChange: handleChange, showValue: false }), _jsxs(Row, { color: "muted", textSize: "xs", justify: "between", children: [_jsx(Span, { children: formatValue ? formatValue(min) : min }), _jsx(Span, { children: formatValue ? formatValue(max) : max })] }), error && (_jsx(Text, { className: ERROR_BASE, role: "alert", children: error }))] }));
|
|
24
25
|
}
|
|
@@ -2,14 +2,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { Div, Row, Span, Text } from "../../ui/index.js";
|
|
4
4
|
import { Textarea as UiTextarea } from "../../ui/components/Textarea.js";
|
|
5
|
-
import {
|
|
5
|
+
import { INPUT_BASE, INPUT_ERROR, LABEL_BASE, HELPER_BASE, ERROR_BASE, } from "./utils.js";
|
|
6
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
7
|
export const Textarea = React.forwardRef(function Textarea({ label, error, helperText, showCharCount, className = "", required, maxLength, value, defaultValue, ...props }, ref) {
|
|
7
8
|
const charCount = typeof value === "string"
|
|
8
9
|
? value.length
|
|
9
10
|
: typeof defaultValue === "string"
|
|
10
11
|
? defaultValue.length
|
|
11
12
|
: 0;
|
|
12
|
-
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsx(UiTextarea, { bare: true, ref: ref, className:
|
|
13
|
+
return (_jsxs(Div, { className: "w-full", children: [label && (_jsxs("label", { className: LABEL_BASE, children: [label, required && (_jsx(Span, { className: "text-error ml-1", "aria-hidden": "true", children: "*" }))] })), _jsx(UiTextarea, { bare: true, ref: ref, className: joinClasses(INPUT_BASE, "resize-y min-h-[80px]", error ? INPUT_ERROR : "", className), "aria-invalid": error ? "true" : undefined, maxLength: maxLength, value: value, defaultValue: defaultValue, ...props }), _jsxs(Row, { className: "mt-1.5", align: "center", justify: "between", children: [_jsxs(Div, { className: "flex-1", children: [error && (_jsx(Text, { className: ERROR_BASE, role: "alert", children: error })), !error && helperText && (_jsx(Text, { className: HELPER_BASE, children: helperText }))] }), showCharCount && maxLength && (_jsxs("p", { className: joinClasses("text-xs tabular-nums ml-2 flex-shrink-0", charCount >= maxLength
|
|
13
14
|
? "text-error"
|
|
14
15
|
: "text-[var(--appkit-color-text-faint)]"), children: [charCount, " / ", maxLength] }))] })] }));
|
|
15
16
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Button, Row, Span } from "../../ui/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { joinClasses } from "../../utils/classnames.js";
|
|
6
6
|
const SIZE_CONFIG = {
|
|
7
7
|
sm: {
|
|
8
8
|
track: "w-8 h-[18px]",
|
|
@@ -43,9 +43,9 @@ export function Toggle({ checked: controlledChecked, defaultChecked = false, onC
|
|
|
43
43
|
const generatedId = React.useId();
|
|
44
44
|
const toggleId = id ?? generatedId;
|
|
45
45
|
const cfg = SIZE_CONFIG[size];
|
|
46
|
-
return (_jsxs(Row, { className:
|
|
46
|
+
return (_jsxs(Row, { className: joinClasses("gap-3", className), children: [_jsx(Button, { type: "button", role: "switch", id: toggleId, "aria-checked": checked, disabled: disabled, onClick: handleChange, className: joinClasses("relative inline-flex items-center rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary-500/30 dark:focus:ring-secondary-400/30 focus:ring-offset-2 dark:focus:ring-offset-slate-900", cfg.track, checked
|
|
47
47
|
? "bg-primary-600 dark:bg-secondary-500"
|
|
48
|
-
: " bg-[var(--appkit-color-border)]", disabled && "opacity-50 cursor-not-allowed"), children: _jsx(Span, { className:
|
|
48
|
+
: " bg-[var(--appkit-color-border)]", disabled && "opacity-50 cursor-not-allowed"), children: _jsx(Span, { className: joinClasses("inline-block rounded-full bg-[var(--appkit-color-surface)] shadow-sm transition-transform duration-200", cfg.thumb, checked ? cfg.translateOn : cfg.translateOff) }) }), label && (_jsx("label", { htmlFor: toggleId, className: joinClasses("text-sm font-medium cursor-pointer select-none", checked
|
|
49
49
|
? "text-[var(--appkit-color-text)]"
|
|
50
50
|
: "text-[var(--appkit-color-text-muted)]", disabled && "opacity-50 cursor-not-allowed"), children: label }))] }));
|
|
51
51
|
}
|
|
@@ -12,4 +12,4 @@ export { Toggle } from "./Toggle";
|
|
|
12
12
|
export type { ToggleProps } from "./Toggle";
|
|
13
13
|
export { Slider } from "./Slider";
|
|
14
14
|
export type { SliderProps } from "./Slider";
|
|
15
|
-
export {
|
|
15
|
+
export { INPUT_BASE, INPUT_ERROR, INPUT_SUCCESS, INPUT_DISABLED, } from "./utils";
|
|
@@ -11,4 +11,4 @@ export { Slider } from "./Slider.js";
|
|
|
11
11
|
// the canonical primitives live in `ui/components/Form.tsx` and provide
|
|
12
12
|
// FormShellContext.Provider for FieldInput/FieldSelect/etc.
|
|
13
13
|
// Consumers should import from `@mohasinac/appkit/ui`.
|
|
14
|
-
export {
|
|
14
|
+
export { INPUT_BASE, INPUT_ERROR, INPUT_SUCCESS, INPUT_DISABLED, } from "./utils.js";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** Simple class-name concatenation utility. */
|
|
2
|
-
export declare function cn(...classes: (string | undefined | null | false)[]): string;
|
|
3
1
|
export declare const INPUT_BASE = "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] px-3.5 py-2.5 text-sm text-[var(--appkit-color-text)] focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 focus:outline-none transition-colors duration-150 placeholder:text-[var(--appkit-color-text-faint)]";
|
|
4
2
|
export declare const INPUT_ERROR = "border-error focus:ring-error/20 focus:border-error bg-error-surface/30";
|
|
5
3
|
export declare const INPUT_SUCCESS = "border-success focus:ring-success/20 focus:border-success";
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/** Simple class-name concatenation utility. */
|
|
2
|
-
export function cn(...classes) {
|
|
3
|
-
return classes.filter(Boolean).join(" ");
|
|
4
|
-
}
|
|
5
1
|
// --- Shared style constants ---------------------------------------------------
|
|
6
2
|
export const INPUT_BASE = "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-input)] px-3.5 py-2.5 text-sm text-[var(--appkit-color-text)] focus:ring-2 focus:ring-primary-500/20 focus:border-primary-500 focus:outline-none transition-colors duration-150 placeholder:text-[var(--appkit-color-text-faint)]";
|
|
7
3
|
export const INPUT_ERROR = "border-error focus:ring-error/20 focus:border-error bg-error-surface/30";
|
|
@@ -17,7 +17,7 @@ export declare const groupedListingFormSchema: z.ZodObject<{
|
|
|
17
17
|
isActive: boolean;
|
|
18
18
|
isFeatured: boolean;
|
|
19
19
|
productIds: string[];
|
|
20
|
-
groupTheme: "set" | "
|
|
20
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
21
21
|
minActiveMembers: number;
|
|
22
22
|
description?: string | undefined;
|
|
23
23
|
coverImage?: string | undefined;
|
|
@@ -26,7 +26,7 @@ export declare const groupedListingFormSchema: z.ZodObject<{
|
|
|
26
26
|
isActive: boolean;
|
|
27
27
|
isFeatured: boolean;
|
|
28
28
|
productIds: string[];
|
|
29
|
-
groupTheme: "set" | "
|
|
29
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
30
30
|
minActiveMembers: number;
|
|
31
31
|
description?: string | undefined;
|
|
32
32
|
coverImage?: string | undefined;
|
|
@@ -49,7 +49,7 @@ export declare const groupedListingCreateSchema: z.ZodObject<{
|
|
|
49
49
|
title: string;
|
|
50
50
|
isActive: boolean;
|
|
51
51
|
productIds: string[];
|
|
52
|
-
groupTheme: "set" | "
|
|
52
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
53
53
|
description?: string | undefined;
|
|
54
54
|
isFeatured?: boolean | undefined;
|
|
55
55
|
coverImage?: string | undefined;
|
|
@@ -58,7 +58,7 @@ export declare const groupedListingCreateSchema: z.ZodObject<{
|
|
|
58
58
|
title: string;
|
|
59
59
|
isActive: boolean;
|
|
60
60
|
productIds: string[];
|
|
61
|
-
groupTheme: "set" | "
|
|
61
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
62
62
|
description?: string | undefined;
|
|
63
63
|
isFeatured?: boolean | undefined;
|
|
64
64
|
coverImage?: string | undefined;
|
|
@@ -85,7 +85,7 @@ export declare const groupedListingUpdateSchema: z.ZodObject<{
|
|
|
85
85
|
isFeatured?: boolean | undefined;
|
|
86
86
|
coverImage?: string | undefined;
|
|
87
87
|
productIds?: string[] | undefined;
|
|
88
|
-
groupTheme?: "set" | "
|
|
88
|
+
groupTheme?: "set" | "character" | "related" | "lineage" | "generic" | undefined;
|
|
89
89
|
minActiveMembers?: number | undefined;
|
|
90
90
|
}, {
|
|
91
91
|
title?: string | undefined;
|
|
@@ -96,6 +96,6 @@ export declare const groupedListingUpdateSchema: z.ZodObject<{
|
|
|
96
96
|
isFeatured?: boolean | undefined;
|
|
97
97
|
coverImage?: string | undefined;
|
|
98
98
|
productIds?: string[] | undefined;
|
|
99
|
-
groupTheme?: "set" | "
|
|
99
|
+
groupTheme?: "set" | "character" | "related" | "lineage" | "generic" | undefined;
|
|
100
100
|
minActiveMembers?: number | undefined;
|
|
101
101
|
}>;
|
|
@@ -47,7 +47,7 @@ export declare const groupedListingFirestoreSchema: z.ZodObject<{
|
|
|
47
47
|
isActive: boolean;
|
|
48
48
|
isFeatured: boolean;
|
|
49
49
|
productIds: string[];
|
|
50
|
-
groupTheme: "set" | "
|
|
50
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
51
51
|
minActiveMembers: number;
|
|
52
52
|
visibilityStatus: "hidden" | "visible";
|
|
53
53
|
activeMemberCount: number;
|
|
@@ -70,7 +70,7 @@ export declare const groupedListingFirestoreSchema: z.ZodObject<{
|
|
|
70
70
|
isActive: boolean;
|
|
71
71
|
isFeatured: boolean;
|
|
72
72
|
productIds: string[];
|
|
73
|
-
groupTheme: "set" | "
|
|
73
|
+
groupTheme: "set" | "character" | "related" | "lineage" | "generic";
|
|
74
74
|
minActiveMembers: number;
|
|
75
75
|
visibilityStatus: "hidden" | "visible";
|
|
76
76
|
activeMemberCount: number;
|
|
@@ -7,14 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
9
9
|
import { HISTORY_MAX } from "../../../constants/limits.js";
|
|
10
|
+
import { getDefaultStorage } from "../../../utils/browser-storage.js";
|
|
10
11
|
const DEFAULT_GUEST_HISTORY_KEY = process.env.NEXT_PUBLIC_APP_ID
|
|
11
12
|
? `${process.env.NEXT_PUBLIC_APP_ID}_guest_history`
|
|
12
13
|
: "guest_history";
|
|
13
|
-
function getDefaultStorage() {
|
|
14
|
-
if (typeof window === "undefined")
|
|
15
|
-
return null;
|
|
16
|
-
return window.localStorage;
|
|
17
|
-
}
|
|
18
14
|
function read(storage, key) {
|
|
19
15
|
if (!storage)
|
|
20
16
|
return [];
|