@wix/himalaya-cli 0.1237.0 → 0.1238.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.
@@ -192,6 +192,14 @@ export declare function emptyState(props: {
192
192
  actionId: string;
193
193
  params?: Record<string, string>;
194
194
  };
195
+ /** Required with `action` (#3846) — the CTA label; there is no default. */
196
+ actionLabel?: string;
197
+ /** `typography` token names (#3846). Defaults: "bodyEmphasis" / "caption". */
198
+ titleStyle?: string;
199
+ subtitleStyle?: string;
200
+ /** The CTA Button's `style` / `size` (#3846). Defaults: "secondary" / "sm". */
201
+ actionStyle?: "primary" | "secondary" | "plain";
202
+ actionSize?: "sm" | "md";
195
203
  }): Component;
196
204
  export declare function appIntent(pkg: string, type: string, props: unknown, opts?: {
197
205
  id?: string;
@@ -549,13 +549,28 @@ export interface CollapsingHeaderProps {
549
549
  /** 0.0 = static, 1.0 = full parallax (default 0.5). */
550
550
  parallaxFactor?: number;
551
551
  }
552
+ /**
553
+ * The ONE standard empty state: a centred stack of icon tile, title, subtitle and
554
+ * button, in that order. Only `title` is required; an absent part draws nothing and keeps
555
+ * no gap. Padding and gaps come from the `spacing` tokens, text from `typography`.
556
+ */
552
557
  export interface EmptyStateProps {
553
558
  title: string;
554
559
  subtitle?: string;
555
560
  icon?: IconRef;
561
+ /** The CTA. Drawn as a real `Button` (see `actionStyle` / `actionSize`). */
556
562
  action?: Action;
557
- /** #1059 — CTA label; overridable for localized hosts. Default "Take action". */
563
+ /** #1059 — CTA label, also its accessibility label. REQUIRED when `action` is set:
564
+ * there is no default, `himi validate` reports an error, and no button is drawn. */
558
565
  actionLabel?: string;
566
+ /** `typography` token for the title. Default `"bodyEmphasis"`. */
567
+ titleStyle?: string;
568
+ /** `typography` token for the subtitle. Default `"caption"`. */
569
+ subtitleStyle?: string;
570
+ /** The CTA's Button `style`. Default `"secondary"`. */
571
+ actionStyle?: "primary" | "secondary" | "plain";
572
+ /** The CTA's Button `size`. Default `"sm"`. */
573
+ actionSize?: "sm" | "md";
559
574
  }
560
575
  export interface InterpolatedText {
561
576
  template: string;
package/dist/cli.mjs CHANGED
@@ -10413,6 +10413,41 @@ function mediaFieldFindings(descriptor) {
10413
10413
  visit(descriptor, "", true);
10414
10414
  return out2;
10415
10415
  }
10416
+ function emptyStateFindings(descriptor) {
10417
+ const out2 = [];
10418
+ const visit = (v2, path2, atRoot) => {
10419
+ if (Array.isArray(v2)) {
10420
+ v2.forEach((x2, i2) => visit(x2, `${path2}[${i2}]`, false));
10421
+ return;
10422
+ }
10423
+ if (!v2 || typeof v2 !== "object") return;
10424
+ const record2 = v2;
10425
+ const body = record2.body;
10426
+ if (body?.package === "himalaya" && body.type === "EmptyState") {
10427
+ const p2 = body.props ?? {};
10428
+ const base = path2 ? `${path2}.body.props` : "body.props";
10429
+ const label2 = p2.actionLabel;
10430
+ if (p2.action != null && (typeof label2 !== "string" || label2.trim() === "")) {
10431
+ out2.push({
10432
+ path: `${base}.actionLabel`,
10433
+ message: "EmptyState.action is set but EmptyState.actionLabel is missing. There is no default label: the renderers draw no button, so the action is unreachable. Set `actionLabel` to the button text (it is also the accessibility label), or remove `action`"
10434
+ });
10435
+ }
10436
+ if (p2.actionStyle !== void 0 && !(typeof p2.actionStyle === "string" && EMPTY_STATE_ACTION_STYLES.has(p2.actionStyle))) {
10437
+ out2.push({ path: `${base}.actionStyle`, message: `EmptyState.actionStyle must be "primary", "secondary" or "plain", got ${JSON.stringify(p2.actionStyle)}` });
10438
+ }
10439
+ if (p2.actionSize !== void 0 && !(typeof p2.actionSize === "string" && EMPTY_STATE_ACTION_SIZES.has(p2.actionSize))) {
10440
+ out2.push({ path: `${base}.actionSize`, message: `EmptyState.actionSize must be "sm" or "md", got ${JSON.stringify(p2.actionSize)}` });
10441
+ }
10442
+ }
10443
+ for (const [key2, child] of Object.entries(record2)) {
10444
+ if (isOpaqueAuthorJson(key2, child, atRoot)) continue;
10445
+ visit(child, path2 ? `${path2}.${key2}` : key2, false);
10446
+ }
10447
+ };
10448
+ visit(descriptor, "", true);
10449
+ return out2;
10450
+ }
10416
10451
  function richTextFieldFindings(descriptor) {
10417
10452
  const out2 = [];
10418
10453
  const visit = (v2, path2, atRoot) => {
@@ -10552,6 +10587,9 @@ function validateScreenDescriptor(screenId, descriptor, opts = {}) {
10552
10587
  for (const finding of richTextFieldFindings(descriptor)) {
10553
10588
  issues.push({ severity: "error", pass: "rich-text-field", path: finding.path, message: finding.message });
10554
10589
  }
10590
+ for (const finding of emptyStateFindings(descriptor)) {
10591
+ issues.push({ severity: "error", pass: "empty-state", path: finding.path, message: finding.message });
10592
+ }
10555
10593
  const bindings = collectServiceBindings(descriptor);
10556
10594
  const descriptorActions = collectDescriptorActions(descriptor);
10557
10595
  const workerActions = descriptorActions.filter(({ actionId }) => actionId.startsWith("tier5."));
@@ -11049,7 +11087,7 @@ function validateScreenDescriptor(screenId, descriptor, opts = {}) {
11049
11087
  services: Object.keys(services)
11050
11088
  };
11051
11089
  }
11052
- var CRAWL_BLOCKING_PASSES, BOOT_EVAL_TIMEOUT_MS, DRIFT_PROBES, DEVICE_PARITY_WITHHELD, defaultEvaluator, bootEvaluator, actionEvaluator, NATIVE_PROP_ENUMS, KNOWN_PROPS, IN_ARRAY_BINDING_PROPS, SERVICE_SIGIL, INTEGER_PROPS, NATIVE_REQUIRED_PROPS, bareNestedClass, NATIVE_NESTED_CLASSES, NATIVE_MODIFIERS_CLASS, NATIVE_DESCRIPTOR_CLASS, NATIVE_DESCRIPTOR_NESTED, NATIVE_NESTED_PROPS, MODIFIER_KEYS, INTEGER_MODIFIERS, MODIFIER_KINDS, SCHEMA_SCORED_NODES, CORE_PACKAGES, LEGACY_UNSCORED_KEYS, DYNAMIC_PLACEHOLDER, LITERAL_ONLY_PROPS, SERVICE_ONLY_BINDING_PROPS, WHOLE_VALUE_BINDING, EMBEDDED_BINDING, OPTIONS_BINDING_PROPS, EDITOR_SERVICE_NAME;
11090
+ var CRAWL_BLOCKING_PASSES, BOOT_EVAL_TIMEOUT_MS, DRIFT_PROBES, DEVICE_PARITY_WITHHELD, defaultEvaluator, bootEvaluator, actionEvaluator, NATIVE_PROP_ENUMS, KNOWN_PROPS, IN_ARRAY_BINDING_PROPS, SERVICE_SIGIL, INTEGER_PROPS, NATIVE_REQUIRED_PROPS, bareNestedClass, NATIVE_NESTED_CLASSES, NATIVE_MODIFIERS_CLASS, NATIVE_DESCRIPTOR_CLASS, NATIVE_DESCRIPTOR_NESTED, NATIVE_NESTED_PROPS, MODIFIER_KEYS, INTEGER_MODIFIERS, MODIFIER_KINDS, SCHEMA_SCORED_NODES, CORE_PACKAGES, LEGACY_UNSCORED_KEYS, DYNAMIC_PLACEHOLDER, LITERAL_ONLY_PROPS, SERVICE_ONLY_BINDING_PROPS, WHOLE_VALUE_BINDING, EMBEDDED_BINDING, OPTIONS_BINDING_PROPS, EDITOR_SERVICE_NAME, EMPTY_STATE_ACTION_STYLES, EMPTY_STATE_ACTION_SIZES;
11053
11091
  var init_validate_descriptor_core = __esm({
11054
11092
  "../../core/dev-server/src/validate-descriptor.core.ts"() {
11055
11093
  init_validator();
@@ -11275,7 +11313,17 @@ var init_validate_descriptor_core = __esm({
11275
11313
  slicesPath: "string",
11276
11314
  thickness: "number"
11277
11315
  },
11278
- EmptyState: { action: "any", actionLabel: "string", icon: "string", subtitle: "string", title: "string" },
11316
+ EmptyState: {
11317
+ action: "any",
11318
+ actionLabel: "string",
11319
+ actionSize: "string",
11320
+ actionStyle: "string",
11321
+ icon: "string",
11322
+ subtitle: "string",
11323
+ subtitleStyle: "string",
11324
+ title: "string",
11325
+ titleStyle: "string"
11326
+ },
11279
11327
  Fab: { action: "any", icon: "string", label: "string", size: "string", tint: "string" },
11280
11328
  FieldError: { severity: "string", text: "string", textPath: "string", visible: "boolean" },
11281
11329
  FilterChips: {
@@ -12006,6 +12054,8 @@ var init_validate_descriptor_core = __esm({
12006
12054
  EMBEDDED_BINDING = /\$service:[A-Za-z0-9_-]+\.[A-Za-z0-9_.[\]]+|\$flag:[A-Za-z0-9_.-]+|\$t:[A-Za-z0-9_.-]+|\$item(?:\.[A-Za-z0-9_.[\]]+)?/;
12007
12055
  OPTIONS_BINDING_PROPS = /* @__PURE__ */ new Set(["optionsPath"]);
12008
12056
  EDITOR_SERVICE_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
12057
+ EMPTY_STATE_ACTION_STYLES = /* @__PURE__ */ new Set(["primary", "secondary", "plain"]);
12058
+ EMPTY_STATE_ACTION_SIZES = /* @__PURE__ */ new Set(["sm", "md"]);
12009
12059
  }
12010
12060
  });
12011
12061
 
@@ -18048,6 +18098,73 @@ var init_icon2 = __esm({
18048
18098
  }
18049
18099
  });
18050
18100
 
18101
+ // ../native-config/watch-companion.ts
18102
+ function watchMode(mode) {
18103
+ if (mode === void 0 || mode === "standalone") return "standalone";
18104
+ if (mode === "companion") return "companion";
18105
+ throw new Error(`native-config: watch.mode must be "standalone" or "companion", got "${mode}"`);
18106
+ }
18107
+ function parseIosBundleId(configTs) {
18108
+ return /\bbundleId\s*:\s*["']([^"']+)["']/.exec(configTs)?.[1];
18109
+ }
18110
+ function companionWatchBundleId(iosBundleId) {
18111
+ return `${iosBundleId}.watchkitapp`;
18112
+ }
18113
+ function requireIosBundleId(iosBundleId) {
18114
+ if (!iosBundleId) {
18115
+ throw new Error(`native-config: watch.mode "companion" needs the app's iOS bundleId in config.ts \u2014 the watch app is paired to it`);
18116
+ }
18117
+ return iosBundleId;
18118
+ }
18119
+ function watchPairingInfo(mode, iosBundleId) {
18120
+ if (mode === "standalone") return { WKWatchOnly: true };
18121
+ return {
18122
+ WKCompanionAppBundleIdentifier: requireIosBundleId(iosBundleId),
18123
+ // The watch still launches and renders its last/baked content with the phone out of reach.
18124
+ WKRunsIndependentlyOfCompanionApp: true
18125
+ };
18126
+ }
18127
+ function watchSettingsPlan(mode, iosBundleId, standaloneBundleId) {
18128
+ if (mode === "standalone") {
18129
+ return standaloneBundleId ? { INFOPLIST_KEY_WKWatchOnly: "YES", PRODUCT_BUNDLE_IDENTIFIER: standaloneBundleId } : { INFOPLIST_KEY_WKWatchOnly: "YES" };
18130
+ }
18131
+ return { PRODUCT_BUNDLE_IDENTIFIER: companionWatchBundleId(requireIosBundleId(iosBundleId)) };
18132
+ }
18133
+ function watchTargetBundleId(watchProjectYml) {
18134
+ return /^\s{4}bundleId:\s*(\S+)\s*$/m.exec(watchProjectYml)?.[1];
18135
+ }
18136
+ function watchProjectName(watchProjectYml) {
18137
+ return /^name:\s*(\S+)\s*$/m.exec(watchProjectYml)?.[1];
18138
+ }
18139
+ function watchProjectReferenceLines(mode, watchName) {
18140
+ if (mode === "standalone") return [];
18141
+ return [
18142
+ "projectReferences:",
18143
+ ` ${WATCH_PROJECT_REF}:`,
18144
+ ` path: ../watchos/${watchName}.xcodeproj`
18145
+ ];
18146
+ }
18147
+ function watchEmbedLines(mode, watchName) {
18148
+ if (mode === "standalone") return [];
18149
+ return [
18150
+ `- target: ${WATCH_PROJECT_REF}/${watchName}`,
18151
+ " embed: true",
18152
+ " codeSign: false",
18153
+ " copy:",
18154
+ " destination: productsDirectory",
18155
+ " subpath: $(CONTENTS_FOLDER_PATH)/Watch"
18156
+ ];
18157
+ }
18158
+ var LABEL_WATCH_SETTINGS, LABEL_WATCH_PROJECT, LABEL_WATCH_EMBED, WATCH_PROJECT_REF;
18159
+ var init_watch_companion = __esm({
18160
+ "../native-config/watch-companion.ts"() {
18161
+ LABEL_WATCH_SETTINGS = "himalaya:watch:settings";
18162
+ LABEL_WATCH_PROJECT = "himalaya:watch:project";
18163
+ LABEL_WATCH_EMBED = "himalaya:watch:embed";
18164
+ WATCH_PROJECT_REF = "Watch";
18165
+ }
18166
+ });
18167
+
18051
18168
  // ../native-config/live-activity.ts
18052
18169
  function liveWidgetSource(deferredActions) {
18053
18170
  const supportedIds = deferredActions ? "[.refresh, .markRead]" : "[.refresh]";
@@ -18348,15 +18465,16 @@ function parseStringArray(configTs, field) {
18348
18465
  return [...body.matchAll(/["']([^"']+)["']/g)].map((x2) => x2[1]);
18349
18466
  }
18350
18467
  function parseWatchConfig(configTs) {
18351
- const block = /watch\s*[:=]\s*\{([^}]*)\}/.exec(configTs);
18468
+ const block = /export\s+const\s+watch\s*=\s*\{([^}]*)\}/.exec(configTs) ?? [...configTs.matchAll(/\bwatch\s*[:=]\s*\{([^}]*)\}/g)].find((m2) => /\bmode\s*:/.test(m2[1]));
18352
18469
  if (!block) return {};
18353
18470
  const body = block[1];
18354
18471
  const mode = /mode\s*:\s*["']([^"']+)["']/.exec(body)?.[1];
18355
18472
  const serve = /serve\s*:\s*["']([^"']+)["']/.exec(body)?.[1];
18356
18473
  return { mode, serve };
18357
18474
  }
18358
- function watchInfoPlan(watch2) {
18359
- const plan = { HimalayaWatchMode: watch2.mode ?? "standalone" };
18475
+ function watchInfoPlan(watch2, iosBundleId) {
18476
+ const mode = watchMode(watch2.mode);
18477
+ const plan = { HimalayaWatchMode: mode, ...watchPairingInfo(mode, iosBundleId) };
18360
18478
  if (watch2.serve) plan.HimalayaWatchServe = watch2.serve;
18361
18479
  return plan;
18362
18480
  }
@@ -18599,7 +18717,19 @@ function genNativeConfig(app, opts = {}) {
18599
18717
  }
18600
18718
  const macos = macosPlanFor(ids, withCopy(ov.macos));
18601
18719
  const android = androidPlanFor(ids, ov.android);
18602
- const watch2 = watchInfoPlan(parseWatchConfig(configTs));
18720
+ const watchConfig = parseWatchConfig(configTs);
18721
+ const iosBundleId = parseIosBundleId(configTs);
18722
+ const watch2 = watchInfoPlan(watchConfig, iosBundleId);
18723
+ const watchYml = watchosProjectYml(app);
18724
+ const watchYmlText = existsSync26(watchYml) ? readFileSync26(watchYml, "utf8") : "";
18725
+ const watchSettings = watchSettingsPlan(watchMode(watchConfig.mode), iosBundleId, watchTargetBundleId(watchYmlText));
18726
+ const watchName = existsSync26(watchYml) ? watchProjectName(readFileSync26(watchYml, "utf8")) : void 0;
18727
+ const watchEmbedFor = (lines) => (indent) => {
18728
+ const mode = watchMode(watchConfig.mode);
18729
+ if (mode === "standalone") return [];
18730
+ if (!watchName) throw new Error(`${app}: watch.mode "companion" but ${watchYml} has no \`name:\` to embed`);
18731
+ return lines(mode, watchName).map((line) => indent + line);
18732
+ };
18603
18733
  const tv = tvInfoPlan(parseTvConfig(configTs));
18604
18734
  const orientation = parseOrientationEnvelope(configTs);
18605
18735
  const carCategory = resolveCarCategory(parseCarConfig(configTs), app);
@@ -18641,6 +18771,10 @@ function genNativeConfig(app, opts = {}) {
18641
18771
  // watchOS (RFC 0047): the watch runtime mode/serve, declared once in config.ts. A watch
18642
18772
  // target opts in by adding the LABEL_WATCH markers; a file without them is skipped.
18643
18773
  applyToFile(watchosProjectYml(app), "yaml", LABEL_WATCH, (i2) => renderPlistYaml(watch2, i2), check),
18774
+ applyToFile(watchosProjectYml(app), "yaml", LABEL_WATCH_SETTINGS, (i2) => renderPlistYaml(watchSettings, i2), check),
18775
+ // #3704 — the iOS half of a companion pair (project reference + embed into <App>.app/Watch).
18776
+ applyToFile(iosProjectYml(app), "yaml", LABEL_WATCH_PROJECT, watchEmbedFor(watchProjectReferenceLines), check),
18777
+ applyToFile(iosProjectYml(app), "yaml", LABEL_WATCH_EMBED, watchEmbedFor(watchEmbedLines), check),
18644
18778
  // tvOS (RFC 0051): HimalayaTvMode managed block; skipped when markers absent.
18645
18779
  applyToFile(tvosProjectYml(app), "yaml", LABEL_TV, (i2) => renderPlistYaml(tv, i2), check),
18646
18780
  // Android TV (RFC 0051 WS7): Leanback features on the `*-tv` module; skipped when markers absent.
@@ -18705,6 +18839,7 @@ var init_gen_native_config = __esm({
18705
18839
  init_paths2();
18706
18840
  init_splash();
18707
18841
  init_icon2();
18842
+ init_watch_companion();
18708
18843
  init_live_activity();
18709
18844
  init_car();
18710
18845
  init_localweb();
@@ -116514,6 +116649,10 @@ function withExtras(name, render) {
116514
116649
  Wrapped.displayName = name;
116515
116650
  return Wrapped;
116516
116651
  }
116652
+ function spacingToken(ctx, [name, fallback]) {
116653
+ const v2 = ctx.tokens?.spacing?.[name];
116654
+ return typeof v2 === "number" && Number.isFinite(v2) ? Math.max(0, v2) : fallback;
116655
+ }
116517
116656
  function sheetTabStops(root) {
116518
116657
  const all = Array.from(root.querySelectorAll(NATIVELY_TABBABLE)).filter((el2) => el2.tabIndex >= 0 && !el2.disabled && !el2.closest('[inert], [hidden], [aria-hidden="true"]'));
116519
116658
  const ordered = all.filter((el2) => el2.tabIndex > 0).sort((a2, b2) => a2.tabIndex - b2.tabIndex);
@@ -117702,12 +117841,7 @@ function renderInner(node, ctx) {
117702
117841
  return /* @__PURE__ */ jsx9("div", { style: { flex: 1 } });
117703
117842
  }
117704
117843
  case "EmptyState":
117705
- return /* @__PURE__ */ jsxs6("div", { style: { textAlign: "center", width: "100%", padding: 12, boxSizing: "border-box", ...wrap }, children: [
117706
- /* @__PURE__ */ jsx9("div", { style: { width: 46, height: 46, borderRadius: 13, background: C2("surfaceTertiary", "#eee"), display: "grid", placeItems: "center", margin: "0 auto 12px", color: C2("textSecondary", "#9aa2ad") }, children: iconSvg(props.icon, { size: 24 }) }),
117707
- /* @__PURE__ */ jsx9("div", { style: { fontFamily: bodyFont(ctx), fontWeight: 600, fontSize: pxToRem(16), color: C2("textPrimary", "#111") }, children: asBoundText(props.title, ctx) || "Empty" }),
117708
- props.subtitle && /* @__PURE__ */ jsx9("div", { style: { fontFamily: bodyFont(ctx), fontSize: pxToRem(13), color: C2("textSecondary", "#888"), marginTop: 3 }, children: txt(props.subtitle, ctx) }),
117709
- actionTextButton(ctx, props.action, asBoundText(props.actionLabel, ctx) || "Take action", accentTextColor(ctx), { margin: "12px auto 0", textAlign: "center" })
117710
- ] });
117844
+ return /* @__PURE__ */ jsx9(EmptyStateNode, { props, ctx, wrap, nodeId: node.id });
117711
117845
  // ───────────── atoms ─────────────
117712
117846
  case "Text":
117713
117847
  return /* @__PURE__ */ jsx9(TextNode, { props, ctx, wrap });
@@ -118130,7 +118264,7 @@ function renderInner(node, ctx) {
118130
118264
  }
118131
118265
  }
118132
118266
  }
118133
- var useIsomorphicLayoutEffect, ResponsiveGrid, SPLIT_EXPANDED_MIN_WIDTH, SPLIT_COLUMN_GAP, ResponsiveSplitView, ABSOLUTE_URL_RE, appearKeyframesInjected, pressStylesInjected, REVEAL_ON_FOCUS_ATTR, FOCUS_RING_CSS, focusStylesInjected, HLIST_CHILD_CSS, hListStylesInjected, listAnimateRuntimeInstalled, transitionKeyframesInjected, countUpRuntimeInjected, MEASURE_PREFIX, measureRuntimeInjected, revealRuntimeInjected, vectorAnimStylesInjected, scrollProgressRuntimeInjected, confettiRuntimeInjected, lottieRuntimeInjected, riveRuntimeInjected, gradientKeyframesInjected, parallaxRuntimeInjected, spinKeyframesInjected, marqueeKeyframesInjected, alignItems, zJustifyItems, hAlignItems, bodyFont, kids2, rowKidCtx, rowKids, staggeredKids, asTextL10n, asFinalTextL10n, asBoundText, useBoundString, InterpolatedTextView, txt, selectFragmentCase, fragmentValueHash, fragmentSlotContent, FragmentSlotNode, RichContentEditorNode, MediaFieldNode, WorkerSubtreeNode, RichContentNode, resolveItemDeep2, resolveSwipeActions, resolveModBindings, resolvedComponentId, directServiceBindings, withRootStyle, ReactiveDirectScalars, HiddenGate, KEYBOARD_INPUT_MODES, AUTOCAPITALIZE_VALUES, ENTER_KEY_HINTS, AUTOGROW_CEILING, EditableField, MenuNode, LoadMoreSentinel, FieldErrorNode, FormView, MediaSlotProvider, MEDIA_ITEM_BINDABLE, MediaPlayerView, VideoNode, PdfDocumentCanvas, PdfViewNode, WebViewNode, InteractivePicker, InteractiveDatePicker, InteractiveSearchField, InteractiveStepper, SYNTHETIC_TAP_ROLE, SYNTHETIC_TABINDEX, INFERRED_DECORATIVE, SELF_RENDERED_LEAF, ROLE_WHEN_NAMED, AUTHORED_ROLE, PENDING_RANGE, RANGE_ROLES, NODE_A11Y_ATTRS, INTERACTIVE_ROLES, ALWAYS_INTERACTIVE_COMPONENT_TYPES, TAP_ACTIVATOR_ATTR, TAP_GROUP_ATTR, VISUALLY_HIDDEN, NAME_SKIP_SELECTOR, activatorNameObservers, ACTIVATOR_STATE_ATTRS, controlNode, ToggleNode, SliderNode, SelectionControlNode, TabsNode, SegmentedControlNode, DisclosureGroupNode, GridNode, SplitViewNode, TextNode, ImageNode, ANIMATION_LEAF, LottieNode, RiveNode, AvatarNode, IconNode, ButtonNode, OtpInputNode, ProgressBarNode, ACTIVITY_DEFAULT_NAME, ActivityIndicatorNode, NATIVELY_TABBABLE, BottomSheetNode, PRESENTED_OVERLAY_ATTR, PopoverNode, ChipsInputNode, ChipNode, TableNode, GaugeRing, GaugeNode, RATING_METRICS, ratingMetrics, RatingNode, AvatarStripNode, CHART_STROKE_CYCLE, chartStrokeAt, chartBarCenter, ChartNode, donutSliceOpacity, DonutChartNode, LegendMarker, LegendNode, cellAlign, DataTableNode, voiceNoteState, voiceNoteDelivery, VOICE_NOTE_DEFAULT_BARS, clampBarCount, VoiceNoteNode, TimelineNode, WheelPickerNode, ColorPickerNode, FilterChipsNode, TILE_PX, MAX_MERCATOR_LAT, DEFAULT_TILE_MAX_ZOOM, CIRCLE_SEGMENTS, METRES_PER_DEG_LAT, MARKER_HIT_RADIUS, SHAPE_HIT_RADIUS, MapNode, CollapsingHeaderNode, UnsupportedOnSurface, CALENDAR_DATE_ONLY_RE, CAL_DEFAULT_MAX_DOTS, CAL_AGENDA_INITIAL_DAYS, CAL_AGENDA_PAGE_DAYS, CAL_DEFAULT_SNAP_MINUTES, CAL_UNASSIGNED_LANE_ID, MonthCalendarNode, TimeGridNode, AgendaNode, ARIA_ROLE, initialFocusScheduled, PRESENTED_SCOPE, focusRoots, A11yHintDescription, WebRenderer;
118267
+ var useIsomorphicLayoutEffect, ResponsiveGrid, SPLIT_EXPANDED_MIN_WIDTH, SPLIT_COLUMN_GAP, ResponsiveSplitView, ABSOLUTE_URL_RE, appearKeyframesInjected, pressStylesInjected, REVEAL_ON_FOCUS_ATTR, FOCUS_RING_CSS, focusStylesInjected, HLIST_CHILD_CSS, hListStylesInjected, listAnimateRuntimeInstalled, transitionKeyframesInjected, countUpRuntimeInjected, MEASURE_PREFIX, measureRuntimeInjected, revealRuntimeInjected, vectorAnimStylesInjected, scrollProgressRuntimeInjected, confettiRuntimeInjected, lottieRuntimeInjected, riveRuntimeInjected, gradientKeyframesInjected, parallaxRuntimeInjected, spinKeyframesInjected, marqueeKeyframesInjected, alignItems, zJustifyItems, hAlignItems, bodyFont, kids2, rowKidCtx, rowKids, staggeredKids, asTextL10n, asFinalTextL10n, asBoundText, useBoundString, InterpolatedTextView, txt, selectFragmentCase, fragmentValueHash, fragmentSlotContent, FragmentSlotNode, RichContentEditorNode, MediaFieldNode, WorkerSubtreeNode, RichContentNode, resolveItemDeep2, resolveSwipeActions, resolveModBindings, resolvedComponentId, directServiceBindings, withRootStyle, ReactiveDirectScalars, HiddenGate, KEYBOARD_INPUT_MODES, AUTOCAPITALIZE_VALUES, ENTER_KEY_HINTS, AUTOGROW_CEILING, EditableField, MenuNode, LoadMoreSentinel, FieldErrorNode, FormView, MediaSlotProvider, MEDIA_ITEM_BINDABLE, MediaPlayerView, VideoNode, PdfDocumentCanvas, PdfViewNode, WebViewNode, InteractivePicker, InteractiveDatePicker, InteractiveSearchField, InteractiveStepper, SYNTHETIC_TAP_ROLE, SYNTHETIC_TABINDEX, INFERRED_DECORATIVE, SELF_RENDERED_LEAF, ROLE_WHEN_NAMED, AUTHORED_ROLE, PENDING_RANGE, RANGE_ROLES, NODE_A11Y_ATTRS, INTERACTIVE_ROLES, ALWAYS_INTERACTIVE_COMPONENT_TYPES, TAP_ACTIVATOR_ATTR, TAP_GROUP_ATTR, VISUALLY_HIDDEN, NAME_SKIP_SELECTOR, activatorNameObservers, ACTIVATOR_STATE_ATTRS, controlNode, ToggleNode, SliderNode, SelectionControlNode, TabsNode, SegmentedControlNode, DisclosureGroupNode, GridNode, SplitViewNode, TextNode, ImageNode, ANIMATION_LEAF, LottieNode, RiveNode, AvatarNode, IconNode, EMPTY_STATE_SPACING, EmptyStateNode, ButtonNode, OtpInputNode, ProgressBarNode, ACTIVITY_DEFAULT_NAME, ActivityIndicatorNode, NATIVELY_TABBABLE, BottomSheetNode, PRESENTED_OVERLAY_ATTR, PopoverNode, ChipsInputNode, ChipNode, TableNode, GaugeRing, GaugeNode, RATING_METRICS, ratingMetrics, RatingNode, AvatarStripNode, CHART_STROKE_CYCLE, chartStrokeAt, chartBarCenter, ChartNode, donutSliceOpacity, DonutChartNode, LegendMarker, LegendNode, cellAlign, DataTableNode, voiceNoteState, voiceNoteDelivery, VOICE_NOTE_DEFAULT_BARS, clampBarCount, VoiceNoteNode, TimelineNode, WheelPickerNode, ColorPickerNode, FilterChipsNode, TILE_PX, MAX_MERCATOR_LAT, DEFAULT_TILE_MAX_ZOOM, CIRCLE_SEGMENTS, METRES_PER_DEG_LAT, MARKER_HIT_RADIUS, SHAPE_HIT_RADIUS, MapNode, CollapsingHeaderNode, UnsupportedOnSurface, CALENDAR_DATE_ONLY_RE, CAL_DEFAULT_MAX_DOTS, CAL_AGENDA_INITIAL_DAYS, CAL_AGENDA_PAGE_DAYS, CAL_DEFAULT_SNAP_MINUTES, CAL_UNASSIGNED_LANE_ID, MonthCalendarNode, TimeGridNode, AgendaNode, ARIA_ROLE, initialFocusScheduled, PRESENTED_SCOPE, focusRoots, A11yHintDescription, WebRenderer;
118134
118268
  var init_renderNode = __esm({
118135
118269
  "../../core/web-renderer/src/renderNode.tsx"() {
118136
118270
  "use strict";
@@ -120212,6 +120346,49 @@ var init_renderNode = __esm({
120212
120346
  const a11y = described ? { role: "img", "aria-label": String(contentDescription), [SELF_RENDERED_LEAF]: true } : { "aria-hidden": "true", [INFERRED_DECORATIVE]: true, [SELF_RENDERED_LEAF]: true, [ROLE_WHEN_NAMED]: "img" };
120213
120347
  return /* @__PURE__ */ jsx9("span", { ...a11y, style: { display: "inline-grid", placeItems: "center", width: size + 6, height: size + 6, lineHeight: 1, flex: "none", ...wrap }, children: iconSvg(iconName, { size, color: C2(resolveScalarStatic(props.tint, ctx) ?? "textPrimary", "#636b78") }) });
120214
120348
  });
120349
+ EMPTY_STATE_SPACING = {
120350
+ padding: ["lg", 16],
120351
+ iconGap: ["md", 12],
120352
+ subtitleGap: ["xs", 4],
120353
+ actionGap: ["lg", 16]
120354
+ };
120355
+ EmptyStateNode = withExtras("EmptyStateNode", ({ props, ctx, wrap, nodeId }) => {
120356
+ const C2 = ctx.color;
120357
+ const subtitleText = useBoundString(props.subtitle, ctx);
120358
+ const titleFont = ctx.font(typeof props.titleStyle === "string" && props.titleStyle ? props.titleStyle : "bodyEmphasis");
120359
+ const subtitleFont = ctx.font(typeof props.subtitleStyle === "string" && props.subtitleStyle ? props.subtitleStyle : "caption");
120360
+ const fontCss = (f2) => ({
120361
+ fontFamily: fontStack(f2.family, ctx.idiom),
120362
+ fontSize: f2.remSize,
120363
+ fontWeight: f2.weight,
120364
+ lineHeight: f2.line,
120365
+ letterSpacing: f2.letterSpacing
120366
+ });
120367
+ const hasIcon2 = typeof props.icon === "string" ? props.icon.length > 0 : props.icon != null;
120368
+ const hasSubtitle = subtitleText.trim().length > 0;
120369
+ const label2 = props.actionLabel;
120370
+ const hasAction = props.action != null && typeof label2 === "string" && label2.trim().length > 0;
120371
+ const padding = spacingToken(ctx, EMPTY_STATE_SPACING.padding);
120372
+ return /* @__PURE__ */ jsxs6("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", width: "100%", padding, boxSizing: "border-box", ...wrap }, children: [
120373
+ hasIcon2 && /* @__PURE__ */ jsx9("div", { "data-empty-state-part": "icon", "aria-hidden": "true", style: { width: 46, height: 46, borderRadius: 13, background: C2("surfaceTertiary", "#eee"), display: "grid", placeItems: "center", color: C2("textSecondary", "#9aa2ad"), flexShrink: 0 }, children: iconSvg(props.icon, { size: 24 }) }),
120374
+ /* @__PURE__ */ jsx9("div", { "data-empty-state-part": "title", style: { ...fontCss(titleFont), color: C2("textPrimary", "#111"), marginTop: hasIcon2 ? spacingToken(ctx, EMPTY_STATE_SPACING.iconGap) : 0, overflowWrap: "anywhere" }, children: txt(props.title, ctx) }),
120375
+ hasSubtitle && /* @__PURE__ */ jsx9("div", { "data-empty-state-part": "subtitle", style: { ...fontCss(subtitleFont), color: C2("textSecondary", "#888"), marginTop: spacingToken(ctx, EMPTY_STATE_SPACING.subtitleGap), overflowWrap: "anywhere" }, children: isBinding(props.subtitle) ? subtitleText : txt(props.subtitle, ctx) }),
120376
+ hasAction && /* @__PURE__ */ jsx9("div", { "data-empty-state-part": "action", style: { marginTop: spacingToken(ctx, EMPTY_STATE_SPACING.actionGap), display: "flex", justifyContent: "center" }, children: renderNode({
120377
+ id: `${nodeId ?? "empty_state"}.action`,
120378
+ body: {
120379
+ package: "himalaya",
120380
+ type: "Button",
120381
+ props: {
120382
+ title: label2,
120383
+ action: props.action,
120384
+ style: props.actionStyle ?? "secondary",
120385
+ size: props.actionSize ?? "sm",
120386
+ fullWidth: false
120387
+ }
120388
+ }
120389
+ }, { ...ctx, rowStretch: false }) })
120390
+ ] });
120391
+ });
120215
120392
  ButtonNode = withExtras("ButtonNode", ({ props, ctx, wrap }) => {
120216
120393
  const C2 = ctx.color;
120217
120394
  const boundTitle = useResolvedScalar(isBinding(props.title) ? props.title : void 0, ctx);
@@ -143473,23 +143650,23 @@ var init_preview_assets_generated = __esm({
143473
143650
  "../../packages/serve/src/preview-assets.generated.ts"() {
143474
143651
  init_preview_assets_format();
143475
143652
  PREVIEW_ASSET_MANIFEST = [
143476
- { key: "assets/index-foA0ySYi.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1191112, sha256: "18d4cc6a89df036267a88103f1c3a1e2bed96cfc78926bf48dc0c2c9b637546d" },
143477
- { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/octet-stream", offset: 1191120, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
143478
- { key: "assets/pdfDocument-mwaz-XmD.js", contentType: "application/javascript; charset=utf-8", offset: 2508154, length: 489166, sha256: "709898ff6336a208e677fff9def67063adaa4605492c81597468199e4968d2d3" },
143479
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 2997320, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
143480
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3001930, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
143481
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3490514, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
143482
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3581438, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
143483
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3672422, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
143484
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3763402, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
143485
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 3854274, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
143486
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 3945254, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
143487
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4053890, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
143488
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4162974, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
143489
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4272022, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
143490
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4379722, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
143491
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4467830, length: 4947, sha256: "58a871eb4e920e5f620d7cbe850b1418ac1cd3e08746145666cad95e2dcb9fd9" },
143492
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4472777, length: 17344, sha256: "d057298d75e7373e5e2c4d11b7d1bcbea88e03af555ba63d97641c78509c58e4" }
143653
+ { key: "assets/index-BZS8K4zM.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1192364, sha256: "50d2524148ee0aedb74e5e6a548f269eb75b4a289f7a1de101de410c01e60bf5" },
143654
+ { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/octet-stream", offset: 1192372, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
143655
+ { key: "assets/pdfDocument-PXxklXbm.js", contentType: "application/javascript; charset=utf-8", offset: 2509406, length: 489166, sha256: "6df079a4bd8eaa2ed5abfb4af8154f2a4c0b8283fb26e1ef2f1964c2d7b6d1ac" },
143656
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 2998572, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
143657
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3003182, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
143658
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3491766, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
143659
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3582690, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
143660
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3673674, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
143661
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3764654, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
143662
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 3855526, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
143663
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 3946506, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
143664
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4055142, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
143665
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4164226, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
143666
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4273274, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
143667
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4380974, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
143668
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4469082, length: 4947, sha256: "ec96c9af3b4b5bf3113b3ef80c4a6f16b77f40e28903dd20813ce866207e145d" },
143669
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4474029, length: 17344, sha256: "d057298d75e7373e5e2c4d11b7d1bcbea88e03af555ba63d97641c78509c58e4" }
143493
143670
  ];
143494
143671
  }
143495
143672
  });
Binary file
@@ -98,7 +98,7 @@
98
98
  "category": "Core",
99
99
  "propsType": "EmptyStateProps",
100
100
  "bindings": [],
101
- "propsSource": "export interface EmptyStateProps {\n title: string;\n subtitle?: string;\n icon?: IconRef;\n action?: Action;\n /** #1059 — CTA label; overridable for localized hosts. Default \"Take action\". */\n actionLabel?: string;\n}"
101
+ "propsSource": "export interface EmptyStateProps {\n title: string;\n subtitle?: string;\n icon?: IconRef;\n /** The CTA. Drawn as a real `Button` (see `actionStyle` / `actionSize`). */\n action?: Action;\n /** #1059 — CTA label, also its accessibility label. REQUIRED when `action` is set:\n * there is no default, `himi validate` reports an error, and no button is drawn. */\n actionLabel?: string;\n /** `typography` token for the title. Default `\"bodyEmphasis\"`. */\n titleStyle?: string;\n /** `typography` token for the subtitle. Default `\"caption\"`. */\n subtitleStyle?: string;\n /** The CTA's Button `style`. Default `\"secondary\"`. */\n actionStyle?: \"primary\" | \"secondary\" | \"plain\";\n /** The CTA's Button `size`. Default `\"sm\"`. */\n actionSize?: \"sm\" | \"md\";\n}"
102
102
  },
103
103
  {
104
104
  "name": "Text",
@@ -396,9 +396,19 @@ export interface EmptyStateProps {
396
396
  title: string;
397
397
  subtitle?: string;
398
398
  icon?: IconRef;
399
+ /** The CTA. Drawn as a real `Button` (see `actionStyle` / `actionSize`). */
399
400
  action?: Action;
400
- /** #1059 — CTA label; overridable for localized hosts. Default "Take action". */
401
+ /** #1059 — CTA label, also its accessibility label. REQUIRED when `action` is set:
402
+ * there is no default, `himi validate` reports an error, and no button is drawn. */
401
403
  actionLabel?: string;
404
+ /** `typography` token for the title. Default `"bodyEmphasis"`. */
405
+ titleStyle?: string;
406
+ /** `typography` token for the subtitle. Default `"caption"`. */
407
+ subtitleStyle?: string;
408
+ /** The CTA's Button `style`. Default `"secondary"`. */
409
+ actionStyle?: "primary" | "secondary" | "plain";
410
+ /** The CTA's Button `size`. Default `"sm"`. */
411
+ actionSize?: "sm" | "md";
402
412
  }
403
413
  ```
404
414
 
@@ -331,7 +331,7 @@ to drive them:
331
331
  | State | Compose with | Notes |
332
332
  |---|---|---|
333
333
  | **Loading** | `Skeleton` (shape of the eventual content) or `StatefulSection` / `ActivityIndicator` | Prefer a content-shaped `Skeleton` over a bare spinner — it previews the layout. The descriptor renders against the state-skeleton, then repaints as worker state lands. |
334
- | **Empty** | `EmptyState` (`title`, `subtitle`, `icon`) **plus a directive action** | An empty screen is an invitation to act, not a dead end — give it the next step (e.g. "Add your first item"). **Caveat:** `EmptyState`'s `action` renders a generic **"Take action"** button label (no field to set the wording). When the CTA text matters ("Clear filters", "Add your first item"), compose a *custom* empty block instead — a centered `icon` + `title` text + `caption` subtitle + a real labeled `button` — rather than the `EmptyState` component. |
334
+ | **Empty** | `EmptyState` (`title`, optional `icon` / `subtitle`) **plus a directive action** (`action` + `actionLabel`) | An empty screen is an invitation to act, not a dead end. Give it the next step, for example `"actionLabel": "Add your first item"`. Use `EmptyState` for **every** empty state, so all empty states in the app have the same layout and style. See [Empty states](#empty-states-one-component-one-layout) below. |
335
335
  | **Error** | `Banner` (or inline) **with a retry action** | Say what failed and offer recovery; never a blank screen or a raw error string. |
336
336
  | **Loaded** | the real content | The one everyone remembers to build. |
337
337
 
@@ -340,12 +340,43 @@ Bind visibility to worker state with `hidden_path` (`$service:…`/`$item.` data
340
340
  components precisely so states are first-class — use them. (Reproducing an existing app? Enumerate
341
341
  every state the *original* shows; the same discipline applies to net-new screens.)
342
342
 
343
+ ### Empty states: one component, one layout
344
+
345
+ `EmptyState` draws one standard layout on iOS, Android, macOS and web. Do not build an empty
346
+ state by hand from `Icon`, `Text` and `Button` nodes.
347
+
348
+ - The layout is a centred vertical stack, in this order: icon tile, title, subtitle, button.
349
+ - Only `title` is required. `icon`, `subtitle` and the button are optional. A missing part
350
+ takes no space.
351
+ - The title uses the `typography` token in `titleStyle`. The default is `bodyEmphasis`.
352
+ - The subtitle uses the `typography` token in `subtitleStyle`. The default is `caption`.
353
+ - The padding and the gaps come from the `spacing` tokens `lg`, `md` and `xs`.
354
+ - The button is a real `Button`. It uses the app's button tokens. Set `actionStyle`
355
+ (`primary`, `secondary` or `plain`) and `actionSize` (`sm` or `md`). The defaults are
356
+ `secondary` and `sm`.
357
+ - `actionLabel` is the button text and its accessibility label. It has no default. If you set
358
+ `action` without `actionLabel`, `himi validate` shows an error and no button is drawn.
359
+
360
+ ```jsonc
361
+ { "id": "saved.empty",
362
+ "body": { "package": "himalaya", "type": "EmptyState",
363
+ "props": { "icon": "bookmark", "title": "No saved items",
364
+ "subtitle": "Save a product to see it here.", "subtitleStyle": "footnoteRegular",
365
+ "action": { "actionId": "himi.open", "params": { "screenId": "shop" } },
366
+ "actionLabel": "Browse products" } },
367
+ "modifiers": { "hiddenPath": "$service:saved.notEmpty" } }
368
+ ```
369
+
370
+ To show the empty state only when the list is empty, bind `hiddenPath` to a worker boolean
371
+ (see `himi docs recipe-four-state`). Write the title and the subtitle as literal copy or as
372
+ `$t:` keys. Today, iOS and Android do not resolve a `$service:` binding in `title` or `subtitle`.
373
+
343
374
  **Loading & empty states are first-class — use the `Skeleton` and `EmptyState` component types.**
344
375
 
345
376
  There is **no builder-DSL package** — the `@wix/himalaya` umbrella exposes `./core`, `./state`,
346
377
  `wix-*`, … but no `skRect` / `emptyBlock` helpers. Author these states as the **raw descriptor
347
378
  nodes** `himi init` scaffolds: a `Skeleton` node for loading, and an `EmptyState` node (`icon` +
348
- `title`/`subtitle` + a labeled `cta`) for empty. Run `himi components EmptyState` /
379
+ `title`/`subtitle` + `action` with its `actionLabel`) for empty. Run `himi components EmptyState` /
349
380
  `himi components Skeleton` for the exact props. Written as raw nodes, these states ship over the
350
381
  air and render on every current binary.
351
382
 
@@ -5,7 +5,8 @@ from **one flat layout** — no conditional trees, no separate loading screen. T
5
5
  worker computes a **display string per state** (non-empty only in its state) and
6
6
  the descriptor binds each to a `Text` with `collapseWhenEmpty: true`. Sections
7
7
  that must fully appear/disappear gate with the `hiddenWhen`/`hiddenPath` modifier
8
- bound to a worker boolean.
8
+ bound to a worker boolean. The empty state is always an `EmptyState` node, gated
9
+ the same way.
9
10
 
10
11
  This is the corpus norm: of the in-repo worker bundles, ~3 in 4 model loading,
11
12
  more than half model error, and the reference apps (`apps/himi-owner-lite`,
@@ -19,8 +20,8 @@ interface State {
19
20
  status: "loading" | "ready" | "error";
20
21
  items: Row[];
21
22
  loadingText: string; // "Loading…" only while loading
22
- emptyText: string; // "No products yet." only when ready + 0 rows
23
23
  error: string; // the message only when failed
24
+ notEmpty: boolean; // gates the EmptyState (hiddenPath hides when TRUE)
24
25
  notErrored: boolean; // gates the Retry button (hiddenPath hides when TRUE)
25
26
  }
26
27
 
@@ -30,24 +31,29 @@ function shape(s: Partial<State>): State {
30
31
  return {
31
32
  status, items,
32
33
  loadingText: status === "loading" ? "Loading products…" : "",
33
- emptyText: status === "ready" && items.length === 0 ? "No products in this store yet." : "",
34
34
  error: s.error ?? "",
35
+ notEmpty: !(status === "ready" && items.length === 0),
35
36
  notErrored: status !== "error",
36
37
  };
37
38
  }
38
39
  ```
39
40
 
40
41
  Route **every** state change through `shape()` — set `status` + raw data, let it
41
- recompute the strings. Hand-maintaining the strings per action is how screens end
42
- up loading-and-errored at once.
42
+ recompute the strings and flags. Hand-maintaining them per action is how screens
43
+ end up loading-and-errored at once.
43
44
 
44
- ## 2. The descriptor: one Text per state + a gated Retry
45
+ ## 2. The descriptor: one node per state + a gated Retry
45
46
 
46
47
  ```jsonc
47
48
  { "id": "loading", "…Text": { "text": "$service:products.loadingText",
48
49
  "color": "textSecondary", "collapseWhenEmpty": true } },
49
- { "id": "empty", "…Text": { "text": "$service:products.emptyText",
50
- "color": "textSecondary", "collapseWhenEmpty": true } },
50
+ { "id": "empty",
51
+ "body": { "package": "himalaya", "type": "EmptyState",
52
+ "props": { "icon": "bag", "title": "No products in this store yet",
53
+ "subtitle": "Add a product to see it here.",
54
+ "action": { "actionId": "tier5.products.create" },
55
+ "actionLabel": "Add your first product" } },
56
+ "modifiers": { "hiddenPath": "$service:products.notEmpty" } },
51
57
  { "id": "error", "…Text": { "text": "$service:products.error",
52
58
  "color": "danger", "collapseWhenEmpty": true } },
53
59
  { "id": "retry",
@@ -64,6 +70,13 @@ up loading-and-errored at once.
64
70
  the binding reads without negation.
65
71
  - Ready state is simply the data itself (the list rows, the detail fields) — it
66
72
  needs no dedicated marker.
73
+ - **Use `EmptyState` for every empty state.** It draws one standard layout on
74
+ every platform: icon, title, subtitle and button, with type and gaps from the
75
+ app's tokens. Only `title` is required. `actionLabel` is required when you set
76
+ `action`: there is no default label, and `himi validate` reports a missing one.
77
+ - Write the `EmptyState` title and subtitle as literal copy or `$t:` keys. Today,
78
+ iOS and Android do not resolve a `$service:` binding in those props, so gate the
79
+ node with `hiddenPath` instead of binding its text.
67
80
 
68
81
  ## 3. Which state when
69
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1237.0",
3
+ "version": "0.1238.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "c18693816eefde3cc9259c9dd46e8deaffa629c1762386017a0ea310"
70
+ "falconPackageHash": "fa100c156434113a7befa8bdb97c6f87ee57d27d3e088aa3b2a9752c"
71
71
  }