@bettercms-ai/kit 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/adapters/dock-values.d.ts +28 -0
- package/dist/adapters/dock-values.js +41 -0
- package/dist/adapters/dock-values.js.map +1 -0
- package/{adapters → dist/adapters}/icons.js +2 -2
- package/dist/adapters/icons.js.map +1 -0
- package/{adapters → dist/adapters}/lossy.d.ts +90 -31
- package/dist/adapters/lossy.js +414 -0
- package/dist/adapters/lossy.js.map +1 -0
- package/{adapters → dist/adapters}/prod-types.d.ts +82 -4
- package/{adapters → dist/adapters}/prod-types.js +71 -3
- package/dist/adapters/prod-types.js.map +1 -0
- package/{adapters → dist/adapters}/schema.d.ts +40 -1
- package/{adapters → dist/adapters}/schema.js +996 -179
- package/dist/adapters/schema.js.map +1 -0
- package/{adapters → dist/adapters}/values.d.ts +26 -5
- package/{adapters → dist/adapters}/values.js +87 -17
- package/dist/adapters/values.js.map +1 -0
- package/dist/brand/logo.js +96 -0
- package/dist/brand/logo.js.map +1 -0
- package/{context.d.ts → dist/context.d.ts} +92 -2
- package/{context.js → dist/context.js} +5 -2
- package/dist/context.js.map +1 -0
- package/{controls → dist/controls}/button.d.ts +19 -5
- package/{controls → dist/controls}/button.js +125 -54
- package/dist/controls/button.js.map +1 -0
- package/{controls → dist/controls}/c1.js +1 -1
- package/dist/controls/c1.js.map +1 -0
- package/{controls → dist/controls}/c2.js +1 -1
- package/dist/controls/c2.js.map +1 -0
- package/{controls → dist/controls}/choice.d.ts +7 -2
- package/{controls → dist/controls}/choice.js +71 -18
- package/dist/controls/choice.js.map +1 -0
- package/{controls → dist/controls}/code-register.js +1 -1
- package/dist/controls/code-register.js.map +1 -0
- package/{controls → dist/controls}/code.js +10 -9
- package/dist/controls/code.js.map +1 -0
- package/{controls → dist/controls}/color.js +8 -8
- package/dist/controls/color.js.map +1 -0
- package/{controls → dist/controls}/component-services.d.ts +42 -0
- package/dist/controls/component-services.js +7 -0
- package/dist/controls/component-services.js.map +1 -0
- package/{controls → dist/controls}/date-register.js +1 -1
- package/dist/controls/date-register.js.map +1 -0
- package/{controls → dist/controls}/date-zone.js +8 -2
- package/dist/controls/date-zone.js.map +1 -0
- package/{controls → dist/controls}/date.js +32 -24
- package/dist/controls/date.js.map +1 -0
- package/{controls → dist/controls}/form-picker-register.js +1 -1
- package/dist/controls/form-picker-register.js.map +1 -0
- package/{controls → dist/controls}/form-picker.js +11 -9
- package/dist/controls/form-picker.js.map +1 -0
- package/{controls → dist/controls}/icon-register.js +1 -1
- package/dist/controls/icon-register.js.map +1 -0
- package/{controls → dist/controls}/icon.d.ts +4 -2
- package/{controls → dist/controls}/icon.js +32 -27
- package/dist/controls/icon.js.map +1 -0
- package/{controls → dist/controls}/index.d.ts +1 -1
- package/{controls → dist/controls}/index.js +4 -3
- package/dist/controls/index.js.map +1 -0
- package/{controls → dist/controls}/json-register.js +1 -1
- package/dist/controls/json-register.js.map +1 -0
- package/{controls → dist/controls}/json.js +13 -3
- package/dist/controls/json.js.map +1 -0
- package/{controls → dist/controls}/link.js +6 -6
- package/dist/controls/link.js.map +1 -0
- package/{controls → dist/controls}/location-register.js +1 -1
- package/dist/controls/location-register.js.map +1 -0
- package/{controls → dist/controls}/location.js +64 -40
- package/dist/controls/location.js.map +1 -0
- package/{controls → dist/controls}/media-register.js +1 -1
- package/dist/controls/media-register.js.map +1 -0
- package/{controls → dist/controls}/media-services.d.ts +6 -0
- package/dist/controls/media-services.js +0 -0
- package/{controls → dist/controls}/media.d.ts +35 -4
- package/{controls → dist/controls}/media.js +491 -171
- package/dist/controls/media.js.map +1 -0
- package/{controls → dist/controls}/number.js +10 -9
- package/dist/controls/number.js.map +1 -0
- package/{controls → dist/controls}/reference-register.js +1 -1
- package/dist/controls/reference-register.js.map +1 -0
- package/{controls → dist/controls}/reference-services.d.ts +4 -0
- package/dist/controls/reference-services.js +0 -0
- package/{controls → dist/controls}/reference.d.ts +1 -1
- package/{controls → dist/controls}/reference.js +93 -77
- package/dist/controls/reference.js.map +1 -0
- package/dist/controls/rich-text/blocks.d.ts +49 -0
- package/{controls → dist/controls}/rich-text/blocks.js +471 -144
- package/dist/controls/rich-text/blocks.js.map +1 -0
- package/{controls → dist/controls}/rich-text/catalog.d.ts +4 -1
- package/{controls → dist/controls}/rich-text/catalog.js +75 -65
- package/dist/controls/rich-text/catalog.js.map +1 -0
- package/{controls → dist/controls}/rich-text/codec.d.ts +9 -1
- package/{controls → dist/controls}/rich-text/codec.js +3 -1
- package/dist/controls/rich-text/codec.js.map +1 -0
- package/{controls → dist/controls}/rich-text/component-card.d.ts +1 -1
- package/{controls → dist/controls}/rich-text/component-card.js +417 -99
- package/dist/controls/rich-text/component-card.js.map +1 -0
- package/dist/controls/rich-text/component-panel.d.ts +47 -0
- package/dist/controls/rich-text/component-panel.js +141 -0
- package/dist/controls/rich-text/component-panel.js.map +1 -0
- package/{controls → dist/controls}/rich-text/control.d.ts +1 -2
- package/{controls → dist/controls}/rich-text/control.js +84 -42
- package/dist/controls/rich-text/control.js.map +1 -0
- package/{controls → dist/controls}/rich-text/dom.d.ts +52 -9
- package/{controls → dist/controls}/rich-text/dom.js +379 -50
- package/dist/controls/rich-text/dom.js.map +1 -0
- package/{controls → dist/controls}/rich-text/editor.d.ts +7 -0
- package/{controls → dist/controls}/rich-text/editor.js +148 -48
- package/dist/controls/rich-text/editor.js.map +1 -0
- package/{controls → dist/controls}/rich-text/floating.d.ts +6 -1
- package/{controls → dist/controls}/rich-text/floating.js +35 -18
- package/dist/controls/rich-text/floating.js.map +1 -0
- package/{controls → dist/controls}/rich-text/index.js +1 -1
- package/dist/controls/rich-text/index.js.map +1 -0
- package/dist/controls/rich-text/link-form.js +147 -0
- package/dist/controls/rich-text/link-form.js.map +1 -0
- package/{controls → dist/controls}/rich-text/markdown.js +4 -2
- package/dist/controls/rich-text/markdown.js.map +1 -0
- package/{controls → dist/controls}/rich-text/ops.js +17 -15
- package/dist/controls/rich-text/ops.js.map +1 -0
- package/dist/controls/rich-text/plain.d.ts +17 -0
- package/dist/controls/rich-text/plain.js +44 -0
- package/dist/controls/rich-text/plain.js.map +1 -0
- package/{controls → dist/controls}/rich-text/toolbar.d.ts +6 -2
- package/{controls → dist/controls}/rich-text/toolbar.js +12 -7
- package/dist/controls/rich-text/toolbar.js.map +1 -0
- package/{controls → dist/controls}/scale.js +5 -5
- package/dist/controls/scale.js.map +1 -0
- package/{controls → dist/controls}/slot-register.js +1 -1
- package/dist/controls/slot-register.js.map +1 -0
- package/{controls → dist/controls}/slot-services.d.ts +25 -0
- package/dist/controls/slot-services.js +155 -0
- package/dist/controls/slot-services.js.map +1 -0
- package/{controls → dist/controls}/slot.js +42 -24
- package/dist/controls/slot.js.map +1 -0
- package/{controls → dist/controls}/table/index.js +1 -1
- package/dist/controls/table/index.js.map +1 -0
- package/{controls → dist/controls}/table/prod.js +3 -3
- package/dist/controls/table/prod.js.map +1 -0
- package/{controls → dist/controls}/table/refs.js +13 -2
- package/dist/controls/table/refs.js.map +1 -0
- package/{controls → dist/controls}/table/stored.js +1 -1
- package/dist/controls/table/stored.js.map +1 -0
- package/{controls → dist/controls}/table/table-editor.js +51 -48
- package/dist/controls/table/table-editor.js.map +1 -0
- package/{controls → dist/controls}/table/table-link.js +3 -3
- package/dist/controls/table/table-link.js.map +1 -0
- package/{controls → dist/controls}/table/table.js +3 -3
- package/dist/controls/table/table.js.map +1 -0
- package/{controls → dist/controls}/text.d.ts +2 -2
- package/{controls → dist/controls}/text.js +78 -43
- package/dist/controls/text.js.map +1 -0
- package/dist/controls/unknown-value.d.ts +17 -0
- package/dist/controls/unknown-value.js +66 -0
- package/dist/controls/unknown-value.js.map +1 -0
- package/dist/core/core.d.ts +63 -0
- package/dist/core/core.js +171 -0
- package/dist/core/core.js.map +1 -0
- package/dist/core/facets.d.ts +347 -0
- package/dist/core/facets.js +334 -0
- package/dist/core/facets.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +3 -0
- package/{engine → dist/engine}/array.js +34 -21
- package/dist/engine/array.js.map +1 -0
- package/{engine → dist/engine}/bits.js +1 -1
- package/dist/engine/bits.js.map +1 -0
- package/{engine → dist/engine}/breadcrumb.d.ts +3 -3
- package/{engine → dist/engine}/breadcrumb.js +12 -36
- package/dist/engine/breadcrumb.js.map +1 -0
- package/{engine → dist/engine}/expand.d.ts +2 -4
- package/{engine → dist/engine}/expand.js +5 -22
- package/dist/engine/expand.js.map +1 -0
- package/{engine → dist/engine}/field-list.js +1 -1
- package/dist/engine/field-list.js.map +1 -0
- package/{engine → dist/engine}/field-shell.js +249 -121
- package/dist/engine/field-shell.js.map +1 -0
- package/{engine → dist/engine}/frame.d.ts +3 -1
- package/{engine → dist/engine}/frame.js +16 -14
- package/dist/engine/frame.js.map +1 -0
- package/{engine → dist/engine}/group-summary.d.ts +2 -1
- package/{engine → dist/engine}/group-summary.js +7 -3
- package/dist/engine/group-summary.js.map +1 -0
- package/{engine → dist/engine}/grouping.d.ts +19 -5
- package/{engine → dist/engine}/grouping.js +193 -21
- package/dist/engine/grouping.js.map +1 -0
- package/{engine → dist/engine}/item-list.d.ts +11 -1
- package/{engine → dist/engine}/item-list.js +35 -13
- package/dist/engine/item-list.js.map +1 -0
- package/{engine → dist/engine}/kit-field.js +45 -10
- package/dist/engine/kit-field.js.map +1 -0
- package/{engine → dist/engine}/schema-form.d.ts +27 -2
- package/{engine → dist/engine}/schema-form.js +182 -40
- package/dist/engine/schema-form.js.map +1 -0
- package/{engine → dist/engine}/slot.js +5 -15
- package/dist/engine/slot.js.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/{kit.css → dist/kit.css} +19 -0
- package/{lib → dist/lib}/cn.js +7 -4
- package/dist/lib/cn.js.map +1 -0
- package/{lib → dist/lib}/columns.js +4 -1
- package/dist/lib/columns.js.map +1 -0
- package/{lib → dist/lib}/frames.js +2 -2
- package/dist/lib/frames.js.map +1 -0
- package/dist/lib/icon-image.d.ts +10 -0
- package/dist/lib/icon-image.js +8 -0
- package/dist/lib/icon-image.js.map +1 -0
- package/{lib → dist/lib}/icons.js +4 -3
- package/dist/lib/icons.js.map +1 -0
- package/dist/lib/input-modality.d.ts +11 -0
- package/dist/lib/input-modality.js +47 -0
- package/dist/lib/input-modality.js.map +1 -0
- package/dist/lib/key-event.d.ts +11 -0
- package/dist/lib/key-event.js +16 -0
- package/dist/lib/key-event.js.map +1 -0
- package/{lib → dist/lib}/path.js +4 -2
- package/dist/lib/path.js.map +1 -0
- package/{lib → dist/lib}/schema.d.ts +34 -3
- package/{lib → dist/lib}/schema.js +110 -13
- package/dist/lib/schema.js.map +1 -0
- package/{lib → dist/lib}/utils.js +1 -1
- package/dist/lib/utils.js.map +1 -0
- package/{lib → dist/lib}/validate.d.ts +11 -1
- package/{lib → dist/lib}/validate.js +85 -11
- package/dist/lib/validate.js.map +1 -0
- package/dist/library/analysis.d.ts +23 -0
- package/dist/library/analysis.js +160 -0
- package/dist/library/analysis.js.map +1 -0
- package/dist/library/catalog.d.ts +57 -0
- package/dist/library/catalog.js +307 -0
- package/dist/library/catalog.js.map +1 -0
- package/dist/library/coreNotes.d.ts +8 -0
- package/dist/library/coreNotes.js +111 -0
- package/dist/library/coreNotes.js.map +1 -0
- package/dist/library/coverage.d.ts +40 -0
- package/dist/library/coverage.js +59 -0
- package/dist/library/coverage.js.map +1 -0
- package/dist/library/entries/advanced.entries.d.ts +2 -0
- package/dist/library/entries/advanced.entries.js +6579 -0
- package/dist/library/entries/advanced.entries.js.map +1 -0
- package/dist/library/entries/commerce.entries.d.ts +2 -0
- package/dist/library/entries/commerce.entries.js +4140 -0
- package/dist/library/entries/commerce.entries.js.map +1 -0
- package/dist/library/entries/company.entries.d.ts +2 -0
- package/dist/library/entries/company.entries.js +2731 -0
- package/dist/library/entries/company.entries.js.map +1 -0
- package/dist/library/entries/conversion.entries.d.ts +2 -0
- package/dist/library/entries/conversion.entries.js +3707 -0
- package/dist/library/entries/conversion.entries.js.map +1 -0
- package/dist/library/entries/data.entries.d.ts +7 -0
- package/dist/library/entries/data.entries.js +4613 -0
- package/dist/library/entries/data.entries.js.map +1 -0
- package/dist/library/entries/events.entries.d.ts +2 -0
- package/dist/library/entries/events.entries.js +3428 -0
- package/dist/library/entries/events.entries.js.map +1 -0
- package/dist/library/entries/features.entries.d.ts +2 -0
- package/dist/library/entries/features.entries.js +3157 -0
- package/dist/library/entries/features.entries.js.map +1 -0
- package/dist/library/entries/heroes.entries.d.ts +2 -0
- package/dist/library/entries/heroes.entries.js +3401 -0
- package/dist/library/entries/heroes.entries.js.map +1 -0
- package/dist/library/entries/index.d.ts +3 -0
- package/dist/library/entries/index.js +35 -0
- package/dist/library/entries/index.js.map +1 -0
- package/dist/library/entries/listings.entries.d.ts +2 -0
- package/dist/library/entries/listings.entries.js +2976 -0
- package/dist/library/entries/listings.entries.js.map +1 -0
- package/dist/library/entries/longform.entries.d.ts +2 -0
- package/dist/library/entries/longform.entries.js +4227 -0
- package/dist/library/entries/longform.entries.js.map +1 -0
- package/dist/library/entries/media.entries.d.ts +2 -0
- package/dist/library/entries/media.entries.js +2781 -0
- package/dist/library/entries/media.entries.js.map +1 -0
- package/dist/library/entries/navigation.entries.d.ts +2 -0
- package/dist/library/entries/navigation.entries.js +4011 -0
- package/dist/library/entries/navigation.entries.js.map +1 -0
- package/dist/library/entries/pricing.entries.d.ts +2 -0
- package/dist/library/entries/pricing.entries.js +2383 -0
- package/dist/library/entries/pricing.entries.js.map +1 -0
- package/dist/library/entries/social-proof.entries.d.ts +2 -0
- package/dist/library/entries/social-proof.entries.js +2297 -0
- package/dist/library/entries/social-proof.entries.js.map +1 -0
- package/dist/library/examples/bento.d.ts +92 -0
- package/dist/library/examples/bento.js +201 -0
- package/dist/library/examples/bento.js.map +1 -0
- package/dist/library/examples/blocks.d.ts +14 -0
- package/dist/library/examples/blocks.js +313 -0
- package/dist/library/examples/blocks.js.map +1 -0
- package/dist/library/examples/faq.d.ts +28 -0
- package/dist/library/examples/faq.js +230 -0
- package/dist/library/examples/faq.js.map +1 -0
- package/dist/library/examples/hero.d.ts +43 -0
- package/dist/library/examples/hero.js +222 -0
- package/dist/library/examples/hero.js.map +1 -0
- package/dist/library/examples/index.d.ts +36 -0
- package/dist/library/examples/index.js +109 -0
- package/dist/library/examples/index.js.map +1 -0
- package/dist/library/examples/journalStory.d.ts +93 -0
- package/dist/library/examples/journalStory.js +338 -0
- package/dist/library/examples/journalStory.js.map +1 -0
- package/dist/library/examples/megaMenu.d.ts +134 -0
- package/dist/library/examples/megaMenu.js +442 -0
- package/dist/library/examples/megaMenu.js.map +1 -0
- package/dist/library/examples/pricing.d.ts +103 -0
- package/dist/library/examples/pricing.js +479 -0
- package/dist/library/examples/pricing.js.map +1 -0
- package/dist/library/examples/tabs.d.ts +87 -0
- package/dist/library/examples/tabs.js +180 -0
- package/dist/library/examples/tabs.js.map +1 -0
- package/dist/library/export.d.ts +42 -0
- package/dist/library/export.js +80 -0
- package/dist/library/export.js.map +1 -0
- package/dist/library/index.d.ts +13 -0
- package/dist/library/index.js +6 -0
- package/dist/library/kit-catalog.json +1510 -0
- package/dist/library/types.d.ts +42 -0
- package/dist/library/types.js +0 -0
- package/{schema → dist/schema}/adapters.js +1 -2
- package/dist/schema/adapters.js.map +1 -0
- package/{schema → dist/schema}/help.d.ts +2 -0
- package/{schema → dist/schema}/help.js +6 -4
- package/dist/schema/help.js.map +1 -0
- package/{schema → dist/schema}/scale.js +1 -1
- package/dist/schema/scale.js.map +1 -0
- package/{schema → dist/schema}/sources.js +1 -1
- package/dist/schema/sources.js.map +1 -0
- package/{schema → dist/schema}/table.js +11 -10
- package/dist/schema/table.js.map +1 -0
- package/{schema → dist/schema}/types.d.ts +29 -1
- package/{schema → dist/schema}/types.js +1 -1
- package/dist/schema/types.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/table/table-view.d.ts +14 -0
- package/dist/table/table-view.js +140 -0
- package/dist/table/table-view.js.map +1 -0
- package/dist/table/table.css +73 -0
- package/dist/testing/corpus.d.ts +63 -0
- package/dist/testing/corpus.js +129 -0
- package/dist/testing/corpus.js.map +1 -0
- package/dist/testing/test-form.d.ts +11 -0
- package/dist/testing/test-form.js +38 -0
- package/dist/testing/test-form.js.map +1 -0
- package/dist/theme.css +345 -0
- package/dist/theme.d.ts +22 -0
- package/dist/theme.js +43 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.css +542 -0
- package/dist/types.js +0 -0
- package/{ui → dist/ui}/action-tray.d.ts +1 -1
- package/{ui → dist/ui}/action-tray.js +20 -19
- package/dist/ui/action-tray.js.map +1 -0
- package/dist/ui/autocomplete.d.ts +30 -0
- package/dist/ui/autocomplete.js +139 -0
- package/dist/ui/autocomplete.js.map +1 -0
- package/{ui → dist/ui}/button.d.ts +3 -3
- package/{ui → dist/ui}/button.js +6 -7
- package/dist/ui/button.js.map +1 -0
- package/{ui → dist/ui}/choice-switch.js +1 -1
- package/dist/ui/choice-switch.js.map +1 -0
- package/{ui → dist/ui}/controls.js +51 -39
- package/dist/ui/controls.js.map +1 -0
- package/{ui → dist/ui}/display.d.ts +10 -3
- package/{ui → dist/ui}/display.js +57 -46
- package/dist/ui/display.js.map +1 -0
- package/{ui → dist/ui}/expanded-editor-modal.d.ts +2 -2
- package/{ui → dist/ui}/expanded-editor-modal.js +6 -6
- package/dist/ui/expanded-editor-modal.js.map +1 -0
- package/{ui → dist/ui}/field-context.js +1 -1
- package/dist/ui/field-context.js.map +1 -0
- package/{ui → dist/ui}/floating-panel.d.ts +1 -1
- package/{ui → dist/ui}/floating-panel.js +28 -12
- package/dist/ui/floating-panel.js.map +1 -0
- package/{ui → dist/ui}/index.d.ts +4 -0
- package/dist/ui/index.js +18 -0
- package/{ui → dist/ui}/input.d.ts +1 -1
- package/{ui → dist/ui}/input.js +9 -9
- package/dist/ui/input.js.map +1 -0
- package/{ui → dist/ui}/mode-bar.js +3 -3
- package/dist/ui/mode-bar.js.map +1 -0
- package/{ui → dist/ui}/overlay.d.ts +77 -5
- package/dist/ui/overlay.js +697 -0
- package/dist/ui/overlay.js.map +1 -0
- package/{ui → dist/ui}/path-crumbs.js +37 -3
- package/dist/ui/path-crumbs.js.map +1 -0
- package/{ui → dist/ui}/presence.js +2 -2
- package/dist/ui/presence.js.map +1 -0
- package/{ui → dist/ui}/select.js +89 -87
- package/dist/ui/select.js.map +1 -0
- package/{ui → dist/ui}/shortcut-hint.js +1 -1
- package/dist/ui/shortcut-hint.js.map +1 -0
- package/dist/ui/spinner.d.ts +24 -0
- package/dist/ui/spinner.js +42 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/split-button.d.ts +32 -0
- package/dist/ui/split-button.js +60 -0
- package/dist/ui/split-button.js.map +1 -0
- package/{ui → dist/ui}/surfaces.js +1 -1
- package/dist/ui/surfaces.js.map +1 -0
- package/dist/ui/tab.d.ts +38 -0
- package/dist/ui/tab.js +87 -0
- package/dist/ui/tab.js.map +1 -0
- package/dist/ui/toast.d.ts +20 -0
- package/dist/ui/toast.js +115 -0
- package/dist/ui/toast.js.map +1 -0
- package/{ui → dist/ui}/tooltip-root.d.ts +4 -3
- package/{ui → dist/ui}/tooltip-root.js +9 -8
- package/dist/ui/tooltip-root.js.map +1 -0
- package/{ui → dist/ui}/variants.js +1 -1
- package/dist/ui/variants.js.map +1 -0
- package/package.json +77 -23
- package/adapters/icons.js.map +0 -1
- package/adapters/lossy.js +0 -233
- package/adapters/lossy.js.map +0 -1
- package/adapters/prod-types.js.map +0 -1
- package/adapters/schema.js.map +0 -1
- package/adapters/values.js.map +0 -1
- package/brand/logo.js +0 -9
- package/brand/logo.js.map +0 -1
- package/context.js.map +0 -1
- package/controls/button.js.map +0 -1
- package/controls/c1.js.map +0 -1
- package/controls/c2.js.map +0 -1
- package/controls/choice.js.map +0 -1
- package/controls/code-register.js.map +0 -1
- package/controls/code.js.map +0 -1
- package/controls/color.js.map +0 -1
- package/controls/date-register.js.map +0 -1
- package/controls/date-zone.js.map +0 -1
- package/controls/date.js.map +0 -1
- package/controls/form-picker-register.js.map +0 -1
- package/controls/form-picker.js.map +0 -1
- package/controls/icon-register.js.map +0 -1
- package/controls/icon.js.map +0 -1
- package/controls/index.js.map +0 -1
- package/controls/json-register.js.map +0 -1
- package/controls/json.js.map +0 -1
- package/controls/link.js.map +0 -1
- package/controls/location-register.js.map +0 -1
- package/controls/location.js.map +0 -1
- package/controls/media-register.js.map +0 -1
- package/controls/media.js.map +0 -1
- package/controls/number.js.map +0 -1
- package/controls/reference-register.js.map +0 -1
- package/controls/reference.js.map +0 -1
- package/controls/rich-text/blocks.d.ts +0 -28
- package/controls/rich-text/blocks.js.map +0 -1
- package/controls/rich-text/catalog.js.map +0 -1
- package/controls/rich-text/codec.js.map +0 -1
- package/controls/rich-text/component-card.js.map +0 -1
- package/controls/rich-text/control.js.map +0 -1
- package/controls/rich-text/dom.js.map +0 -1
- package/controls/rich-text/editor.js.map +0 -1
- package/controls/rich-text/floating.js.map +0 -1
- package/controls/rich-text/index.js.map +0 -1
- package/controls/rich-text/link-form.js +0 -111
- package/controls/rich-text/link-form.js.map +0 -1
- package/controls/rich-text/markdown.js.map +0 -1
- package/controls/rich-text/ops.js.map +0 -1
- package/controls/rich-text/toolbar.js.map +0 -1
- package/controls/scale.js.map +0 -1
- package/controls/slot-register.js.map +0 -1
- package/controls/slot-services.js +0 -38
- package/controls/slot-services.js.map +0 -1
- package/controls/slot.js.map +0 -1
- package/controls/table/index.js.map +0 -1
- package/controls/table/prod.js.map +0 -1
- package/controls/table/refs.js.map +0 -1
- package/controls/table/stored.js.map +0 -1
- package/controls/table/table-editor.js.map +0 -1
- package/controls/table/table-link.js.map +0 -1
- package/controls/table/table.js.map +0 -1
- package/controls/text.js.map +0 -1
- package/engine/array.js.map +0 -1
- package/engine/bits.js.map +0 -1
- package/engine/breadcrumb.js.map +0 -1
- package/engine/expand.js.map +0 -1
- package/engine/field-list.js.map +0 -1
- package/engine/field-shell.js.map +0 -1
- package/engine/frame.js.map +0 -1
- package/engine/group-summary.js.map +0 -1
- package/engine/grouping.js.map +0 -1
- package/engine/item-list.js.map +0 -1
- package/engine/kit-field.js.map +0 -1
- package/engine/schema-form.js.map +0 -1
- package/engine/slot.js.map +0 -1
- package/index.js +0 -55
- package/index.js.map +0 -1
- package/lib/cn.js.map +0 -1
- package/lib/columns.js.map +0 -1
- package/lib/frames.js.map +0 -1
- package/lib/icons.js.map +0 -1
- package/lib/path.js.map +0 -1
- package/lib/schema.js.map +0 -1
- package/lib/utils.js.map +0 -1
- package/lib/validate.js.map +0 -1
- package/schema/adapters.js.map +0 -1
- package/schema/help.js.map +0 -1
- package/schema/scale.js.map +0 -1
- package/schema/sources.js.map +0 -1
- package/schema/table.js.map +0 -1
- package/schema/types.js.map +0 -1
- package/ui/action-tray.js.map +0 -1
- package/ui/button.js.map +0 -1
- package/ui/choice-switch.js.map +0 -1
- package/ui/controls.js.map +0 -1
- package/ui/display.js.map +0 -1
- package/ui/expanded-editor-modal.js.map +0 -1
- package/ui/field-context.js.map +0 -1
- package/ui/floating-panel.js.map +0 -1
- package/ui/index.js +0 -13
- package/ui/input.js.map +0 -1
- package/ui/mode-bar.js.map +0 -1
- package/ui/overlay.js +0 -378
- package/ui/overlay.js.map +0 -1
- package/ui/path-crumbs.js.map +0 -1
- package/ui/presence.js.map +0 -1
- package/ui/select.js.map +0 -1
- package/ui/shortcut-hint.js.map +0 -1
- package/ui/surfaces.js.map +0 -1
- package/ui/tooltip-root.js.map +0 -1
- package/ui/variants.js.map +0 -1
- /package/{_virtual → dist/_virtual}/_rolldown/runtime.js +0 -0
- /package/{adapters → dist/adapters}/icons.d.ts +0 -0
- /package/{brand → dist/brand}/logo.d.ts +0 -0
- /package/{controls → dist/controls}/c1.d.ts +0 -0
- /package/{controls → dist/controls}/c2.d.ts +0 -0
- /package/{controls → dist/controls}/code-register.d.ts +0 -0
- /package/{controls → dist/controls}/code.d.ts +0 -0
- /package/{controls → dist/controls}/color.d.ts +0 -0
- /package/{controls → dist/controls}/date-register.d.ts +0 -0
- /package/{controls → dist/controls}/date-zone.d.ts +0 -0
- /package/{controls → dist/controls}/date.d.ts +0 -0
- /package/{controls → dist/controls}/form-picker-register.d.ts +0 -0
- /package/{controls → dist/controls}/form-picker.d.ts +0 -0
- /package/{controls → dist/controls}/icon-register.d.ts +0 -0
- /package/{controls → dist/controls}/json-register.d.ts +0 -0
- /package/{controls → dist/controls}/json.d.ts +0 -0
- /package/{controls → dist/controls}/link.d.ts +0 -0
- /package/{controls → dist/controls}/location-register.d.ts +0 -0
- /package/{controls → dist/controls}/location.d.ts +0 -0
- /package/{controls → dist/controls}/media-register.d.ts +0 -0
- /package/{controls → dist/controls}/number.d.ts +0 -0
- /package/{controls → dist/controls}/reference-register.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/index.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/link-form.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/markdown.d.ts +0 -0
- /package/{controls → dist/controls}/rich-text/ops.d.ts +0 -0
- /package/{controls → dist/controls}/scale.d.ts +0 -0
- /package/{controls → dist/controls}/slot-register.d.ts +0 -0
- /package/{controls → dist/controls}/slot.d.ts +0 -0
- /package/{controls → dist/controls}/table/index.d.ts +0 -0
- /package/{controls → dist/controls}/table/prod.d.ts +0 -0
- /package/{controls → dist/controls}/table/refs.d.ts +0 -0
- /package/{controls → dist/controls}/table/stored.d.ts +0 -0
- /package/{controls → dist/controls}/table/table-editor.d.ts +0 -0
- /package/{controls → dist/controls}/table/table-link.d.ts +0 -0
- /package/{controls → dist/controls}/table/table.d.ts +0 -0
- /package/{engine → dist/engine}/array.d.ts +0 -0
- /package/{engine → dist/engine}/bits.d.ts +0 -0
- /package/{engine → dist/engine}/field-list.d.ts +0 -0
- /package/{engine → dist/engine}/field-shell.d.ts +0 -0
- /package/{engine → dist/engine}/kit-field.d.ts +0 -0
- /package/{engine → dist/engine}/slot.d.ts +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{lib → dist/lib}/cn.d.ts +0 -0
- /package/{lib → dist/lib}/columns.d.ts +0 -0
- /package/{lib → dist/lib}/frames.d.ts +0 -0
- /package/{lib → dist/lib}/icons.d.ts +0 -0
- /package/{lib → dist/lib}/path.d.ts +0 -0
- /package/{lib → dist/lib}/utils.d.ts +0 -0
- /package/{schema → dist/schema}/adapters.d.ts +0 -0
- /package/{schema → dist/schema}/scale.d.ts +0 -0
- /package/{schema → dist/schema}/sources.d.ts +0 -0
- /package/{schema → dist/schema}/table.d.ts +0 -0
- /package/{types.d.ts → dist/types.d.ts} +0 -0
- /package/{ui → dist/ui}/choice-switch.d.ts +0 -0
- /package/{ui → dist/ui}/controls.d.ts +0 -0
- /package/{ui → dist/ui}/field-context.d.ts +0 -0
- /package/{ui → dist/ui}/mode-bar.d.ts +0 -0
- /package/{ui → dist/ui}/path-crumbs.d.ts +0 -0
- /package/{ui → dist/ui}/presence.d.ts +0 -0
- /package/{ui → dist/ui}/select.d.ts +0 -0
- /package/{ui → dist/ui}/shortcut-hint.d.ts +0 -0
- /package/{ui → dist/ui}/surfaces.d.ts +0 -0
- /package/{ui → dist/ui}/variants.d.ts +0 -0
|
@@ -0,0 +1,4011 @@
|
|
|
1
|
+
//#region src/library/entries/navigation.entries.ts
|
|
2
|
+
var pic = (seed, alt, w = 1600, h = 1e3) => ({
|
|
3
|
+
url: `https://picsum.photos/seed/${seed}/${w}/${h}`,
|
|
4
|
+
alt,
|
|
5
|
+
name: `${seed}.jpg`,
|
|
6
|
+
width: w,
|
|
7
|
+
height: h
|
|
8
|
+
});
|
|
9
|
+
var page = (href, title) => ({
|
|
10
|
+
type: "page",
|
|
11
|
+
href,
|
|
12
|
+
title
|
|
13
|
+
});
|
|
14
|
+
var btn = (label, href, style = "primary", extra = {}) => ({
|
|
15
|
+
label,
|
|
16
|
+
href,
|
|
17
|
+
type: href.startsWith("http") ? "url" : "page",
|
|
18
|
+
style,
|
|
19
|
+
...extra
|
|
20
|
+
});
|
|
21
|
+
var logoFields = [{
|
|
22
|
+
name: "logo",
|
|
23
|
+
title: "Logo",
|
|
24
|
+
type: "image",
|
|
25
|
+
fieldset: "brand",
|
|
26
|
+
description: "SVG, or a PNG at least 64px tall."
|
|
27
|
+
}, {
|
|
28
|
+
name: "logoText",
|
|
29
|
+
title: "Logo text",
|
|
30
|
+
type: "string",
|
|
31
|
+
fieldset: "brand",
|
|
32
|
+
validation: {
|
|
33
|
+
required: true,
|
|
34
|
+
max: 40
|
|
35
|
+
},
|
|
36
|
+
description: "Read by screen readers. Shown if the logo can't load."
|
|
37
|
+
}];
|
|
38
|
+
/** A plain top-level link: label + link, nothing else. Two fields → opens inline. */
|
|
39
|
+
var navLink = (maxLabel = 24) => ({
|
|
40
|
+
name: "link",
|
|
41
|
+
title: "Link",
|
|
42
|
+
type: "object",
|
|
43
|
+
icon: "link",
|
|
44
|
+
openMode: "inline",
|
|
45
|
+
preview: {
|
|
46
|
+
title: "label",
|
|
47
|
+
subtitle: "link.href",
|
|
48
|
+
fallback: "Untitled link"
|
|
49
|
+
},
|
|
50
|
+
fields: [{
|
|
51
|
+
name: "label",
|
|
52
|
+
title: "Label",
|
|
53
|
+
type: "string",
|
|
54
|
+
validation: {
|
|
55
|
+
required: true,
|
|
56
|
+
max: maxLabel
|
|
57
|
+
}
|
|
58
|
+
}, {
|
|
59
|
+
name: "link",
|
|
60
|
+
title: "Links to",
|
|
61
|
+
type: "link",
|
|
62
|
+
validation: { required: true }
|
|
63
|
+
}]
|
|
64
|
+
});
|
|
65
|
+
var socialItem = {
|
|
66
|
+
name: "socialLink",
|
|
67
|
+
title: "Social profile",
|
|
68
|
+
type: "object",
|
|
69
|
+
icon: "share",
|
|
70
|
+
openMode: "inline",
|
|
71
|
+
preview: {
|
|
72
|
+
title: "platform",
|
|
73
|
+
subtitle: "url",
|
|
74
|
+
fallback: "Untitled profile"
|
|
75
|
+
},
|
|
76
|
+
fields: [{
|
|
77
|
+
name: "platform",
|
|
78
|
+
title: "Platform",
|
|
79
|
+
type: "select",
|
|
80
|
+
validation: { required: true },
|
|
81
|
+
options: [
|
|
82
|
+
{
|
|
83
|
+
value: "instagram",
|
|
84
|
+
label: "Instagram",
|
|
85
|
+
icon: "brand-instagram"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
value: "youtube",
|
|
89
|
+
label: "YouTube",
|
|
90
|
+
icon: "brand-youtube"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
value: "tiktok",
|
|
94
|
+
label: "TikTok",
|
|
95
|
+
icon: "brand-tiktok"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
value: "facebook",
|
|
99
|
+
label: "Facebook",
|
|
100
|
+
icon: "brand-facebook"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
value: "x",
|
|
104
|
+
label: "X",
|
|
105
|
+
icon: "brand-x"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
value: "linkedin",
|
|
109
|
+
label: "LinkedIn",
|
|
110
|
+
icon: "brand-linkedin"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
value: "pinterest",
|
|
114
|
+
label: "Pinterest",
|
|
115
|
+
icon: "brand-pinterest"
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
value: "dribbble",
|
|
119
|
+
label: "Dribbble",
|
|
120
|
+
icon: "brand-dribbble"
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
}, {
|
|
124
|
+
name: "url",
|
|
125
|
+
title: "Profile address",
|
|
126
|
+
type: "url",
|
|
127
|
+
validation: {
|
|
128
|
+
required: true,
|
|
129
|
+
pattern: "^https://",
|
|
130
|
+
patternMessage: "Use the full address, like https://your-site.com."
|
|
131
|
+
}
|
|
132
|
+
}]
|
|
133
|
+
};
|
|
134
|
+
var linkGroup = (maxLinks = 8) => ({
|
|
135
|
+
name: "linkGroup",
|
|
136
|
+
title: "Link group",
|
|
137
|
+
type: "object",
|
|
138
|
+
icon: "list",
|
|
139
|
+
preview: {
|
|
140
|
+
title: "title",
|
|
141
|
+
fallback: "Untitled group"
|
|
142
|
+
},
|
|
143
|
+
fields: [{
|
|
144
|
+
name: "title",
|
|
145
|
+
title: "Group title",
|
|
146
|
+
type: "string",
|
|
147
|
+
validation: {
|
|
148
|
+
required: true,
|
|
149
|
+
max: 24
|
|
150
|
+
}
|
|
151
|
+
}, {
|
|
152
|
+
name: "links",
|
|
153
|
+
title: "Links",
|
|
154
|
+
type: "array",
|
|
155
|
+
itemNoun: "link",
|
|
156
|
+
validation: {
|
|
157
|
+
min: 1,
|
|
158
|
+
max: maxLinks
|
|
159
|
+
},
|
|
160
|
+
of: [navLink(32)]
|
|
161
|
+
}]
|
|
162
|
+
});
|
|
163
|
+
var themeField = {
|
|
164
|
+
name: "theme",
|
|
165
|
+
title: "Theme",
|
|
166
|
+
type: "select",
|
|
167
|
+
group: "design",
|
|
168
|
+
layout: "segmented",
|
|
169
|
+
initialValue: "light",
|
|
170
|
+
options: [
|
|
171
|
+
{
|
|
172
|
+
value: "light",
|
|
173
|
+
label: "Light"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
value: "dark",
|
|
177
|
+
label: "Dark"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
value: "brand",
|
|
181
|
+
label: "Brand"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
};
|
|
185
|
+
var DESIGN_GROUPS = [{
|
|
186
|
+
name: "content",
|
|
187
|
+
title: "Content",
|
|
188
|
+
default: true
|
|
189
|
+
}, {
|
|
190
|
+
name: "design",
|
|
191
|
+
title: "Design"
|
|
192
|
+
}];
|
|
193
|
+
var ENTRIES = [
|
|
194
|
+
{
|
|
195
|
+
id: "nav-simple-cta",
|
|
196
|
+
previewLayout: "nav",
|
|
197
|
+
label: "Simple navbar with CTA",
|
|
198
|
+
proves: "Smallest global header · inline link rows · one button unit · tap-to-call link · design kept out of content",
|
|
199
|
+
category: "Navigation & chrome",
|
|
200
|
+
tags: [
|
|
201
|
+
"global-synced",
|
|
202
|
+
"repeater",
|
|
203
|
+
"validation",
|
|
204
|
+
"variants"
|
|
205
|
+
],
|
|
206
|
+
brand: "Hearth & Ash",
|
|
207
|
+
syncedFrom: "Header",
|
|
208
|
+
schema: {
|
|
209
|
+
name: "header",
|
|
210
|
+
title: "Header",
|
|
211
|
+
kind: "global",
|
|
212
|
+
icon: "layout-navbar",
|
|
213
|
+
category: "Navigation & chrome",
|
|
214
|
+
description: "Logo, a few links and one main button. Synced across every page.",
|
|
215
|
+
variants: [{
|
|
216
|
+
id: "logo-left",
|
|
217
|
+
name: "Logo left",
|
|
218
|
+
description: "Logo left, links and button right"
|
|
219
|
+
}, {
|
|
220
|
+
id: "logo-center",
|
|
221
|
+
name: "Centered logo",
|
|
222
|
+
description: "Links split either side of the logo"
|
|
223
|
+
}],
|
|
224
|
+
preview: {
|
|
225
|
+
title: "logoText",
|
|
226
|
+
media: "logo"
|
|
227
|
+
},
|
|
228
|
+
groups: DESIGN_GROUPS,
|
|
229
|
+
fieldsets: [{
|
|
230
|
+
name: "brand",
|
|
231
|
+
title: "Brand"
|
|
232
|
+
}],
|
|
233
|
+
fields: [
|
|
234
|
+
...logoFields,
|
|
235
|
+
{
|
|
236
|
+
name: "links",
|
|
237
|
+
title: "Links",
|
|
238
|
+
type: "array",
|
|
239
|
+
itemNoun: "link",
|
|
240
|
+
validation: {
|
|
241
|
+
min: 1,
|
|
242
|
+
max: 6
|
|
243
|
+
},
|
|
244
|
+
of: [navLink()]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "phone",
|
|
248
|
+
title: "Phone",
|
|
249
|
+
type: "link",
|
|
250
|
+
description: "Leave empty to hide it."
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "button",
|
|
254
|
+
title: "Button",
|
|
255
|
+
type: "button"
|
|
256
|
+
},
|
|
257
|
+
themeField,
|
|
258
|
+
{
|
|
259
|
+
name: "isSticky",
|
|
260
|
+
title: "Stick to the top",
|
|
261
|
+
type: "boolean",
|
|
262
|
+
group: "design",
|
|
263
|
+
initialValue: true
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: "isTransparent",
|
|
267
|
+
title: "Transparent over heroes",
|
|
268
|
+
type: "boolean",
|
|
269
|
+
group: "design"
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: "mobileMenu",
|
|
273
|
+
title: "Mobile menu",
|
|
274
|
+
type: "select",
|
|
275
|
+
group: "design",
|
|
276
|
+
layout: "radio",
|
|
277
|
+
initialValue: "drawer",
|
|
278
|
+
options: [{
|
|
279
|
+
value: "drawer",
|
|
280
|
+
label: "Drawer",
|
|
281
|
+
description: "Slides in from the right"
|
|
282
|
+
}, {
|
|
283
|
+
value: "fullscreen",
|
|
284
|
+
label: "Full screen",
|
|
285
|
+
description: "Covers the page, big type"
|
|
286
|
+
}]
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
value: {
|
|
291
|
+
_variant: "logo-left",
|
|
292
|
+
logo: pic("hearth-ash-mark", "Hearth & Ash", 240, 64),
|
|
293
|
+
logoText: "Hearth & Ash",
|
|
294
|
+
links: [
|
|
295
|
+
{
|
|
296
|
+
_key: "l1",
|
|
297
|
+
label: "Menus",
|
|
298
|
+
link: page("/menus", "Menus")
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
_key: "l2",
|
|
302
|
+
label: "Private dining",
|
|
303
|
+
link: page("/private-dining", "Private dining")
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
_key: "l3",
|
|
307
|
+
label: "Gift cards",
|
|
308
|
+
link: page("/gift-cards", "Gift cards")
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
_key: "l4",
|
|
312
|
+
label: "Our story",
|
|
313
|
+
link: page("/story", "Our story")
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
phone: {
|
|
317
|
+
type: "phone",
|
|
318
|
+
href: "tel:+15035550142",
|
|
319
|
+
phone: "+1 503 555 0142",
|
|
320
|
+
title: "(503) 555-0142"
|
|
321
|
+
},
|
|
322
|
+
button: btn("Book a table", "https://resy.com/cities/pdx/hearth-and-ash", "primary", {
|
|
323
|
+
newTab: true,
|
|
324
|
+
rel: "follow"
|
|
325
|
+
}),
|
|
326
|
+
theme: "dark",
|
|
327
|
+
isSticky: true,
|
|
328
|
+
isTransparent: true,
|
|
329
|
+
mobileMenu: "fullscreen"
|
|
330
|
+
},
|
|
331
|
+
notes: {
|
|
332
|
+
why: "A header is site chrome, so it is ONE global (kind: global) that every page renders — never a block copied onto each page, or a menu change becomes 40 edits. Links are an array of tiny objects (label + link) that open inline because two fields don't deserve a drill-in. The CTA is a `button` (label + link + style as one unit) so the label can never drift from where it goes. Phone is a `link` of type phone, not a string, so the renderer gets a valid tel: href. Theme, sticky, transparency and the mobile menu style live in Design: they change how it looks, not what it says.",
|
|
333
|
+
sanity: "Singleton document `siteHeader` { logo: image, logoText: string, links: array<object navLink { label: string, link: object { internal: reference<page>, external: url } }>, phone: string, cta: object button { label, link }, theme: string(list) } surfaced through a Structure singleton.",
|
|
334
|
+
htmlSignals: [
|
|
335
|
+
"<header> (or role=banner) as the first child of <body>, containing <nav> with a short <ul> of <a>",
|
|
336
|
+
"A logo <a href=\"/\"> wrapping an <img>/<svg> at the start of the row",
|
|
337
|
+
"One visually distinct <a>/<button> at the end of the row (filled background) — the CTA",
|
|
338
|
+
"An <a href=\"tel:…\"> or phone icon; a hamburger <button aria-expanded> hidden on desktop",
|
|
339
|
+
"position: sticky/fixed on the header; identical markup on every crawled page"
|
|
340
|
+
],
|
|
341
|
+
mcp: "Detect on 2+ pages with identical markup → create ONE global `header` and reference it from layouts; never per page. Map logo <img> → logo (alt → logoText if no visible text), each top-level <nav> <a> → links[] { label: textContent, link: internal page if same origin else url }. The last styled anchor/button → button (style from its class: filled → primary, bordered → outline). tel: anchors → phone. position sticky/fixed → isSticky: true; a transparent background over a hero → isTransparent: true. Ignore the hamburger markup — it's the renderer's job.",
|
|
342
|
+
pitfalls: ["A 'Book a table' widget that opens a third-party modal is still a button with an external link — don't model the widget's script.", "Don't create a link for the logo; it always goes home."]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "nav-announcement-utility",
|
|
347
|
+
previewLayout: "nav",
|
|
348
|
+
label: "Header with announcement + utility row",
|
|
349
|
+
proves: "Three stacked rows in one global · conditional row content · tab groups per row · design values split from content",
|
|
350
|
+
category: "Navigation & chrome",
|
|
351
|
+
tags: [
|
|
352
|
+
"global-synced",
|
|
353
|
+
"conditional",
|
|
354
|
+
"repeater",
|
|
355
|
+
"localized"
|
|
356
|
+
],
|
|
357
|
+
brand: "Caldera",
|
|
358
|
+
syncedFrom: "Header",
|
|
359
|
+
schema: {
|
|
360
|
+
name: "utilityHeader",
|
|
361
|
+
title: "Utility header",
|
|
362
|
+
kind: "global",
|
|
363
|
+
icon: "layout-navbar",
|
|
364
|
+
category: "Navigation & chrome",
|
|
365
|
+
description: "An announcement row, a utility row and the main menu. Synced across every page.",
|
|
366
|
+
preview: {
|
|
367
|
+
title: "logoText",
|
|
368
|
+
subtitle: "announcement.message",
|
|
369
|
+
media: "logo"
|
|
370
|
+
},
|
|
371
|
+
groups: [
|
|
372
|
+
{
|
|
373
|
+
name: "content",
|
|
374
|
+
title: "Main row",
|
|
375
|
+
default: true
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
name: "rows",
|
|
379
|
+
title: "Top rows"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "design",
|
|
383
|
+
title: "Design"
|
|
384
|
+
}
|
|
385
|
+
],
|
|
386
|
+
fieldsets: [{
|
|
387
|
+
name: "brand",
|
|
388
|
+
title: "Brand"
|
|
389
|
+
}],
|
|
390
|
+
fields: [
|
|
391
|
+
...logoFields,
|
|
392
|
+
{
|
|
393
|
+
name: "items",
|
|
394
|
+
title: "Menu items",
|
|
395
|
+
type: "array",
|
|
396
|
+
itemNoun: "menu item",
|
|
397
|
+
validation: {
|
|
398
|
+
min: 1,
|
|
399
|
+
max: 7
|
|
400
|
+
},
|
|
401
|
+
of: [navLink(20)]
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "button",
|
|
405
|
+
title: "Button",
|
|
406
|
+
type: "button"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "announcement",
|
|
410
|
+
title: "Announcement row",
|
|
411
|
+
type: "object",
|
|
412
|
+
group: "rows",
|
|
413
|
+
icon: "speakerphone",
|
|
414
|
+
openMode: "drill",
|
|
415
|
+
fields: [
|
|
416
|
+
{
|
|
417
|
+
name: "showAnnouncement",
|
|
418
|
+
title: "Show the announcement",
|
|
419
|
+
type: "boolean",
|
|
420
|
+
initialValue: true
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: "message",
|
|
424
|
+
title: "Message",
|
|
425
|
+
type: "string",
|
|
426
|
+
hidden: {
|
|
427
|
+
field: "showAnnouncement",
|
|
428
|
+
equals: true
|
|
429
|
+
},
|
|
430
|
+
validation: {
|
|
431
|
+
required: true,
|
|
432
|
+
max: 90,
|
|
433
|
+
recommendedMax: 70
|
|
434
|
+
},
|
|
435
|
+
localized: true,
|
|
436
|
+
ai: true
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "link",
|
|
440
|
+
title: "Links to",
|
|
441
|
+
type: "link",
|
|
442
|
+
hidden: {
|
|
443
|
+
field: "showAnnouncement",
|
|
444
|
+
equals: true
|
|
445
|
+
},
|
|
446
|
+
description: "Leave empty for plain text."
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "utilityLinks",
|
|
452
|
+
title: "Utility links",
|
|
453
|
+
type: "array",
|
|
454
|
+
group: "rows",
|
|
455
|
+
itemNoun: "utility link",
|
|
456
|
+
validation: { max: 4 },
|
|
457
|
+
description: "Hidden on phones.",
|
|
458
|
+
of: [{
|
|
459
|
+
name: "utilityLink",
|
|
460
|
+
title: "Utility link",
|
|
461
|
+
type: "object",
|
|
462
|
+
icon: "link",
|
|
463
|
+
openMode: "inline",
|
|
464
|
+
preview: {
|
|
465
|
+
title: "label",
|
|
466
|
+
subtitle: "link.href",
|
|
467
|
+
fallback: "Untitled link"
|
|
468
|
+
},
|
|
469
|
+
fields: [
|
|
470
|
+
{
|
|
471
|
+
name: "icon",
|
|
472
|
+
title: "Icon",
|
|
473
|
+
type: "icon"
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: "label",
|
|
477
|
+
title: "Label",
|
|
478
|
+
type: "string",
|
|
479
|
+
validation: {
|
|
480
|
+
required: true,
|
|
481
|
+
max: 20
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "link",
|
|
486
|
+
title: "Links to",
|
|
487
|
+
type: "link",
|
|
488
|
+
validation: { required: true }
|
|
489
|
+
}
|
|
490
|
+
]
|
|
491
|
+
}]
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "announcementTone",
|
|
495
|
+
title: "Announcement color",
|
|
496
|
+
type: "select",
|
|
497
|
+
group: "design",
|
|
498
|
+
layout: "segmented",
|
|
499
|
+
initialValue: "brand",
|
|
500
|
+
options: [
|
|
501
|
+
{
|
|
502
|
+
value: "neutral",
|
|
503
|
+
label: "Neutral"
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
value: "brand",
|
|
507
|
+
label: "Brand"
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
value: "sale",
|
|
511
|
+
label: "Sale"
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "stick",
|
|
517
|
+
title: "What stays on scroll",
|
|
518
|
+
type: "select",
|
|
519
|
+
group: "design",
|
|
520
|
+
layout: "radio",
|
|
521
|
+
initialValue: "main",
|
|
522
|
+
options: [
|
|
523
|
+
{
|
|
524
|
+
value: "none",
|
|
525
|
+
label: "Nothing",
|
|
526
|
+
description: "The whole header scrolls away"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
value: "main",
|
|
530
|
+
label: "Main row only",
|
|
531
|
+
description: "Top rows scroll away, the menu stays"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
value: "all",
|
|
535
|
+
label: "All rows"
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
]
|
|
540
|
+
},
|
|
541
|
+
value: {
|
|
542
|
+
logo: pic("caldera-mark", "Caldera", 240, 64),
|
|
543
|
+
logoText: "Caldera",
|
|
544
|
+
items: [
|
|
545
|
+
{
|
|
546
|
+
_key: "i1",
|
|
547
|
+
label: "Shop",
|
|
548
|
+
link: page("/products", "Products")
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
_key: "i2",
|
|
552
|
+
label: "Bundles",
|
|
553
|
+
link: page("/shop/bundles", "Bundles")
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
_key: "i3",
|
|
557
|
+
label: "Recipes",
|
|
558
|
+
link: page("/recipes", "Recipes")
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
_key: "i4",
|
|
562
|
+
label: "Journal",
|
|
563
|
+
link: page("/journal", "Journal")
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
_key: "i5",
|
|
567
|
+
label: "About",
|
|
568
|
+
link: page("/about", "About")
|
|
569
|
+
}
|
|
570
|
+
],
|
|
571
|
+
button: btn("Shop the Fold", "/products/cable-fold"),
|
|
572
|
+
announcement: {
|
|
573
|
+
showAnnouncement: true,
|
|
574
|
+
message: "Free shipping over $80 · Lifetime warranty on all cast iron",
|
|
575
|
+
link: page("/shipping", "Shipping & returns")
|
|
576
|
+
},
|
|
577
|
+
utilityLinks: [
|
|
578
|
+
{
|
|
579
|
+
_key: "u1",
|
|
580
|
+
icon: "truck",
|
|
581
|
+
label: "Track an order",
|
|
582
|
+
link: page("/orders/track", "Track an order")
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
_key: "u2",
|
|
586
|
+
icon: "help-circle",
|
|
587
|
+
label: "Help centre",
|
|
588
|
+
link: page("/help", "Help centre")
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
_key: "u3",
|
|
592
|
+
icon: "map-pin",
|
|
593
|
+
label: "Find a retailer",
|
|
594
|
+
link: page("/retailers", "Retailers")
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
_key: "u4",
|
|
598
|
+
icon: "building-store",
|
|
599
|
+
label: "Wholesale",
|
|
600
|
+
link: page("/wholesale", "Wholesale pricing")
|
|
601
|
+
}
|
|
602
|
+
],
|
|
603
|
+
announcementTone: "brand",
|
|
604
|
+
stick: "main"
|
|
605
|
+
},
|
|
606
|
+
notes: {
|
|
607
|
+
why: "Three visual rows, ONE global: the rows always ship together and share a sticky behavior, so splitting them into three globals would make 'what stays on scroll' impossible to express. Tab groups mirror the rows (Main row / Top rows / Design) so a marketer changing the shipping message never sees the menu. The announcement is an object with an `showAnnouncement` switch and its fields conditional on it — turning the strip off keeps the copy for next time. Its color is a design choice and lives in Design, not inside the announcement object.",
|
|
608
|
+
sanity: "Singleton `siteHeader` with `groups: [main, rows, design]`; `announcement: object { showAnnouncement: boolean, message: string, link }` with `hidden: ({parent}) => !parent?.showAnnouncement`; `utilityLinks: array<object { icon: string, label, link }>`.",
|
|
609
|
+
htmlSignals: [
|
|
610
|
+
"Two or three stacked rows inside <header>: a full-width colored strip with one sentence, then a row of small links, then the main nav",
|
|
611
|
+
"The strip text contains shipping/discount wording ('Free shipping', '% off', 'Use code')",
|
|
612
|
+
"Utility row uses smaller type and icons (truck, help, map pin) and is display:none under ~768px",
|
|
613
|
+
"Only the bottom row has position: sticky"
|
|
614
|
+
],
|
|
615
|
+
mcp: "Treat the stacked rows as ONE global. Colored single-sentence strip → announcement { showAnnouncement: true, message, link: the strip's <a> if any }; its background color → announcementTone (red/orange → sale, brand color → brand, gray → neutral). Small links above the main nav → utilityLinks (keep the icon name if an icon library class is present). Main <nav> anchors → items. If the strip is a separate dismissible element with its own close button and a countdown, use `announcement-countdown` instead and leave this one's announcement disabled.",
|
|
616
|
+
pitfalls: ["A rotating announcement (several messages cycling) is an array, not one string — switch `message` to an array of {message, link} rather than joining with ·.", "Don't put the account/cart icons in utility links — those are app state; see `nav-ecommerce-header`."]
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
id: "nav-mega-featured-promo",
|
|
621
|
+
previewLayout: "nav",
|
|
622
|
+
label: "Mega menu with featured cards + promo slot",
|
|
623
|
+
proves: "Six levels deep · product references inside a menu · a marketer-owned promo SLOT next to structured columns",
|
|
624
|
+
category: "Navigation & chrome",
|
|
625
|
+
tags: [
|
|
626
|
+
"deep-nesting",
|
|
627
|
+
"slots",
|
|
628
|
+
"global-synced",
|
|
629
|
+
"conditional",
|
|
630
|
+
"references",
|
|
631
|
+
"variants",
|
|
632
|
+
"commerce"
|
|
633
|
+
],
|
|
634
|
+
brand: "Caldera",
|
|
635
|
+
syncedFrom: "Navigation",
|
|
636
|
+
schema: {
|
|
637
|
+
name: "megaMenuHeader",
|
|
638
|
+
title: "Mega menu header",
|
|
639
|
+
kind: "global",
|
|
640
|
+
icon: "layout-navbar",
|
|
641
|
+
category: "Navigation & chrome",
|
|
642
|
+
description: "Menu items that open wide panels with link columns, product cards and a promo.",
|
|
643
|
+
variants: [{
|
|
644
|
+
id: "full",
|
|
645
|
+
name: "Full-width panel",
|
|
646
|
+
description: "Panel spans the whole window"
|
|
647
|
+
}, {
|
|
648
|
+
id: "card",
|
|
649
|
+
name: "Dropdown card",
|
|
650
|
+
description: "Panel is a card under the item"
|
|
651
|
+
}],
|
|
652
|
+
preview: {
|
|
653
|
+
title: "logoText",
|
|
654
|
+
media: "logo"
|
|
655
|
+
},
|
|
656
|
+
groups: DESIGN_GROUPS,
|
|
657
|
+
fieldsets: [{
|
|
658
|
+
name: "brand",
|
|
659
|
+
title: "Brand"
|
|
660
|
+
}],
|
|
661
|
+
fields: [
|
|
662
|
+
...logoFields,
|
|
663
|
+
{
|
|
664
|
+
name: "items",
|
|
665
|
+
title: "Menu items",
|
|
666
|
+
type: "array",
|
|
667
|
+
itemNoun: "menu item",
|
|
668
|
+
validation: {
|
|
669
|
+
min: 1,
|
|
670
|
+
max: 6
|
|
671
|
+
},
|
|
672
|
+
of: [{
|
|
673
|
+
name: "menuItem",
|
|
674
|
+
title: "Menu item",
|
|
675
|
+
type: "object",
|
|
676
|
+
icon: "menu",
|
|
677
|
+
preview: {
|
|
678
|
+
title: "label",
|
|
679
|
+
subtitle: "opens",
|
|
680
|
+
fallback: "Untitled item"
|
|
681
|
+
},
|
|
682
|
+
fields: [
|
|
683
|
+
{
|
|
684
|
+
name: "label",
|
|
685
|
+
title: "Label",
|
|
686
|
+
type: "string",
|
|
687
|
+
validation: {
|
|
688
|
+
required: true,
|
|
689
|
+
max: 20
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "opens",
|
|
694
|
+
title: "Opens",
|
|
695
|
+
type: "select",
|
|
696
|
+
layout: "segmented",
|
|
697
|
+
initialValue: "link",
|
|
698
|
+
options: [{
|
|
699
|
+
value: "link",
|
|
700
|
+
label: "A page"
|
|
701
|
+
}, {
|
|
702
|
+
value: "panel",
|
|
703
|
+
label: "A panel"
|
|
704
|
+
}]
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
name: "link",
|
|
708
|
+
title: "Links to",
|
|
709
|
+
type: "link",
|
|
710
|
+
hidden: {
|
|
711
|
+
field: "opens",
|
|
712
|
+
equals: "link"
|
|
713
|
+
},
|
|
714
|
+
validation: { required: true }
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: "panel",
|
|
718
|
+
title: "Panel",
|
|
719
|
+
type: "object",
|
|
720
|
+
icon: "layout-board",
|
|
721
|
+
openMode: "drill",
|
|
722
|
+
hidden: {
|
|
723
|
+
field: "opens",
|
|
724
|
+
equals: "panel"
|
|
725
|
+
},
|
|
726
|
+
fields: [
|
|
727
|
+
{
|
|
728
|
+
name: "columns",
|
|
729
|
+
title: "Link columns",
|
|
730
|
+
type: "array",
|
|
731
|
+
itemNoun: "column",
|
|
732
|
+
validation: {
|
|
733
|
+
min: 1,
|
|
734
|
+
max: 4
|
|
735
|
+
},
|
|
736
|
+
of: [{
|
|
737
|
+
name: "column",
|
|
738
|
+
title: "Column",
|
|
739
|
+
type: "object",
|
|
740
|
+
icon: "columns-3",
|
|
741
|
+
preview: {
|
|
742
|
+
title: "title",
|
|
743
|
+
fallback: "Untitled column"
|
|
744
|
+
},
|
|
745
|
+
fields: [
|
|
746
|
+
{
|
|
747
|
+
name: "title",
|
|
748
|
+
title: "Column title",
|
|
749
|
+
type: "string",
|
|
750
|
+
validation: {
|
|
751
|
+
required: true,
|
|
752
|
+
max: 24
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: "viewAll",
|
|
757
|
+
title: "View all link",
|
|
758
|
+
type: "link"
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "links",
|
|
762
|
+
title: "Links",
|
|
763
|
+
type: "array",
|
|
764
|
+
itemNoun: "link",
|
|
765
|
+
validation: { max: 8 },
|
|
766
|
+
of: [{
|
|
767
|
+
name: "panelLink",
|
|
768
|
+
title: "Link",
|
|
769
|
+
type: "object",
|
|
770
|
+
icon: "link",
|
|
771
|
+
preview: {
|
|
772
|
+
title: "label",
|
|
773
|
+
subtitle: "description",
|
|
774
|
+
fallback: "Untitled link"
|
|
775
|
+
},
|
|
776
|
+
fields: [
|
|
777
|
+
{
|
|
778
|
+
name: "label",
|
|
779
|
+
title: "Label",
|
|
780
|
+
type: "string",
|
|
781
|
+
validation: {
|
|
782
|
+
required: true,
|
|
783
|
+
max: 32
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "link",
|
|
788
|
+
title: "Links to",
|
|
789
|
+
type: "link",
|
|
790
|
+
validation: { required: true }
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
name: "description",
|
|
794
|
+
title: "Description",
|
|
795
|
+
type: "string",
|
|
796
|
+
validation: { recommendedMax: 60 }
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "badge",
|
|
800
|
+
title: "Badge",
|
|
801
|
+
type: "object",
|
|
802
|
+
icon: "badge",
|
|
803
|
+
openMode: "popover",
|
|
804
|
+
fields: [
|
|
805
|
+
{
|
|
806
|
+
name: "label",
|
|
807
|
+
title: "Label",
|
|
808
|
+
type: "string",
|
|
809
|
+
validation: { max: 12 }
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
name: "tone",
|
|
813
|
+
title: "Color",
|
|
814
|
+
type: "select",
|
|
815
|
+
layout: "segmented",
|
|
816
|
+
options: [
|
|
817
|
+
{
|
|
818
|
+
value: "brand",
|
|
819
|
+
label: "Brand"
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
value: "sale",
|
|
823
|
+
label: "Sale"
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
value: "neutral",
|
|
827
|
+
label: "Neutral"
|
|
828
|
+
}
|
|
829
|
+
]
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
name: "hideAfter",
|
|
833
|
+
title: "Hide after",
|
|
834
|
+
type: "datetime"
|
|
835
|
+
}
|
|
836
|
+
]
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
}]
|
|
840
|
+
}
|
|
841
|
+
]
|
|
842
|
+
}]
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: "featuredCards",
|
|
846
|
+
title: "Featured cards",
|
|
847
|
+
type: "array",
|
|
848
|
+
itemNoun: "card",
|
|
849
|
+
validation: { max: 2 },
|
|
850
|
+
of: [{
|
|
851
|
+
name: "featuredCard",
|
|
852
|
+
title: "Card",
|
|
853
|
+
type: "object",
|
|
854
|
+
icon: "layout-cards",
|
|
855
|
+
preview: {
|
|
856
|
+
title: "title",
|
|
857
|
+
subtitle: "source",
|
|
858
|
+
media: "image",
|
|
859
|
+
fallback: "Untitled product card"
|
|
860
|
+
},
|
|
861
|
+
fields: [
|
|
862
|
+
{
|
|
863
|
+
name: "source",
|
|
864
|
+
title: "Shows",
|
|
865
|
+
type: "select",
|
|
866
|
+
layout: "segmented",
|
|
867
|
+
initialValue: "product",
|
|
868
|
+
options: [{
|
|
869
|
+
value: "product",
|
|
870
|
+
label: "A product"
|
|
871
|
+
}, {
|
|
872
|
+
value: "custom",
|
|
873
|
+
label: "Custom card"
|
|
874
|
+
}]
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "product",
|
|
878
|
+
title: "Product",
|
|
879
|
+
type: "reference",
|
|
880
|
+
reference: { to: ["shopify"] },
|
|
881
|
+
hidden: {
|
|
882
|
+
field: "source",
|
|
883
|
+
equals: "product"
|
|
884
|
+
},
|
|
885
|
+
validation: { required: true }
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: "image",
|
|
889
|
+
title: "Image",
|
|
890
|
+
type: "image",
|
|
891
|
+
hidden: {
|
|
892
|
+
field: "source",
|
|
893
|
+
equals: "custom"
|
|
894
|
+
},
|
|
895
|
+
validation: { required: true }
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: "title",
|
|
899
|
+
title: "Title",
|
|
900
|
+
type: "string",
|
|
901
|
+
hidden: {
|
|
902
|
+
field: "source",
|
|
903
|
+
equals: "custom"
|
|
904
|
+
},
|
|
905
|
+
validation: {
|
|
906
|
+
required: true,
|
|
907
|
+
recommendedMax: 40
|
|
908
|
+
},
|
|
909
|
+
ai: true
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "button",
|
|
913
|
+
title: "Button",
|
|
914
|
+
type: "button",
|
|
915
|
+
hidden: {
|
|
916
|
+
field: "source",
|
|
917
|
+
equals: "custom"
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
]
|
|
921
|
+
}]
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: "promo",
|
|
925
|
+
title: "Promo",
|
|
926
|
+
type: "slot",
|
|
927
|
+
validation: { max: 1 },
|
|
928
|
+
of: {
|
|
929
|
+
type: "block",
|
|
930
|
+
allow: [
|
|
931
|
+
"ctaCard",
|
|
932
|
+
"imageBlock",
|
|
933
|
+
"productCard",
|
|
934
|
+
"videoBlock"
|
|
935
|
+
]
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
]
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
}]
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "button",
|
|
945
|
+
title: "Button",
|
|
946
|
+
type: "button"
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
name: "openOn",
|
|
950
|
+
title: "Panels open on",
|
|
951
|
+
type: "select",
|
|
952
|
+
group: "design",
|
|
953
|
+
layout: "segmented",
|
|
954
|
+
initialValue: "hover",
|
|
955
|
+
options: [{
|
|
956
|
+
value: "hover",
|
|
957
|
+
label: "Hover"
|
|
958
|
+
}, {
|
|
959
|
+
value: "click",
|
|
960
|
+
label: "Click"
|
|
961
|
+
}]
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: "useBackdrop",
|
|
965
|
+
title: "Dim the page behind",
|
|
966
|
+
type: "boolean",
|
|
967
|
+
group: "design",
|
|
968
|
+
initialValue: true
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
},
|
|
972
|
+
value: {
|
|
973
|
+
_variant: "full",
|
|
974
|
+
logo: pic("caldera-mark", "Caldera", 240, 64),
|
|
975
|
+
logoText: "Caldera",
|
|
976
|
+
items: [
|
|
977
|
+
{
|
|
978
|
+
_key: "m1",
|
|
979
|
+
label: "Shop",
|
|
980
|
+
opens: "panel",
|
|
981
|
+
panel: {
|
|
982
|
+
columns: [
|
|
983
|
+
{
|
|
984
|
+
_key: "c1",
|
|
985
|
+
title: "Cookware",
|
|
986
|
+
viewAll: page("/products", "All cookware"),
|
|
987
|
+
links: [
|
|
988
|
+
{
|
|
989
|
+
_key: "a1",
|
|
990
|
+
label: "Skillets",
|
|
991
|
+
link: page("/products?type=skillet", "Skillets"),
|
|
992
|
+
description: "Cast iron and carbon steel, 8 to 14 inches"
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
_key: "a2",
|
|
996
|
+
label: "Dutch ovens",
|
|
997
|
+
link: {
|
|
998
|
+
type: "entry",
|
|
999
|
+
href: "/products/ember-dutch-oven",
|
|
1000
|
+
title: "Ember Dutch Oven 5.5qt",
|
|
1001
|
+
entryId: "en_p2"
|
|
1002
|
+
},
|
|
1003
|
+
description: "Enamelled, 3.5 to 7 quarts",
|
|
1004
|
+
badge: {
|
|
1005
|
+
label: "New",
|
|
1006
|
+
tone: "brand"
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
_key: "a3",
|
|
1011
|
+
label: "Grills",
|
|
1012
|
+
link: {
|
|
1013
|
+
type: "entry",
|
|
1014
|
+
href: "/products/cable-fold",
|
|
1015
|
+
title: "Cable Fold Grill",
|
|
1016
|
+
entryId: "en_p1"
|
|
1017
|
+
},
|
|
1018
|
+
description: "The Cable Fold and the Ridge grate"
|
|
1019
|
+
}
|
|
1020
|
+
]
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
_key: "c2",
|
|
1024
|
+
title: "Camp kitchen",
|
|
1025
|
+
links: [{
|
|
1026
|
+
_key: "a4",
|
|
1027
|
+
label: "Kettles",
|
|
1028
|
+
link: {
|
|
1029
|
+
type: "entry",
|
|
1030
|
+
href: "/products/trail-kettle",
|
|
1031
|
+
title: "Trail Kettle 1L",
|
|
1032
|
+
entryId: "en_p4"
|
|
1033
|
+
}
|
|
1034
|
+
}, {
|
|
1035
|
+
_key: "a5",
|
|
1036
|
+
label: "Bundles",
|
|
1037
|
+
link: page("/shop/bundles", "Bundles"),
|
|
1038
|
+
description: "Save 15% on sets",
|
|
1039
|
+
badge: {
|
|
1040
|
+
label: "Sale",
|
|
1041
|
+
tone: "sale",
|
|
1042
|
+
hideAfter: "2026-12-01T23:59:00-08:00[America/Los_Angeles]"
|
|
1043
|
+
}
|
|
1044
|
+
}]
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
_key: "c3",
|
|
1048
|
+
title: "Care",
|
|
1049
|
+
links: [{
|
|
1050
|
+
_key: "a6",
|
|
1051
|
+
label: "Seasoning oil",
|
|
1052
|
+
link: page("/products/seasoning-oil", "Seasoning oil")
|
|
1053
|
+
}, {
|
|
1054
|
+
_key: "a7",
|
|
1055
|
+
label: "Chainmail scrubber",
|
|
1056
|
+
link: page("/products/chainmail", "Chainmail scrubber")
|
|
1057
|
+
}]
|
|
1058
|
+
}
|
|
1059
|
+
],
|
|
1060
|
+
featuredCards: [{
|
|
1061
|
+
_key: "f1",
|
|
1062
|
+
source: "product",
|
|
1063
|
+
product: "gid://shopify/Product/8812"
|
|
1064
|
+
}, {
|
|
1065
|
+
_key: "f2",
|
|
1066
|
+
source: "custom",
|
|
1067
|
+
image: pic("caldera-gift-guide", "Cast iron set wrapped in brown paper on a cabin table", 900, 700),
|
|
1068
|
+
title: "The 2026 gift guide",
|
|
1069
|
+
button: btn("Find a gift", "/journal/gift-guide", "link", { arrow: true })
|
|
1070
|
+
}],
|
|
1071
|
+
promo: [{
|
|
1072
|
+
_key: "p1",
|
|
1073
|
+
_type: "ctaCard",
|
|
1074
|
+
_variant: "solid",
|
|
1075
|
+
heading: "Holiday sets ship free",
|
|
1076
|
+
body: "Order by December 18 for delivery before the 24th.",
|
|
1077
|
+
button: btn("Shop sets", "/shop/bundles")
|
|
1078
|
+
}]
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
_key: "m2",
|
|
1083
|
+
label: "Learn",
|
|
1084
|
+
opens: "panel",
|
|
1085
|
+
panel: {
|
|
1086
|
+
columns: [{
|
|
1087
|
+
_key: "c4",
|
|
1088
|
+
title: "Cook",
|
|
1089
|
+
links: [{
|
|
1090
|
+
_key: "b1",
|
|
1091
|
+
label: "Recipes",
|
|
1092
|
+
link: page("/recipes", "Recipes"),
|
|
1093
|
+
description: "Tested over coals and on the stove"
|
|
1094
|
+
}, {
|
|
1095
|
+
_key: "b2",
|
|
1096
|
+
label: "Cast iron care",
|
|
1097
|
+
link: page("/journal/cast-iron-care", "Cast iron care")
|
|
1098
|
+
}]
|
|
1099
|
+
}],
|
|
1100
|
+
featuredCards: [],
|
|
1101
|
+
promo: [{
|
|
1102
|
+
_key: "p2",
|
|
1103
|
+
_type: "imageBlock",
|
|
1104
|
+
_variant: "cover",
|
|
1105
|
+
image: pic("caldera-film-still", "Cook kneeling at a campfire at dusk", 900, 700),
|
|
1106
|
+
caption: "Watch: Fire Lessons, episode 4",
|
|
1107
|
+
link: page("/journal/fire-lessons-4", "Fire Lessons, episode 4")
|
|
1108
|
+
}]
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
_key: "m3",
|
|
1113
|
+
label: "About",
|
|
1114
|
+
opens: "link",
|
|
1115
|
+
link: page("/about", "About")
|
|
1116
|
+
}
|
|
1117
|
+
],
|
|
1118
|
+
button: btn("Shop the Fold", "/products/cable-fold"),
|
|
1119
|
+
openOn: "click",
|
|
1120
|
+
useBackdrop: true
|
|
1121
|
+
},
|
|
1122
|
+
notes: {
|
|
1123
|
+
why: "Same global header shape as the core Mega menu, but the panel separates what is STRUCTURED from what is CAMPAIGN. Link columns and product cards are typed fields (a converter and a validator can reason about them); the promo is a `slot` limited to one block from a short allow-list, so marketing can drop in a card, image or video for a launch without a developer adding fields. Product cards are REFERENCES to Shopify products — photo, name, price and stock flow in, so the menu never shows a stale price. `opens` switches between a plain link and a panel; the panel object only shows when needed. The badge is a popover object because it's 3 small fields edited rarely, and it carries its own expiry.",
|
|
1124
|
+
sanity: "`navigation` singleton → `items: array<object menuItem { label, opens, link, panel: object { columns: array<object { title, viewAll, links: array<object { label, link, description, badge: object }> }>, featuredCards: array<object { source, product: reference<shopify.product>, image, title, button }>, promo: array<ctaCard | imageBlock | productCard | videoBlock> (max 1) } }>` — Sanity has no slot type; it's an array of block types.",
|
|
1125
|
+
htmlSignals: [
|
|
1126
|
+
"Top-level nav <li> containing a <div> panel (often role=menu or [data-mega], display:none until hover/aria-expanded)",
|
|
1127
|
+
"Panel holds 2–4 <ul> lists each preceded by a small heading — the columns",
|
|
1128
|
+
"Image cards inside the panel with a price or 'Shop' link — product cards (look for /products/ hrefs and price text)",
|
|
1129
|
+
"A visually different tile (full-bleed image or colored card) at the panel edge — the promo",
|
|
1130
|
+
"Small pills (<span class=\"badge\">New</span>) next to some links"
|
|
1131
|
+
],
|
|
1132
|
+
mcp: "Per top-level <li>: if it has a panel → opens: 'panel', else opens: 'link'. Panel headings + their <ul> → columns[] (a trailing 'View all/Shop all' anchor → viewAll, not a link). Card with a price and a /products/<handle> href → featuredCards[] { source: 'product', product: resolve handle to the Shopify product id }; card without price → { source: 'custom', image, title, button }. The remaining odd-one-out tile → promo slot as ctaCard (text+button), imageBlock (image+caption) or videoBlock. Pills → badge.label; red/sale colors → tone: 'sale'. Never copy the price text into a field.",
|
|
1133
|
+
pitfalls: [
|
|
1134
|
+
"Product cards that hard-code price text go stale on the next price change — always resolve to a reference.",
|
|
1135
|
+
"If two menu items share the same panel content, that is still two panels in content; don't invent a shared-panel global for it.",
|
|
1136
|
+
"Hover-only panels fail keyboard users — the converter should default `openOn` to click when the source has no aria-expanded handling."
|
|
1137
|
+
]
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
id: "nav-docs-sidebar",
|
|
1142
|
+
previewLayout: "list",
|
|
1143
|
+
label: "Docs sidebar navigation",
|
|
1144
|
+
proves: "Four-level tree at a fixed depth · curated vs auto from the page tree · version switcher · status badges",
|
|
1145
|
+
category: "Navigation & chrome",
|
|
1146
|
+
tags: [
|
|
1147
|
+
"deep-nesting",
|
|
1148
|
+
"repeater",
|
|
1149
|
+
"global-synced",
|
|
1150
|
+
"conditional",
|
|
1151
|
+
"dynamic-query",
|
|
1152
|
+
"references"
|
|
1153
|
+
],
|
|
1154
|
+
brand: "Tideline",
|
|
1155
|
+
syncedFrom: "Docs sidebar",
|
|
1156
|
+
schema: {
|
|
1157
|
+
name: "docsSidebar",
|
|
1158
|
+
title: "Docs sidebar",
|
|
1159
|
+
kind: "global",
|
|
1160
|
+
icon: "layout-sidebar",
|
|
1161
|
+
category: "Navigation & chrome",
|
|
1162
|
+
description: "The left-hand tree on every docs page, with search and a version switcher.",
|
|
1163
|
+
preview: { title: "searchPlaceholder" },
|
|
1164
|
+
groups: [{
|
|
1165
|
+
name: "content",
|
|
1166
|
+
title: "Content",
|
|
1167
|
+
default: true
|
|
1168
|
+
}, {
|
|
1169
|
+
name: "behavior",
|
|
1170
|
+
title: "Behavior"
|
|
1171
|
+
}],
|
|
1172
|
+
fields: [
|
|
1173
|
+
{
|
|
1174
|
+
name: "versions",
|
|
1175
|
+
title: "Versions",
|
|
1176
|
+
type: "array",
|
|
1177
|
+
itemNoun: "version",
|
|
1178
|
+
validation: { max: 6 },
|
|
1179
|
+
description: "Leave empty to hide the switcher.",
|
|
1180
|
+
of: [{
|
|
1181
|
+
name: "version",
|
|
1182
|
+
title: "Version",
|
|
1183
|
+
type: "object",
|
|
1184
|
+
icon: "versions",
|
|
1185
|
+
openMode: "inline",
|
|
1186
|
+
preview: {
|
|
1187
|
+
title: "label",
|
|
1188
|
+
subtitle: "status",
|
|
1189
|
+
fallback: "Untitled version"
|
|
1190
|
+
},
|
|
1191
|
+
fields: [
|
|
1192
|
+
{
|
|
1193
|
+
name: "label",
|
|
1194
|
+
title: "Label",
|
|
1195
|
+
type: "string",
|
|
1196
|
+
validation: {
|
|
1197
|
+
required: true,
|
|
1198
|
+
max: 16
|
|
1199
|
+
},
|
|
1200
|
+
placeholder: "v3"
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
name: "link",
|
|
1204
|
+
title: "Docs home",
|
|
1205
|
+
type: "link",
|
|
1206
|
+
validation: { required: true }
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
name: "status",
|
|
1210
|
+
title: "Status",
|
|
1211
|
+
type: "select",
|
|
1212
|
+
options: [
|
|
1213
|
+
{
|
|
1214
|
+
value: "current",
|
|
1215
|
+
label: "Current"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
value: "beta",
|
|
1219
|
+
label: "Beta"
|
|
1220
|
+
},
|
|
1221
|
+
{
|
|
1222
|
+
value: "legacy",
|
|
1223
|
+
label: "Legacy",
|
|
1224
|
+
description: "Shows an out-of-date warning on its pages"
|
|
1225
|
+
}
|
|
1226
|
+
]
|
|
1227
|
+
}
|
|
1228
|
+
]
|
|
1229
|
+
}]
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
name: "searchPlaceholder",
|
|
1233
|
+
title: "Search placeholder",
|
|
1234
|
+
type: "string",
|
|
1235
|
+
validation: { max: 40 },
|
|
1236
|
+
placeholder: "Search docs"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
name: "source",
|
|
1240
|
+
title: "Tree comes from",
|
|
1241
|
+
type: "select",
|
|
1242
|
+
layout: "cards",
|
|
1243
|
+
initialValue: "curated",
|
|
1244
|
+
options: [{
|
|
1245
|
+
value: "curated",
|
|
1246
|
+
label: "Curated",
|
|
1247
|
+
icon: "list-tree",
|
|
1248
|
+
description: "You order sections and pages here"
|
|
1249
|
+
}, {
|
|
1250
|
+
value: "auto",
|
|
1251
|
+
label: "Page tree",
|
|
1252
|
+
icon: "folders",
|
|
1253
|
+
description: "Follows the docs folder; new pages appear on their own"
|
|
1254
|
+
}]
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "root",
|
|
1258
|
+
title: "Docs root page",
|
|
1259
|
+
type: "reference",
|
|
1260
|
+
reference: { to: ["page"] },
|
|
1261
|
+
hidden: {
|
|
1262
|
+
field: "source",
|
|
1263
|
+
equals: "auto"
|
|
1264
|
+
},
|
|
1265
|
+
validation: { required: true },
|
|
1266
|
+
description: "Every page under this one is listed, in folder order."
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
name: "sections",
|
|
1270
|
+
title: "Sections",
|
|
1271
|
+
type: "array",
|
|
1272
|
+
itemNoun: "section",
|
|
1273
|
+
hidden: {
|
|
1274
|
+
field: "source",
|
|
1275
|
+
equals: "curated"
|
|
1276
|
+
},
|
|
1277
|
+
validation: {
|
|
1278
|
+
min: 1,
|
|
1279
|
+
max: 12
|
|
1280
|
+
},
|
|
1281
|
+
of: [{
|
|
1282
|
+
name: "section",
|
|
1283
|
+
title: "Section",
|
|
1284
|
+
type: "object",
|
|
1285
|
+
icon: "folder",
|
|
1286
|
+
preview: {
|
|
1287
|
+
title: "title",
|
|
1288
|
+
fallback: "Untitled section"
|
|
1289
|
+
},
|
|
1290
|
+
fields: [
|
|
1291
|
+
{
|
|
1292
|
+
name: "title",
|
|
1293
|
+
title: "Section title",
|
|
1294
|
+
type: "string",
|
|
1295
|
+
validation: {
|
|
1296
|
+
required: true,
|
|
1297
|
+
max: 32
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
name: "icon",
|
|
1302
|
+
title: "Icon",
|
|
1303
|
+
type: "icon"
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
name: "isCollapsed",
|
|
1307
|
+
title: "Start collapsed",
|
|
1308
|
+
type: "boolean"
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
name: "pages",
|
|
1312
|
+
title: "Pages",
|
|
1313
|
+
type: "array",
|
|
1314
|
+
itemNoun: "page",
|
|
1315
|
+
validation: {
|
|
1316
|
+
min: 1,
|
|
1317
|
+
max: 30
|
|
1318
|
+
},
|
|
1319
|
+
of: [{
|
|
1320
|
+
name: "docLink",
|
|
1321
|
+
title: "Page",
|
|
1322
|
+
type: "object",
|
|
1323
|
+
icon: "file-text",
|
|
1324
|
+
preview: {
|
|
1325
|
+
title: "label",
|
|
1326
|
+
subtitle: "badge",
|
|
1327
|
+
fallback: "Untitled page"
|
|
1328
|
+
},
|
|
1329
|
+
fields: [
|
|
1330
|
+
{
|
|
1331
|
+
name: "label",
|
|
1332
|
+
title: "Label",
|
|
1333
|
+
type: "string",
|
|
1334
|
+
validation: {
|
|
1335
|
+
required: true,
|
|
1336
|
+
max: 40
|
|
1337
|
+
}
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
name: "link",
|
|
1341
|
+
title: "Links to",
|
|
1342
|
+
type: "link",
|
|
1343
|
+
validation: { required: true }
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
name: "badge",
|
|
1347
|
+
title: "Badge",
|
|
1348
|
+
type: "select",
|
|
1349
|
+
layout: "segmented",
|
|
1350
|
+
options: [
|
|
1351
|
+
{
|
|
1352
|
+
value: "",
|
|
1353
|
+
label: "None"
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
value: "new",
|
|
1357
|
+
label: "New"
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
value: "beta",
|
|
1361
|
+
label: "Beta"
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
value: "deprecated",
|
|
1365
|
+
label: "Deprecated"
|
|
1366
|
+
}
|
|
1367
|
+
]
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
name: "children",
|
|
1371
|
+
title: "Sub-pages",
|
|
1372
|
+
type: "array",
|
|
1373
|
+
itemNoun: "sub-page",
|
|
1374
|
+
validation: { max: 15 },
|
|
1375
|
+
description: "One level only.",
|
|
1376
|
+
of: [{
|
|
1377
|
+
name: "docSubLink",
|
|
1378
|
+
title: "Sub-page",
|
|
1379
|
+
type: "object",
|
|
1380
|
+
icon: "file",
|
|
1381
|
+
openMode: "inline",
|
|
1382
|
+
preview: {
|
|
1383
|
+
title: "label",
|
|
1384
|
+
fallback: "Untitled sub-page"
|
|
1385
|
+
},
|
|
1386
|
+
fields: [{
|
|
1387
|
+
name: "label",
|
|
1388
|
+
title: "Label",
|
|
1389
|
+
type: "string",
|
|
1390
|
+
validation: {
|
|
1391
|
+
required: true,
|
|
1392
|
+
max: 40
|
|
1393
|
+
}
|
|
1394
|
+
}, {
|
|
1395
|
+
name: "link",
|
|
1396
|
+
title: "Links to",
|
|
1397
|
+
type: "link",
|
|
1398
|
+
validation: { required: true }
|
|
1399
|
+
}]
|
|
1400
|
+
}]
|
|
1401
|
+
}
|
|
1402
|
+
]
|
|
1403
|
+
}]
|
|
1404
|
+
}
|
|
1405
|
+
]
|
|
1406
|
+
}]
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "footerLinks",
|
|
1410
|
+
title: "Footer links",
|
|
1411
|
+
type: "array",
|
|
1412
|
+
itemNoun: "link",
|
|
1413
|
+
validation: { max: 4 },
|
|
1414
|
+
of: [navLink(24)]
|
|
1415
|
+
},
|
|
1416
|
+
{
|
|
1417
|
+
name: "isCurrentOpen",
|
|
1418
|
+
title: "Open the current section",
|
|
1419
|
+
type: "boolean",
|
|
1420
|
+
group: "behavior",
|
|
1421
|
+
initialValue: true
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "showPrevNext",
|
|
1425
|
+
title: "Previous and next links",
|
|
1426
|
+
type: "boolean",
|
|
1427
|
+
group: "behavior",
|
|
1428
|
+
initialValue: true
|
|
1429
|
+
}
|
|
1430
|
+
]
|
|
1431
|
+
},
|
|
1432
|
+
value: {
|
|
1433
|
+
versions: [{
|
|
1434
|
+
_key: "v1",
|
|
1435
|
+
label: "v3",
|
|
1436
|
+
link: page("/docs", "Docs"),
|
|
1437
|
+
status: "current"
|
|
1438
|
+
}, {
|
|
1439
|
+
_key: "v2",
|
|
1440
|
+
label: "v2",
|
|
1441
|
+
link: page("/docs/v2", "Docs v2"),
|
|
1442
|
+
status: "legacy"
|
|
1443
|
+
}],
|
|
1444
|
+
searchPlaceholder: "Search docs ⌘K",
|
|
1445
|
+
source: "curated",
|
|
1446
|
+
sections: [{
|
|
1447
|
+
_key: "s1",
|
|
1448
|
+
title: "Get started",
|
|
1449
|
+
icon: "rocket",
|
|
1450
|
+
isCollapsed: false,
|
|
1451
|
+
pages: [{
|
|
1452
|
+
_key: "p1",
|
|
1453
|
+
label: "Quickstart",
|
|
1454
|
+
link: page("/docs/quickstart", "Quickstart"),
|
|
1455
|
+
badge: ""
|
|
1456
|
+
}, {
|
|
1457
|
+
_key: "p2",
|
|
1458
|
+
label: "Install the agent",
|
|
1459
|
+
link: page("/docs/install", "Install the agent"),
|
|
1460
|
+
badge: "",
|
|
1461
|
+
children: [
|
|
1462
|
+
{
|
|
1463
|
+
_key: "k1",
|
|
1464
|
+
label: "Kubernetes",
|
|
1465
|
+
link: page("/docs/install/kubernetes", "Kubernetes")
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
_key: "k2",
|
|
1469
|
+
label: "Docker",
|
|
1470
|
+
link: page("/docs/install/docker", "Docker")
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
_key: "k3",
|
|
1474
|
+
label: "Bare metal",
|
|
1475
|
+
link: page("/docs/install/linux", "Linux hosts")
|
|
1476
|
+
}
|
|
1477
|
+
]
|
|
1478
|
+
}]
|
|
1479
|
+
}, {
|
|
1480
|
+
_key: "s2",
|
|
1481
|
+
title: "Traces",
|
|
1482
|
+
icon: "route",
|
|
1483
|
+
isCollapsed: true,
|
|
1484
|
+
pages: [
|
|
1485
|
+
{
|
|
1486
|
+
_key: "p3",
|
|
1487
|
+
label: "Sampling rules",
|
|
1488
|
+
link: page("/docs/traces/sampling", "Sampling rules"),
|
|
1489
|
+
badge: "new"
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
_key: "p4",
|
|
1493
|
+
label: "Service map",
|
|
1494
|
+
link: page("/docs/traces/service-map", "Service map"),
|
|
1495
|
+
badge: "beta"
|
|
1496
|
+
},
|
|
1497
|
+
{
|
|
1498
|
+
_key: "p5",
|
|
1499
|
+
label: "Legacy span API",
|
|
1500
|
+
link: page("/docs/traces/span-api-v1", "Span API v1"),
|
|
1501
|
+
badge: "deprecated"
|
|
1502
|
+
}
|
|
1503
|
+
]
|
|
1504
|
+
}],
|
|
1505
|
+
footerLinks: [{
|
|
1506
|
+
_key: "f1",
|
|
1507
|
+
label: "Changelog",
|
|
1508
|
+
link: page("/changelog", "Changelog")
|
|
1509
|
+
}, {
|
|
1510
|
+
_key: "f2",
|
|
1511
|
+
label: "Status",
|
|
1512
|
+
link: {
|
|
1513
|
+
type: "url",
|
|
1514
|
+
href: "https://status.tideline.dev",
|
|
1515
|
+
newTab: true
|
|
1516
|
+
}
|
|
1517
|
+
}],
|
|
1518
|
+
isCurrentOpen: true,
|
|
1519
|
+
showPrevNext: true
|
|
1520
|
+
},
|
|
1521
|
+
notes: {
|
|
1522
|
+
why: "A docs sidebar is shared by every docs page, so it is a global. Most teams want BOTH modes: a curated order (what to read first) and an automatic tree (no one forgets to add the new page), so `source` decides which fields show. The tree is modelled to a FIXED depth — Section › Page › Sub-page — because our schema language has no recursive types; three levels is also the usability ceiling for a sidebar. Badges are a select (a fixed vocabulary the renderer can color and the search index can filter), not free text. Versions are a small inline array because each one is just a label, a link and a status.",
|
|
1523
|
+
sanity: "Singleton `docsNav` { versions: array<object>, source: string, root: reference<page>, sections: array<object { title, icon, isCollapsed, pages: array<object { label, link: reference<docPage>, badge, children: array<object> }> }> } — Sanity CAN self-reference (`children: array<docLink>`), which is how it supports unlimited depth.",
|
|
1524
|
+
htmlSignals: [
|
|
1525
|
+
"<aside>/<nav aria-label=\"Docs\"> fixed to the left with a long nested <ul> and collapsible groups (<details>/<button aria-expanded>)",
|
|
1526
|
+
"Group headings in small caps above lists of links whose hrefs share a /docs/ prefix",
|
|
1527
|
+
"A version <select> or dropdown ('v3', 'latest') at the top; a search input with a ⌘K / Ctrl K hint",
|
|
1528
|
+
"Pills like 'New', 'Beta', 'Deprecated' next to links",
|
|
1529
|
+
"The same sidebar on every /docs/* page with the current link marked aria-current=page"
|
|
1530
|
+
],
|
|
1531
|
+
mcp: "Crawl 2+ /docs pages; identical sidebars → ONE global `docsSidebar`. If the tree order matches the URL/folder order exactly and there's no hand-written grouping → source: 'auto' with root = the docs index page; otherwise source: 'curated'. Group headings → sections[].title; links under them → pages[]; nested lists → children[] (flatten anything deeper into the sub-page level and report it). Pills → badge (map unknown text to '' and report). Version dropdown options → versions[] (status 'legacy' for anything not marked latest).",
|
|
1532
|
+
pitfalls: ["Sidebars 4+ levels deep can't be represented — flatten to sub-pages and flag it rather than inventing nested objects.", "Don't import the 'On this page' table of contents — it's generated from each page's headings."]
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
id: "nav-ecommerce-header",
|
|
1537
|
+
previewLayout: "nav",
|
|
1538
|
+
label: "E-commerce header",
|
|
1539
|
+
proves: "Search, account and cart config in one global · product references for search · app state (cart count) NOT modelled",
|
|
1540
|
+
category: "Navigation & chrome",
|
|
1541
|
+
tags: [
|
|
1542
|
+
"global-synced",
|
|
1543
|
+
"commerce",
|
|
1544
|
+
"references",
|
|
1545
|
+
"repeater",
|
|
1546
|
+
"conditional",
|
|
1547
|
+
"media-heavy"
|
|
1548
|
+
],
|
|
1549
|
+
brand: "Caldera",
|
|
1550
|
+
syncedFrom: "Header",
|
|
1551
|
+
schema: {
|
|
1552
|
+
name: "storeHeader",
|
|
1553
|
+
title: "Store header",
|
|
1554
|
+
kind: "global",
|
|
1555
|
+
icon: "layout-navbar",
|
|
1556
|
+
category: "Navigation & chrome",
|
|
1557
|
+
description: "Logo, category bar, search, account and cart. Synced across every page.",
|
|
1558
|
+
variants: [{
|
|
1559
|
+
id: "inline-search",
|
|
1560
|
+
name: "Search in the bar",
|
|
1561
|
+
description: "A wide search field between logo and icons"
|
|
1562
|
+
}, {
|
|
1563
|
+
id: "icon-search",
|
|
1564
|
+
name: "Search icon",
|
|
1565
|
+
description: "A magnifier that opens a full-width search"
|
|
1566
|
+
}],
|
|
1567
|
+
preview: {
|
|
1568
|
+
title: "logoText",
|
|
1569
|
+
media: "logo"
|
|
1570
|
+
},
|
|
1571
|
+
groups: [
|
|
1572
|
+
{
|
|
1573
|
+
name: "content",
|
|
1574
|
+
title: "Content",
|
|
1575
|
+
default: true
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
name: "search",
|
|
1579
|
+
title: "Search"
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
name: "cart",
|
|
1583
|
+
title: "Account & cart"
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
name: "design",
|
|
1587
|
+
title: "Design"
|
|
1588
|
+
}
|
|
1589
|
+
],
|
|
1590
|
+
fieldsets: [{
|
|
1591
|
+
name: "brand",
|
|
1592
|
+
title: "Brand"
|
|
1593
|
+
}],
|
|
1594
|
+
fields: [
|
|
1595
|
+
...logoFields,
|
|
1596
|
+
{
|
|
1597
|
+
name: "categories",
|
|
1598
|
+
title: "Categories",
|
|
1599
|
+
type: "array",
|
|
1600
|
+
itemNoun: "category",
|
|
1601
|
+
validation: {
|
|
1602
|
+
min: 1,
|
|
1603
|
+
max: 8
|
|
1604
|
+
},
|
|
1605
|
+
of: [{
|
|
1606
|
+
name: "category",
|
|
1607
|
+
title: "Category",
|
|
1608
|
+
type: "object",
|
|
1609
|
+
icon: "category",
|
|
1610
|
+
preview: {
|
|
1611
|
+
title: "label",
|
|
1612
|
+
subtitle: "link.href",
|
|
1613
|
+
media: "thumbnail",
|
|
1614
|
+
fallback: "Untitled category"
|
|
1615
|
+
},
|
|
1616
|
+
fields: [
|
|
1617
|
+
{
|
|
1618
|
+
name: "label",
|
|
1619
|
+
title: "Label",
|
|
1620
|
+
type: "string",
|
|
1621
|
+
validation: {
|
|
1622
|
+
required: true,
|
|
1623
|
+
max: 20
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "link",
|
|
1628
|
+
title: "Links to",
|
|
1629
|
+
type: "link",
|
|
1630
|
+
validation: { required: true }
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
name: "thumbnail",
|
|
1634
|
+
title: "Thumbnail",
|
|
1635
|
+
type: "image"
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
name: "isHighlighted",
|
|
1639
|
+
title: "Show in the sale color",
|
|
1640
|
+
type: "boolean"
|
|
1641
|
+
}
|
|
1642
|
+
]
|
|
1643
|
+
}]
|
|
1644
|
+
},
|
|
1645
|
+
{
|
|
1646
|
+
name: "searchPlaceholder",
|
|
1647
|
+
title: "Placeholder",
|
|
1648
|
+
type: "string",
|
|
1649
|
+
group: "search",
|
|
1650
|
+
validation: { max: 48 }
|
|
1651
|
+
},
|
|
1652
|
+
{
|
|
1653
|
+
name: "suggestions",
|
|
1654
|
+
title: "Suggested searches",
|
|
1655
|
+
type: "array",
|
|
1656
|
+
itemNoun: "suggestion",
|
|
1657
|
+
of: [{
|
|
1658
|
+
name: "suggestion",
|
|
1659
|
+
title: "Suggestion",
|
|
1660
|
+
type: "string"
|
|
1661
|
+
}],
|
|
1662
|
+
group: "search",
|
|
1663
|
+
validation: { max: 6 }
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
name: "popularProducts",
|
|
1667
|
+
title: "Popular products",
|
|
1668
|
+
type: "array",
|
|
1669
|
+
group: "search",
|
|
1670
|
+
itemNoun: "product",
|
|
1671
|
+
validation: { max: 4 },
|
|
1672
|
+
of: [{
|
|
1673
|
+
name: "product",
|
|
1674
|
+
title: "Product",
|
|
1675
|
+
type: "reference",
|
|
1676
|
+
reference: { to: ["shopify"] }
|
|
1677
|
+
}]
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
name: "noResults",
|
|
1681
|
+
title: "No results message",
|
|
1682
|
+
type: "string",
|
|
1683
|
+
group: "search",
|
|
1684
|
+
validation: { max: 80 },
|
|
1685
|
+
placeholder: "Nothing matched. Try \"skillet\" or \"kettle\"."
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
name: "accountLabel",
|
|
1689
|
+
title: "Account label",
|
|
1690
|
+
type: "string",
|
|
1691
|
+
group: "cart",
|
|
1692
|
+
initialValue: "Sign in",
|
|
1693
|
+
validation: { max: 16 },
|
|
1694
|
+
description: "Shown to signed-out visitors only."
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
name: "accountLink",
|
|
1698
|
+
title: "Account page",
|
|
1699
|
+
type: "link",
|
|
1700
|
+
group: "cart"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: "cartOpens",
|
|
1704
|
+
title: "Cart opens",
|
|
1705
|
+
type: "select",
|
|
1706
|
+
group: "cart",
|
|
1707
|
+
layout: "radio",
|
|
1708
|
+
initialValue: "drawer",
|
|
1709
|
+
options: [{
|
|
1710
|
+
value: "drawer",
|
|
1711
|
+
label: "A drawer",
|
|
1712
|
+
description: "Slides in; people keep their place"
|
|
1713
|
+
}, {
|
|
1714
|
+
value: "page",
|
|
1715
|
+
label: "The cart page"
|
|
1716
|
+
}]
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
name: "emptyCart",
|
|
1720
|
+
title: "Empty cart message",
|
|
1721
|
+
type: "string",
|
|
1722
|
+
group: "cart",
|
|
1723
|
+
hidden: {
|
|
1724
|
+
field: "cartOpens",
|
|
1725
|
+
equals: "drawer"
|
|
1726
|
+
},
|
|
1727
|
+
validation: { max: 80 }
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
name: "drawerButton",
|
|
1731
|
+
title: "Drawer button",
|
|
1732
|
+
type: "button",
|
|
1733
|
+
group: "cart",
|
|
1734
|
+
hidden: {
|
|
1735
|
+
field: "cartOpens",
|
|
1736
|
+
equals: "drawer"
|
|
1737
|
+
}
|
|
1738
|
+
},
|
|
1739
|
+
{
|
|
1740
|
+
name: "isSticky",
|
|
1741
|
+
title: "Stick to the top",
|
|
1742
|
+
type: "boolean",
|
|
1743
|
+
group: "design",
|
|
1744
|
+
initialValue: true
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
name: "categoryBar",
|
|
1748
|
+
title: "Category bar",
|
|
1749
|
+
type: "select",
|
|
1750
|
+
group: "design",
|
|
1751
|
+
layout: "segmented",
|
|
1752
|
+
initialValue: "below",
|
|
1753
|
+
options: [{
|
|
1754
|
+
value: "below",
|
|
1755
|
+
label: "Below the logo"
|
|
1756
|
+
}, {
|
|
1757
|
+
value: "inline",
|
|
1758
|
+
label: "Next to the logo"
|
|
1759
|
+
}]
|
|
1760
|
+
}
|
|
1761
|
+
]
|
|
1762
|
+
},
|
|
1763
|
+
value: {
|
|
1764
|
+
_variant: "inline-search",
|
|
1765
|
+
logo: pic("caldera-mark", "Caldera", 240, 64),
|
|
1766
|
+
logoText: "Caldera",
|
|
1767
|
+
categories: [
|
|
1768
|
+
{
|
|
1769
|
+
_key: "c1",
|
|
1770
|
+
label: "Skillets",
|
|
1771
|
+
link: page("/collections/skillets", "Skillets"),
|
|
1772
|
+
thumbnail: pic("caldera-skillet-thumb", "", 320, 320)
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
_key: "c2",
|
|
1776
|
+
label: "Dutch ovens",
|
|
1777
|
+
link: page("/collections/dutch-ovens", "Dutch ovens"),
|
|
1778
|
+
thumbnail: pic("caldera-dutch-thumb", "", 320, 320)
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
_key: "c3",
|
|
1782
|
+
label: "Grills",
|
|
1783
|
+
link: page("/collections/grills", "Grills"),
|
|
1784
|
+
thumbnail: pic("caldera-grill-thumb", "", 320, 320)
|
|
1785
|
+
},
|
|
1786
|
+
{
|
|
1787
|
+
_key: "c4",
|
|
1788
|
+
label: "Camp kitchen",
|
|
1789
|
+
link: page("/collections/camp-kitchen", "Camp kitchen")
|
|
1790
|
+
},
|
|
1791
|
+
{
|
|
1792
|
+
_key: "c5",
|
|
1793
|
+
label: "Bundles",
|
|
1794
|
+
link: page("/shop/bundles", "Bundles")
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
_key: "c6",
|
|
1798
|
+
label: "Sale",
|
|
1799
|
+
link: page("/collections/sale", "Sale"),
|
|
1800
|
+
isHighlighted: true
|
|
1801
|
+
}
|
|
1802
|
+
],
|
|
1803
|
+
searchPlaceholder: "Search skillets, grills, recipes",
|
|
1804
|
+
suggestions: [
|
|
1805
|
+
"cast iron skillet",
|
|
1806
|
+
"dutch oven",
|
|
1807
|
+
"seasoning",
|
|
1808
|
+
"camp grill"
|
|
1809
|
+
],
|
|
1810
|
+
popularProducts: [
|
|
1811
|
+
"gid://shopify/Product/8812",
|
|
1812
|
+
"gid://shopify/Product/8813",
|
|
1813
|
+
"gid://shopify/Product/8814"
|
|
1814
|
+
],
|
|
1815
|
+
noResults: "Nothing matched. Try “skillet” or “kettle”.",
|
|
1816
|
+
accountLabel: "Sign in",
|
|
1817
|
+
accountLink: page("/account", "Account"),
|
|
1818
|
+
cartOpens: "drawer",
|
|
1819
|
+
emptyCart: "Your cart is empty. The fire's still going.",
|
|
1820
|
+
drawerButton: btn("Shop bestsellers", "/collections/bestsellers"),
|
|
1821
|
+
isSticky: true,
|
|
1822
|
+
categoryBar: "below"
|
|
1823
|
+
},
|
|
1824
|
+
notes: {
|
|
1825
|
+
why: "Everything a store header SAYS is content; everything it COUNTS is app state. The cart count, the signed-in name and search results come from the commerce/session layer at runtime and are deliberately not fields. What remains — categories, search hints, empty states, which way the cart opens — is one global. Popular products are references (photos and prices stay live). Suggested searches are `tags` because they're short strings with no other properties. Groups follow the visitor's mental model (Content / Search / Account & cart) so the merchandiser doesn't wade through cart copy to change a category.",
|
|
1826
|
+
sanity: "Singleton `storeHeader` { categories: array<object { label, link, thumbnail: image, highlight: boolean }>, search: object { placeholder, suggestions: array<string>, popularProducts: array<reference<product>> }, cart: object { opens: string, empty: string, cta } } with fieldset groups.",
|
|
1827
|
+
htmlSignals: [
|
|
1828
|
+
"Header with a search <input type=\"search\"> or <form role=\"search\"> plus account and bag/cart icons on the right",
|
|
1829
|
+
"A cart icon with a numeric badge (e.g. <span class=\"cart-count\">2</span>) — runtime state",
|
|
1830
|
+
"A row of collection links (/collections/…, /shop/…) under or beside the logo",
|
|
1831
|
+
"Search dropdown markup with 'Popular searches' or 'Trending' lists and product tiles",
|
|
1832
|
+
"A hidden cart drawer (<aside aria-label=\"Cart\"> / role=dialog) elsewhere in the DOM"
|
|
1833
|
+
],
|
|
1834
|
+
mcp: "Create ONE global `storeHeader`. Collection links → categories[] (a link colored red / labeled Sale → isHighlighted: true). Search placeholder → searchPlaceholder; 'Popular searches' text items → suggestions; product tiles inside the search dropdown → popularProducts resolved by handle to Shopify ids. A cart drawer element present → cartOpens: 'drawer' and copy its empty-state text/button into emptyCart/drawerButton. NEVER create fields for the cart count, prices, the customer name or currency — report them as runtime.",
|
|
1835
|
+
pitfalls: ["Copying '(2)' from the cart badge into content is the most common converter mistake — it's session state.", "Free-shipping thresholds belong to commerce settings, not the header; reference them from the renderer."]
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
id: "nav-locale-header",
|
|
1840
|
+
previewLayout: "nav",
|
|
1841
|
+
label: "Multi-locale header with switchers",
|
|
1842
|
+
proves: "Localized labels inside a global · switcher config with conditional lists · what belongs to site settings vs content",
|
|
1843
|
+
category: "Navigation & chrome",
|
|
1844
|
+
tags: [
|
|
1845
|
+
"localized",
|
|
1846
|
+
"global-synced",
|
|
1847
|
+
"conditional",
|
|
1848
|
+
"repeater"
|
|
1849
|
+
],
|
|
1850
|
+
brand: "Caldera",
|
|
1851
|
+
syncedFrom: "Header",
|
|
1852
|
+
schema: {
|
|
1853
|
+
name: "multilingualHeader",
|
|
1854
|
+
title: "Multilingual header",
|
|
1855
|
+
kind: "global",
|
|
1856
|
+
icon: "layout-navbar",
|
|
1857
|
+
category: "Navigation & chrome",
|
|
1858
|
+
description: "Main menu plus language and currency switchers. Labels are translated per locale.",
|
|
1859
|
+
preview: {
|
|
1860
|
+
title: "logoText",
|
|
1861
|
+
media: "logo"
|
|
1862
|
+
},
|
|
1863
|
+
groups: [
|
|
1864
|
+
{
|
|
1865
|
+
name: "content",
|
|
1866
|
+
title: "Content",
|
|
1867
|
+
default: true
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
name: "switchers",
|
|
1871
|
+
title: "Switchers"
|
|
1872
|
+
},
|
|
1873
|
+
{
|
|
1874
|
+
name: "design",
|
|
1875
|
+
title: "Design"
|
|
1876
|
+
}
|
|
1877
|
+
],
|
|
1878
|
+
fieldsets: [{
|
|
1879
|
+
name: "brand",
|
|
1880
|
+
title: "Brand"
|
|
1881
|
+
}],
|
|
1882
|
+
fields: [
|
|
1883
|
+
...logoFields,
|
|
1884
|
+
{
|
|
1885
|
+
name: "items",
|
|
1886
|
+
title: "Menu items",
|
|
1887
|
+
type: "array",
|
|
1888
|
+
itemNoun: "menu item",
|
|
1889
|
+
validation: {
|
|
1890
|
+
min: 1,
|
|
1891
|
+
max: 6
|
|
1892
|
+
},
|
|
1893
|
+
of: [{
|
|
1894
|
+
name: "localizedLink",
|
|
1895
|
+
title: "Link",
|
|
1896
|
+
type: "object",
|
|
1897
|
+
icon: "link",
|
|
1898
|
+
openMode: "inline",
|
|
1899
|
+
preview: {
|
|
1900
|
+
title: "label",
|
|
1901
|
+
subtitle: "link.href",
|
|
1902
|
+
fallback: "Untitled link"
|
|
1903
|
+
},
|
|
1904
|
+
fields: [{
|
|
1905
|
+
name: "label",
|
|
1906
|
+
title: "Label",
|
|
1907
|
+
type: "string",
|
|
1908
|
+
localized: true,
|
|
1909
|
+
validation: {
|
|
1910
|
+
required: true,
|
|
1911
|
+
max: 20
|
|
1912
|
+
}
|
|
1913
|
+
}, {
|
|
1914
|
+
name: "link",
|
|
1915
|
+
title: "Links to",
|
|
1916
|
+
type: "link",
|
|
1917
|
+
validation: { required: true },
|
|
1918
|
+
description: "Each language opens its own translation."
|
|
1919
|
+
}]
|
|
1920
|
+
}]
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
name: "button",
|
|
1924
|
+
title: "Button",
|
|
1925
|
+
type: "button",
|
|
1926
|
+
localized: true
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
name: "showLanguage",
|
|
1930
|
+
title: "Language switcher",
|
|
1931
|
+
type: "boolean",
|
|
1932
|
+
group: "switchers",
|
|
1933
|
+
initialValue: true
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
name: "languages",
|
|
1937
|
+
title: "Languages",
|
|
1938
|
+
type: "array",
|
|
1939
|
+
group: "switchers",
|
|
1940
|
+
itemNoun: "language",
|
|
1941
|
+
hidden: {
|
|
1942
|
+
field: "showLanguage",
|
|
1943
|
+
equals: true
|
|
1944
|
+
},
|
|
1945
|
+
validation: {
|
|
1946
|
+
min: 2,
|
|
1947
|
+
max: 12
|
|
1948
|
+
},
|
|
1949
|
+
description: "Only languages the site is published in.",
|
|
1950
|
+
of: [{
|
|
1951
|
+
name: "language",
|
|
1952
|
+
title: "Language",
|
|
1953
|
+
type: "object",
|
|
1954
|
+
icon: "language",
|
|
1955
|
+
openMode: "inline",
|
|
1956
|
+
preview: {
|
|
1957
|
+
title: "nativeName",
|
|
1958
|
+
subtitle: "locale",
|
|
1959
|
+
fallback: "Untitled language"
|
|
1960
|
+
},
|
|
1961
|
+
fields: [{
|
|
1962
|
+
name: "locale",
|
|
1963
|
+
title: "Locale",
|
|
1964
|
+
type: "select",
|
|
1965
|
+
validation: {
|
|
1966
|
+
required: true,
|
|
1967
|
+
unique: true
|
|
1968
|
+
},
|
|
1969
|
+
options: [
|
|
1970
|
+
{
|
|
1971
|
+
value: "en-US",
|
|
1972
|
+
label: "English (US)"
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
value: "en-GB",
|
|
1976
|
+
label: "English (UK)"
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
value: "de-DE",
|
|
1980
|
+
label: "German"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
value: "fr-FR",
|
|
1984
|
+
label: "French"
|
|
1985
|
+
},
|
|
1986
|
+
{
|
|
1987
|
+
value: "ja-JP",
|
|
1988
|
+
label: "Japanese"
|
|
1989
|
+
}
|
|
1990
|
+
]
|
|
1991
|
+
}, {
|
|
1992
|
+
name: "nativeName",
|
|
1993
|
+
title: "Name in the menu",
|
|
1994
|
+
type: "string",
|
|
1995
|
+
validation: {
|
|
1996
|
+
required: true,
|
|
1997
|
+
max: 20
|
|
1998
|
+
},
|
|
1999
|
+
description: "In its own language, e.g. Deutsch."
|
|
2000
|
+
}]
|
|
2001
|
+
}]
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "showCurrency",
|
|
2005
|
+
title: "Currency switcher",
|
|
2006
|
+
type: "boolean",
|
|
2007
|
+
group: "switchers",
|
|
2008
|
+
initialValue: true
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
name: "currencies",
|
|
2012
|
+
title: "Currencies",
|
|
2013
|
+
type: "multiselect",
|
|
2014
|
+
group: "switchers",
|
|
2015
|
+
hidden: {
|
|
2016
|
+
field: "showCurrency",
|
|
2017
|
+
equals: true
|
|
2018
|
+
},
|
|
2019
|
+
options: [
|
|
2020
|
+
{
|
|
2021
|
+
value: "USD",
|
|
2022
|
+
label: "US dollar · $"
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
value: "EUR",
|
|
2026
|
+
label: "Euro · €"
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
value: "GBP",
|
|
2030
|
+
label: "Pound sterling · £"
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
value: "CAD",
|
|
2034
|
+
label: "Canadian dollar · CA$"
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
value: "JPY",
|
|
2038
|
+
label: "Japanese yen · ¥"
|
|
2039
|
+
}
|
|
2040
|
+
]
|
|
2041
|
+
},
|
|
2042
|
+
{
|
|
2043
|
+
name: "firstVisit",
|
|
2044
|
+
title: "On the first visit",
|
|
2045
|
+
type: "select",
|
|
2046
|
+
group: "switchers",
|
|
2047
|
+
layout: "radio",
|
|
2048
|
+
initialValue: "suggest",
|
|
2049
|
+
options: [{
|
|
2050
|
+
value: "suggest",
|
|
2051
|
+
label: "Suggest a locale",
|
|
2052
|
+
description: "A banner offers the visitor's language; nothing changes until they pick"
|
|
2053
|
+
}, {
|
|
2054
|
+
value: "stay",
|
|
2055
|
+
label: "Stay on the address they opened"
|
|
2056
|
+
}]
|
|
2057
|
+
},
|
|
2058
|
+
{
|
|
2059
|
+
name: "suggestMessage",
|
|
2060
|
+
title: "Suggestion message",
|
|
2061
|
+
type: "string",
|
|
2062
|
+
group: "switchers",
|
|
2063
|
+
localized: true,
|
|
2064
|
+
hidden: {
|
|
2065
|
+
field: "firstVisit",
|
|
2066
|
+
equals: "suggest"
|
|
2067
|
+
},
|
|
2068
|
+
validation: { max: 80 },
|
|
2069
|
+
placeholder: "Looks like you're in Germany. Shop in Deutsch · EUR?"
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
name: "display",
|
|
2073
|
+
title: "Switcher shows",
|
|
2074
|
+
type: "select",
|
|
2075
|
+
group: "design",
|
|
2076
|
+
layout: "segmented",
|
|
2077
|
+
initialValue: "code",
|
|
2078
|
+
options: [
|
|
2079
|
+
{
|
|
2080
|
+
value: "code",
|
|
2081
|
+
label: "EN · USD"
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
value: "name",
|
|
2085
|
+
label: "English · $"
|
|
2086
|
+
},
|
|
2087
|
+
{
|
|
2088
|
+
value: "globe",
|
|
2089
|
+
label: "Globe icon"
|
|
2090
|
+
}
|
|
2091
|
+
]
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: "placement",
|
|
2095
|
+
title: "Placement",
|
|
2096
|
+
type: "select",
|
|
2097
|
+
group: "design",
|
|
2098
|
+
layout: "segmented",
|
|
2099
|
+
initialValue: "header",
|
|
2100
|
+
options: [
|
|
2101
|
+
{
|
|
2102
|
+
value: "header",
|
|
2103
|
+
label: "Header"
|
|
2104
|
+
},
|
|
2105
|
+
{
|
|
2106
|
+
value: "footer",
|
|
2107
|
+
label: "Footer"
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
value: "both",
|
|
2111
|
+
label: "Both"
|
|
2112
|
+
}
|
|
2113
|
+
]
|
|
2114
|
+
}
|
|
2115
|
+
]
|
|
2116
|
+
},
|
|
2117
|
+
value: {
|
|
2118
|
+
logo: pic("caldera-mark", "Caldera", 240, 64),
|
|
2119
|
+
logoText: "Caldera",
|
|
2120
|
+
items: [
|
|
2121
|
+
{
|
|
2122
|
+
_key: "i1",
|
|
2123
|
+
label: "Shop",
|
|
2124
|
+
link: page("/products", "Products")
|
|
2125
|
+
},
|
|
2126
|
+
{
|
|
2127
|
+
_key: "i2",
|
|
2128
|
+
label: "Recipes",
|
|
2129
|
+
link: page("/recipes", "Recipes")
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
_key: "i3",
|
|
2133
|
+
label: "Stockists",
|
|
2134
|
+
link: page("/retailers", "Retailers")
|
|
2135
|
+
},
|
|
2136
|
+
{
|
|
2137
|
+
_key: "i4",
|
|
2138
|
+
label: "About",
|
|
2139
|
+
link: page("/about", "About")
|
|
2140
|
+
}
|
|
2141
|
+
],
|
|
2142
|
+
button: btn("Shop the Fold", "/products/cable-fold"),
|
|
2143
|
+
showLanguage: true,
|
|
2144
|
+
languages: [
|
|
2145
|
+
{
|
|
2146
|
+
_key: "g1",
|
|
2147
|
+
locale: "en-US",
|
|
2148
|
+
nativeName: "English"
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
_key: "g2",
|
|
2152
|
+
locale: "de-DE",
|
|
2153
|
+
nativeName: "Deutsch"
|
|
2154
|
+
},
|
|
2155
|
+
{
|
|
2156
|
+
_key: "g3",
|
|
2157
|
+
locale: "fr-FR",
|
|
2158
|
+
nativeName: "Français"
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
_key: "g4",
|
|
2162
|
+
locale: "ja-JP",
|
|
2163
|
+
nativeName: "日本語"
|
|
2164
|
+
}
|
|
2165
|
+
],
|
|
2166
|
+
showCurrency: true,
|
|
2167
|
+
currencies: [
|
|
2168
|
+
"USD",
|
|
2169
|
+
"EUR",
|
|
2170
|
+
"GBP",
|
|
2171
|
+
"JPY"
|
|
2172
|
+
],
|
|
2173
|
+
firstVisit: "suggest",
|
|
2174
|
+
suggestMessage: "Looks like you're in Germany. Shop in Deutsch · EUR?",
|
|
2175
|
+
display: "code",
|
|
2176
|
+
placement: "header"
|
|
2177
|
+
},
|
|
2178
|
+
notes: {
|
|
2179
|
+
why: "Labels are `localized: true` (one field, a value per locale) instead of one header per language: the menu STRUCTURE is shared, only the words change, so adding a link once adds it everywhere. Links point at pages, and the router resolves each locale's translation — no per-locale URLs typed in. The switcher's lists are content today because the site owner curates order and native names, but they mirror what should be site settings (published locales, enabled currencies) — see the gap note. Display (codes vs names) and placement are Design.",
|
|
2180
|
+
sanity: "Field-level i18n with `sanity-plugin-internationalized-array`: `label: internationalizedArrayString`; locales defined in plugin config, not in the document. Or document-level i18n (one header doc per language via @sanity/document-internationalization).",
|
|
2181
|
+
htmlSignals: [
|
|
2182
|
+
"<html lang> differs across crawled URLs (/de/, /fr/ prefixes or subdomains) while the header markup is identical",
|
|
2183
|
+
"A dropdown listing language names in their own script (Deutsch, Français, 日本語) and/or currency codes/symbols",
|
|
2184
|
+
"<link rel=\"alternate\" hreflang=\"…\"> tags in <head> (native SEO — don't model them)",
|
|
2185
|
+
"A 'Shop in your language?' geo banner or modal"
|
|
2186
|
+
],
|
|
2187
|
+
mcp: "Crawl the same page in each locale. Header with identical structure → ONE global; for each nav label, write the per-locale strings into the localized `label` (match items by href position, not by text). Language dropdown options → languages[] { locale from hreflang/path prefix, nativeName: the option text }. Currency options → currencies (ISO codes). Never model hreflang tags — the platform emits them. If headers differ structurally by locale (different items), report it: that's a per-market header, not a translation.",
|
|
2188
|
+
pitfalls: ["Using flags for languages (🇬🇧 for English) — map to codes and set display: 'code'.", "Translating the link target by typing /de/... URLs — link to the page and let locale routing resolve it."]
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
id: "sticky-mobile-cta-bar",
|
|
2193
|
+
previewLayout: "banner",
|
|
2194
|
+
label: "Sticky bottom CTA bar (mobile)",
|
|
2195
|
+
proves: "A per-template block with breakpoint visibility, scroll triggers and a secondary contact action",
|
|
2196
|
+
category: "Navigation & chrome",
|
|
2197
|
+
tags: [
|
|
2198
|
+
"responsive-layout",
|
|
2199
|
+
"conditional",
|
|
2200
|
+
"validation",
|
|
2201
|
+
"commerce"
|
|
2202
|
+
],
|
|
2203
|
+
brand: "Caldera",
|
|
2204
|
+
schema: {
|
|
2205
|
+
name: "stickyBar",
|
|
2206
|
+
title: "Sticky action bar",
|
|
2207
|
+
kind: "block",
|
|
2208
|
+
icon: "layout-bottombar-expand",
|
|
2209
|
+
category: "Navigation & chrome",
|
|
2210
|
+
description: "A bar pinned to the bottom of the screen with one main action.",
|
|
2211
|
+
preview: {
|
|
2212
|
+
title: "message",
|
|
2213
|
+
subtitle: "button.label"
|
|
2214
|
+
},
|
|
2215
|
+
groups: [
|
|
2216
|
+
{
|
|
2217
|
+
name: "content",
|
|
2218
|
+
title: "Content",
|
|
2219
|
+
default: true
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
name: "behavior",
|
|
2223
|
+
title: "Behavior"
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
name: "design",
|
|
2227
|
+
title: "Design"
|
|
2228
|
+
}
|
|
2229
|
+
],
|
|
2230
|
+
fields: [
|
|
2231
|
+
{
|
|
2232
|
+
name: "message",
|
|
2233
|
+
title: "Message",
|
|
2234
|
+
type: "string",
|
|
2235
|
+
validation: {
|
|
2236
|
+
recommendedMax: 40,
|
|
2237
|
+
max: 60
|
|
2238
|
+
},
|
|
2239
|
+
ai: true
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
name: "showPrice",
|
|
2243
|
+
title: "Show the product price",
|
|
2244
|
+
type: "boolean",
|
|
2245
|
+
description: "Product pages only."
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
name: "button",
|
|
2249
|
+
title: "Button",
|
|
2250
|
+
type: "button",
|
|
2251
|
+
validation: { required: true }
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
name: "secondaryAction",
|
|
2255
|
+
title: "Second action",
|
|
2256
|
+
type: "select",
|
|
2257
|
+
layout: "segmented",
|
|
2258
|
+
initialValue: "none",
|
|
2259
|
+
options: [
|
|
2260
|
+
{
|
|
2261
|
+
value: "none",
|
|
2262
|
+
label: "None"
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
value: "call",
|
|
2266
|
+
label: "Call"
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
value: "chat",
|
|
2270
|
+
label: "Chat"
|
|
2271
|
+
}
|
|
2272
|
+
]
|
|
2273
|
+
},
|
|
2274
|
+
{
|
|
2275
|
+
name: "phone",
|
|
2276
|
+
title: "Phone",
|
|
2277
|
+
type: "link",
|
|
2278
|
+
hidden: {
|
|
2279
|
+
field: "secondaryAction",
|
|
2280
|
+
equals: "call"
|
|
2281
|
+
},
|
|
2282
|
+
validation: { required: true }
|
|
2283
|
+
},
|
|
2284
|
+
{
|
|
2285
|
+
name: "showOn",
|
|
2286
|
+
title: "Show on",
|
|
2287
|
+
type: "multiselect",
|
|
2288
|
+
group: "behavior",
|
|
2289
|
+
initialValue: ["mobile"],
|
|
2290
|
+
validation: { min: 1 },
|
|
2291
|
+
options: [
|
|
2292
|
+
{
|
|
2293
|
+
value: "mobile",
|
|
2294
|
+
label: "Phones"
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
value: "tablet",
|
|
2298
|
+
label: "Tablets"
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
value: "desktop",
|
|
2302
|
+
label: "Desktop"
|
|
2303
|
+
}
|
|
2304
|
+
]
|
|
2305
|
+
},
|
|
2306
|
+
{
|
|
2307
|
+
name: "trigger",
|
|
2308
|
+
title: "Appears",
|
|
2309
|
+
type: "select",
|
|
2310
|
+
group: "behavior",
|
|
2311
|
+
layout: "radio",
|
|
2312
|
+
initialValue: "pastHero",
|
|
2313
|
+
options: [
|
|
2314
|
+
{
|
|
2315
|
+
value: "always",
|
|
2316
|
+
label: "Right away"
|
|
2317
|
+
},
|
|
2318
|
+
{
|
|
2319
|
+
value: "pastHero",
|
|
2320
|
+
label: "After the first section scrolls away",
|
|
2321
|
+
description: "Avoids two identical buttons on screen"
|
|
2322
|
+
},
|
|
2323
|
+
{
|
|
2324
|
+
value: "percent",
|
|
2325
|
+
label: "After part of the page"
|
|
2326
|
+
}
|
|
2327
|
+
]
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
name: "percent",
|
|
2331
|
+
title: "Scrolled",
|
|
2332
|
+
type: "number",
|
|
2333
|
+
group: "behavior",
|
|
2334
|
+
layout: "slider",
|
|
2335
|
+
hidden: {
|
|
2336
|
+
field: "trigger",
|
|
2337
|
+
equals: "percent"
|
|
2338
|
+
},
|
|
2339
|
+
numeric: {
|
|
2340
|
+
min: 10,
|
|
2341
|
+
max: 90,
|
|
2342
|
+
step: 5,
|
|
2343
|
+
unit: "%"
|
|
2344
|
+
},
|
|
2345
|
+
initialValue: 30
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
name: "hideNearFooter",
|
|
2349
|
+
title: "Hide near the footer",
|
|
2350
|
+
type: "boolean",
|
|
2351
|
+
group: "behavior",
|
|
2352
|
+
initialValue: true
|
|
2353
|
+
},
|
|
2354
|
+
themeField
|
|
2355
|
+
]
|
|
2356
|
+
},
|
|
2357
|
+
value: {
|
|
2358
|
+
message: "Cable Fold Grill · ships in 1–2 days",
|
|
2359
|
+
showPrice: true,
|
|
2360
|
+
button: btn("Add to cart", "#buy", "primary"),
|
|
2361
|
+
secondaryAction: "chat",
|
|
2362
|
+
showOn: ["mobile", "tablet"],
|
|
2363
|
+
trigger: "pastHero",
|
|
2364
|
+
percent: 30,
|
|
2365
|
+
hideNearFooter: true,
|
|
2366
|
+
theme: "dark"
|
|
2367
|
+
},
|
|
2368
|
+
notes: {
|
|
2369
|
+
why: "Unlike the header, this bar is placed per template (product pages, a landing page), so it's an ordinary block, not a global. Breakpoint visibility is a multiselect because our schema has no per-breakpoint value type — this is the honest minimum (see gaps). The price is a switch, not a number: it's read live from the product the page is about. The second action is a select with a conditional phone link, so a chat action doesn't ask for a number it won't use.",
|
|
2370
|
+
sanity: "object `stickyCta` { message: string, showPrice: boolean, button: object, secondary: string(list), phone: string (hidden unless call), showOn: array<string>(list), trigger: string, percent: number }.",
|
|
2371
|
+
htmlSignals: [
|
|
2372
|
+
"A fixed element at bottom: 0 with a single prominent button, often display:none ≥ 768px",
|
|
2373
|
+
"IntersectionObserver / scroll listener toggling a class like is-visible on the bar",
|
|
2374
|
+
"Duplicates the main CTA text from the hero or buy box",
|
|
2375
|
+
"env(safe-area-inset-bottom) padding"
|
|
2376
|
+
],
|
|
2377
|
+
mcp: "position: fixed + bottom: 0 + one button → `stickyBar`, placed on the template it appears on (not global unless it's on every page). Read media queries for showOn. A scroll listener tied to the hero → trigger: 'pastHero'. Copy the price only as showPrice: true; never the number. If the button submits a cart form, keep href '#buy' and report that the renderer must wire it to the product's add-to-cart.",
|
|
2378
|
+
pitfalls: ["'Add to cart' is an ACTION, not a link — our button type only carries links (gap: action buttons).", "A bar covering the cookie banner or chat launcher: the renderer must stack them; nothing to model."]
|
|
2379
|
+
}
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
id: "announcement-countdown",
|
|
2383
|
+
previewLayout: "banner",
|
|
2384
|
+
label: "Announcement bar with countdown",
|
|
2385
|
+
proves: "Scheduled global with time zone · countdown with an after-end state · dismiss memory · rotating messages",
|
|
2386
|
+
category: "Navigation & chrome",
|
|
2387
|
+
tags: [
|
|
2388
|
+
"global-synced",
|
|
2389
|
+
"conditional",
|
|
2390
|
+
"validation",
|
|
2391
|
+
"repeater",
|
|
2392
|
+
"localized",
|
|
2393
|
+
"a11y"
|
|
2394
|
+
],
|
|
2395
|
+
brand: "Caldera",
|
|
2396
|
+
syncedFrom: "Announcement bar",
|
|
2397
|
+
schema: {
|
|
2398
|
+
name: "countdownBar",
|
|
2399
|
+
title: "Countdown bar",
|
|
2400
|
+
kind: "global",
|
|
2401
|
+
icon: "alarm",
|
|
2402
|
+
category: "Navigation & chrome",
|
|
2403
|
+
description: "A strip above the header for a sale or launch, with an optional countdown.",
|
|
2404
|
+
preview: {
|
|
2405
|
+
title: "messages.0.text",
|
|
2406
|
+
subtitle: "countdown.endsAt"
|
|
2407
|
+
},
|
|
2408
|
+
groups: [
|
|
2409
|
+
{
|
|
2410
|
+
name: "content",
|
|
2411
|
+
title: "Content",
|
|
2412
|
+
default: true
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
name: "schedule",
|
|
2416
|
+
title: "Schedule"
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
name: "design",
|
|
2420
|
+
title: "Design"
|
|
2421
|
+
}
|
|
2422
|
+
],
|
|
2423
|
+
fields: [
|
|
2424
|
+
{
|
|
2425
|
+
name: "messages",
|
|
2426
|
+
title: "Messages",
|
|
2427
|
+
type: "array",
|
|
2428
|
+
itemNoun: "message",
|
|
2429
|
+
validation: {
|
|
2430
|
+
min: 1,
|
|
2431
|
+
max: 3
|
|
2432
|
+
},
|
|
2433
|
+
description: "Messages rotate every 5 seconds.",
|
|
2434
|
+
of: [{
|
|
2435
|
+
name: "message",
|
|
2436
|
+
title: "Message",
|
|
2437
|
+
type: "object",
|
|
2438
|
+
icon: "message",
|
|
2439
|
+
openMode: "inline",
|
|
2440
|
+
preview: {
|
|
2441
|
+
title: "text",
|
|
2442
|
+
subtitle: "link.href",
|
|
2443
|
+
fallback: "Untitled message"
|
|
2444
|
+
},
|
|
2445
|
+
fields: [{
|
|
2446
|
+
name: "text",
|
|
2447
|
+
title: "Text",
|
|
2448
|
+
type: "string",
|
|
2449
|
+
localized: true,
|
|
2450
|
+
ai: true,
|
|
2451
|
+
validation: {
|
|
2452
|
+
required: true,
|
|
2453
|
+
max: 80,
|
|
2454
|
+
recommendedMax: 60
|
|
2455
|
+
}
|
|
2456
|
+
}, {
|
|
2457
|
+
name: "link",
|
|
2458
|
+
title: "Links to",
|
|
2459
|
+
type: "link"
|
|
2460
|
+
}]
|
|
2461
|
+
}]
|
|
2462
|
+
},
|
|
2463
|
+
{
|
|
2464
|
+
name: "countdown",
|
|
2465
|
+
title: "Countdown",
|
|
2466
|
+
type: "object",
|
|
2467
|
+
icon: "clock",
|
|
2468
|
+
openMode: "drill",
|
|
2469
|
+
fields: [
|
|
2470
|
+
{
|
|
2471
|
+
name: "showCountdown",
|
|
2472
|
+
title: "Show a countdown",
|
|
2473
|
+
type: "boolean"
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
name: "endsAt",
|
|
2477
|
+
title: "Counts down to",
|
|
2478
|
+
type: "datetime",
|
|
2479
|
+
hidden: {
|
|
2480
|
+
field: "showCountdown",
|
|
2481
|
+
equals: true
|
|
2482
|
+
},
|
|
2483
|
+
validation: { required: true },
|
|
2484
|
+
description: "In the sale's time zone. Visitors see their local time."
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
name: "label",
|
|
2488
|
+
title: "Label",
|
|
2489
|
+
type: "string",
|
|
2490
|
+
hidden: {
|
|
2491
|
+
field: "showCountdown",
|
|
2492
|
+
equals: true
|
|
2493
|
+
},
|
|
2494
|
+
validation: { max: 20 },
|
|
2495
|
+
placeholder: "Ends in"
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
name: "afterEnd",
|
|
2499
|
+
title: "When it reaches zero",
|
|
2500
|
+
type: "select",
|
|
2501
|
+
hidden: {
|
|
2502
|
+
field: "showCountdown",
|
|
2503
|
+
equals: true
|
|
2504
|
+
},
|
|
2505
|
+
layout: "radio",
|
|
2506
|
+
initialValue: "hide",
|
|
2507
|
+
options: [{
|
|
2508
|
+
value: "hide",
|
|
2509
|
+
label: "Hide the bar"
|
|
2510
|
+
}, {
|
|
2511
|
+
value: "message",
|
|
2512
|
+
label: "Show a different message"
|
|
2513
|
+
}]
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
name: "endedText",
|
|
2517
|
+
title: "Message after the end",
|
|
2518
|
+
type: "string",
|
|
2519
|
+
localized: true,
|
|
2520
|
+
hidden: {
|
|
2521
|
+
field: "afterEnd",
|
|
2522
|
+
equals: "message"
|
|
2523
|
+
},
|
|
2524
|
+
validation: { max: 80 },
|
|
2525
|
+
placeholder: "The sale has ended. Thanks for shopping."
|
|
2526
|
+
}
|
|
2527
|
+
]
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
name: "isDismissible",
|
|
2531
|
+
title: "Visitors can close it",
|
|
2532
|
+
type: "boolean",
|
|
2533
|
+
initialValue: true
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
name: "rememberDays",
|
|
2537
|
+
title: "Stay closed for",
|
|
2538
|
+
type: "number",
|
|
2539
|
+
hidden: {
|
|
2540
|
+
field: "isDismissible",
|
|
2541
|
+
equals: true
|
|
2542
|
+
},
|
|
2543
|
+
numeric: {
|
|
2544
|
+
min: 1,
|
|
2545
|
+
max: 90,
|
|
2546
|
+
step: 1,
|
|
2547
|
+
unit: "days"
|
|
2548
|
+
},
|
|
2549
|
+
initialValue: 7,
|
|
2550
|
+
description: "Changing the message shows it again to people who closed it."
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
name: "startsAt",
|
|
2554
|
+
title: "Show from",
|
|
2555
|
+
type: "datetime",
|
|
2556
|
+
group: "schedule",
|
|
2557
|
+
description: "Leave empty to show it as soon as it's published."
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
name: "endsAt",
|
|
2561
|
+
title: "Hide after",
|
|
2562
|
+
type: "datetime",
|
|
2563
|
+
group: "schedule",
|
|
2564
|
+
description: "Must be on or after the countdown's end."
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
name: "tone",
|
|
2568
|
+
title: "Color",
|
|
2569
|
+
type: "select",
|
|
2570
|
+
group: "design",
|
|
2571
|
+
layout: "segmented",
|
|
2572
|
+
initialValue: "brand",
|
|
2573
|
+
options: [
|
|
2574
|
+
{
|
|
2575
|
+
value: "neutral",
|
|
2576
|
+
label: "Neutral"
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
value: "brand",
|
|
2580
|
+
label: "Brand"
|
|
2581
|
+
},
|
|
2582
|
+
{
|
|
2583
|
+
value: "sale",
|
|
2584
|
+
label: "Sale"
|
|
2585
|
+
}
|
|
2586
|
+
]
|
|
2587
|
+
},
|
|
2588
|
+
{
|
|
2589
|
+
name: "usePauseOnHover",
|
|
2590
|
+
title: "Pause on hover and focus",
|
|
2591
|
+
type: "boolean",
|
|
2592
|
+
group: "design",
|
|
2593
|
+
initialValue: true,
|
|
2594
|
+
access: { edit: "schema.manage" },
|
|
2595
|
+
lockReason: "Required for accessibility: moving content must be pausable."
|
|
2596
|
+
}
|
|
2597
|
+
]
|
|
2598
|
+
},
|
|
2599
|
+
value: {
|
|
2600
|
+
messages: [{
|
|
2601
|
+
_key: "a1",
|
|
2602
|
+
text: "Black Friday: 25% off every skillet and Dutch oven",
|
|
2603
|
+
link: page("/collections/sale", "Sale")
|
|
2604
|
+
}, {
|
|
2605
|
+
_key: "a2",
|
|
2606
|
+
text: "Free shipping over $80 · Order by Dec 18 for the holidays",
|
|
2607
|
+
link: page("/shipping", "Shipping & returns")
|
|
2608
|
+
}],
|
|
2609
|
+
countdown: {
|
|
2610
|
+
showCountdown: true,
|
|
2611
|
+
endsAt: "2026-11-30T23:59:00-08:00[America/Los_Angeles]",
|
|
2612
|
+
label: "Ends in",
|
|
2613
|
+
afterEnd: "message",
|
|
2614
|
+
endedText: "The sale has ended — thanks for cooking with us."
|
|
2615
|
+
},
|
|
2616
|
+
isDismissible: true,
|
|
2617
|
+
rememberDays: 3,
|
|
2618
|
+
startsAt: "2026-11-26T00:00:00-08:00[America/Los_Angeles]",
|
|
2619
|
+
endsAt: "2026-12-02T23:59:00-08:00[America/Los_Angeles]",
|
|
2620
|
+
tone: "sale",
|
|
2621
|
+
usePauseOnHover: true
|
|
2622
|
+
},
|
|
2623
|
+
notes: {
|
|
2624
|
+
why: "A standalone global (not part of the header) because it has its own lifecycle: scheduled on and off, often swapped weekly while the header stays put. Messages are a small array so rotation is data, not a hack of joined strings. The countdown is an object with an `showCountdown` switch whose fields appear only when it's on, including what happens at zero — the moment most hand-built bars break. Dismissal is behavior that belongs to the content (how long a closed bar stays closed). The pause control is locked on for marketers because WCAG 2.2.2 requires moving content to be pausable.",
|
|
2625
|
+
sanity: "Singleton `countdownBar` { messages: array<object { text, link }>, countdown: object { enabled, endsAt: datetime, label, afterEnd, endedText }, dismissible: boolean, rememberDays: number, startsAt/endsAt: datetime }. Scheduling would usually be Sanity's Scheduled Publishing instead of fields.",
|
|
2626
|
+
htmlSignals: [
|
|
2627
|
+
"A full-width strip above <header> with one sentence and a close (×) <button aria-label=\"Close\">",
|
|
2628
|
+
"Digits ticking every second (setInterval) formatted as 02d 14h 05m, plus an end timestamp in a data attribute",
|
|
2629
|
+
"localStorage/cookie key like announcement_dismissed",
|
|
2630
|
+
"Several messages in a track with translateX/opacity animation — rotation"
|
|
2631
|
+
],
|
|
2632
|
+
mcp: "Separate global `countdownBar`, independent from the header. Each rotating text → messages[] { text, link }. A countdown's target timestamp (data-end, a Date string in the script) → countdown.endsAt WITH its offset; never store the rendered '02d 14h'. A close button → isDismissible: true; a cookie/localStorage max-age → rememberDays. If the script hides the bar at zero → afterEnd: 'hide'. Map red/sale colors to tone: 'sale'.",
|
|
2633
|
+
pitfalls: ["Storing the countdown as text ('2 days left') — it's wrong the next day. Always a datetime with offset.", "Hide-after earlier than the countdown end can't be caught today (gap: cross-field validation)."]
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2636
|
+
{
|
|
2637
|
+
id: "cookie-consent",
|
|
2638
|
+
previewLayout: "banner",
|
|
2639
|
+
label: "Cookie consent banner",
|
|
2640
|
+
proves: "Consent categories as data · action labels that are NOT links · developer-locked keys · localized legal copy",
|
|
2641
|
+
category: "Navigation & chrome",
|
|
2642
|
+
tags: [
|
|
2643
|
+
"global-synced",
|
|
2644
|
+
"repeater",
|
|
2645
|
+
"validation",
|
|
2646
|
+
"a11y",
|
|
2647
|
+
"localized",
|
|
2648
|
+
"conditional",
|
|
2649
|
+
"variants"
|
|
2650
|
+
],
|
|
2651
|
+
brand: "Open Water Trust",
|
|
2652
|
+
syncedFrom: "Cookie consent",
|
|
2653
|
+
schema: {
|
|
2654
|
+
name: "cookieBanner",
|
|
2655
|
+
title: "Cookie banner",
|
|
2656
|
+
kind: "global",
|
|
2657
|
+
icon: "cookie",
|
|
2658
|
+
category: "Navigation & chrome",
|
|
2659
|
+
description: "The consent banner and its preferences panel. Shown until a visitor chooses.",
|
|
2660
|
+
variants: [
|
|
2661
|
+
{
|
|
2662
|
+
id: "bar",
|
|
2663
|
+
name: "Bottom bar",
|
|
2664
|
+
description: "Full width, doesn't block the page"
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
id: "box",
|
|
2668
|
+
name: "Corner box",
|
|
2669
|
+
description: "Small card in the bottom-left corner"
|
|
2670
|
+
},
|
|
2671
|
+
{
|
|
2672
|
+
id: "modal",
|
|
2673
|
+
name: "Centered modal",
|
|
2674
|
+
description: "Blocks the page until a choice is made"
|
|
2675
|
+
}
|
|
2676
|
+
],
|
|
2677
|
+
preview: {
|
|
2678
|
+
title: "title",
|
|
2679
|
+
subtitle: "body"
|
|
2680
|
+
},
|
|
2681
|
+
groups: [
|
|
2682
|
+
{
|
|
2683
|
+
name: "content",
|
|
2684
|
+
title: "Banner",
|
|
2685
|
+
default: true
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
name: "categories",
|
|
2689
|
+
title: "Categories"
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
name: "settings",
|
|
2693
|
+
title: "Settings"
|
|
2694
|
+
}
|
|
2695
|
+
],
|
|
2696
|
+
fields: [
|
|
2697
|
+
{
|
|
2698
|
+
name: "title",
|
|
2699
|
+
title: "Title",
|
|
2700
|
+
type: "string",
|
|
2701
|
+
localized: true,
|
|
2702
|
+
validation: { max: 48 },
|
|
2703
|
+
placeholder: "We use cookies"
|
|
2704
|
+
},
|
|
2705
|
+
{
|
|
2706
|
+
name: "body",
|
|
2707
|
+
title: "Body",
|
|
2708
|
+
type: "text",
|
|
2709
|
+
rows: 3,
|
|
2710
|
+
localized: true,
|
|
2711
|
+
validation: {
|
|
2712
|
+
required: true,
|
|
2713
|
+
recommendedMax: 220
|
|
2714
|
+
}
|
|
2715
|
+
},
|
|
2716
|
+
{
|
|
2717
|
+
name: "policyLink",
|
|
2718
|
+
title: "Cookie policy",
|
|
2719
|
+
type: "link",
|
|
2720
|
+
validation: { required: true },
|
|
2721
|
+
description: "Most privacy laws require it."
|
|
2722
|
+
},
|
|
2723
|
+
{
|
|
2724
|
+
name: "labels",
|
|
2725
|
+
title: "Button labels",
|
|
2726
|
+
type: "object",
|
|
2727
|
+
icon: "click",
|
|
2728
|
+
openMode: "drill",
|
|
2729
|
+
fields: [
|
|
2730
|
+
{
|
|
2731
|
+
name: "acceptAll",
|
|
2732
|
+
title: "Accept all",
|
|
2733
|
+
type: "string",
|
|
2734
|
+
localized: true,
|
|
2735
|
+
validation: {
|
|
2736
|
+
required: true,
|
|
2737
|
+
max: 20
|
|
2738
|
+
},
|
|
2739
|
+
initialValue: "Accept all"
|
|
2740
|
+
},
|
|
2741
|
+
{
|
|
2742
|
+
name: "rejectAll",
|
|
2743
|
+
title: "Reject all",
|
|
2744
|
+
type: "string",
|
|
2745
|
+
localized: true,
|
|
2746
|
+
validation: {
|
|
2747
|
+
required: true,
|
|
2748
|
+
max: 20
|
|
2749
|
+
},
|
|
2750
|
+
initialValue: "Reject all"
|
|
2751
|
+
},
|
|
2752
|
+
{
|
|
2753
|
+
name: "customize",
|
|
2754
|
+
title: "Open preferences",
|
|
2755
|
+
type: "string",
|
|
2756
|
+
localized: true,
|
|
2757
|
+
validation: {
|
|
2758
|
+
required: true,
|
|
2759
|
+
max: 24
|
|
2760
|
+
},
|
|
2761
|
+
initialValue: "Choose cookies"
|
|
2762
|
+
},
|
|
2763
|
+
{
|
|
2764
|
+
name: "save",
|
|
2765
|
+
title: "Save preferences",
|
|
2766
|
+
type: "string",
|
|
2767
|
+
localized: true,
|
|
2768
|
+
validation: {
|
|
2769
|
+
required: true,
|
|
2770
|
+
max: 24
|
|
2771
|
+
},
|
|
2772
|
+
initialValue: "Save my choices"
|
|
2773
|
+
}
|
|
2774
|
+
]
|
|
2775
|
+
},
|
|
2776
|
+
{
|
|
2777
|
+
name: "categories",
|
|
2778
|
+
title: "Categories",
|
|
2779
|
+
type: "array",
|
|
2780
|
+
group: "categories",
|
|
2781
|
+
itemNoun: "category",
|
|
2782
|
+
validation: {
|
|
2783
|
+
min: 1,
|
|
2784
|
+
max: 6
|
|
2785
|
+
},
|
|
2786
|
+
of: [{
|
|
2787
|
+
name: "category",
|
|
2788
|
+
title: "Category",
|
|
2789
|
+
type: "object",
|
|
2790
|
+
icon: "toggle-right",
|
|
2791
|
+
preview: {
|
|
2792
|
+
title: "label",
|
|
2793
|
+
subtitle: "key",
|
|
2794
|
+
fallback: "Untitled category"
|
|
2795
|
+
},
|
|
2796
|
+
fields: [
|
|
2797
|
+
{
|
|
2798
|
+
name: "key",
|
|
2799
|
+
title: "Key",
|
|
2800
|
+
type: "string",
|
|
2801
|
+
access: { edit: "schema.manage" },
|
|
2802
|
+
lockReason: "Scripts check this key before they load.",
|
|
2803
|
+
validation: {
|
|
2804
|
+
required: true,
|
|
2805
|
+
unique: true,
|
|
2806
|
+
pattern: "^[a-z][a-z_]*$",
|
|
2807
|
+
patternMessage: "Use lowercase letters and underscores, like analytics_storage."
|
|
2808
|
+
}
|
|
2809
|
+
},
|
|
2810
|
+
{
|
|
2811
|
+
name: "label",
|
|
2812
|
+
title: "Label",
|
|
2813
|
+
type: "string",
|
|
2814
|
+
localized: true,
|
|
2815
|
+
validation: {
|
|
2816
|
+
required: true,
|
|
2817
|
+
max: 32
|
|
2818
|
+
}
|
|
2819
|
+
},
|
|
2820
|
+
{
|
|
2821
|
+
name: "description",
|
|
2822
|
+
title: "What it covers",
|
|
2823
|
+
type: "text",
|
|
2824
|
+
rows: 2,
|
|
2825
|
+
localized: true,
|
|
2826
|
+
validation: {
|
|
2827
|
+
required: true,
|
|
2828
|
+
recommendedMax: 200
|
|
2829
|
+
}
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
name: "isRequired",
|
|
2833
|
+
title: "Always on",
|
|
2834
|
+
type: "boolean",
|
|
2835
|
+
description: "Visitors can't switch it off."
|
|
2836
|
+
},
|
|
2837
|
+
{
|
|
2838
|
+
name: "isOnByDefault",
|
|
2839
|
+
title: "On by default",
|
|
2840
|
+
type: "boolean",
|
|
2841
|
+
hidden: {
|
|
2842
|
+
field: "isRequired",
|
|
2843
|
+
equals: false
|
|
2844
|
+
},
|
|
2845
|
+
description: "Must stay off in the EU and UK for anything but essential cookies."
|
|
2846
|
+
}
|
|
2847
|
+
]
|
|
2848
|
+
}]
|
|
2849
|
+
},
|
|
2850
|
+
{
|
|
2851
|
+
name: "showTo",
|
|
2852
|
+
title: "Show the banner to",
|
|
2853
|
+
type: "select",
|
|
2854
|
+
group: "settings",
|
|
2855
|
+
layout: "radio",
|
|
2856
|
+
initialValue: "everyone",
|
|
2857
|
+
options: [{
|
|
2858
|
+
value: "everyone",
|
|
2859
|
+
label: "Everyone"
|
|
2860
|
+
}, {
|
|
2861
|
+
value: "regulated",
|
|
2862
|
+
label: "Visitors in regions that require consent",
|
|
2863
|
+
description: "EU, UK, Switzerland, Brazil, California"
|
|
2864
|
+
}]
|
|
2865
|
+
},
|
|
2866
|
+
{
|
|
2867
|
+
name: "useGpcSignal",
|
|
2868
|
+
title: "Honor Global Privacy Control",
|
|
2869
|
+
type: "boolean",
|
|
2870
|
+
group: "settings",
|
|
2871
|
+
initialValue: true,
|
|
2872
|
+
description: "Browsers that send GPC start with optional cookies off."
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
name: "renewMonths",
|
|
2876
|
+
title: "Ask again after",
|
|
2877
|
+
type: "number",
|
|
2878
|
+
group: "settings",
|
|
2879
|
+
numeric: {
|
|
2880
|
+
min: 1,
|
|
2881
|
+
max: 13,
|
|
2882
|
+
step: 1,
|
|
2883
|
+
unit: "months"
|
|
2884
|
+
},
|
|
2885
|
+
initialValue: 12,
|
|
2886
|
+
description: "EU regulators expect 13 months at most."
|
|
2887
|
+
}
|
|
2888
|
+
]
|
|
2889
|
+
},
|
|
2890
|
+
value: {
|
|
2891
|
+
_variant: "bar",
|
|
2892
|
+
title: "Cookies, briefly",
|
|
2893
|
+
body: "We use essential cookies to run this site and, if you agree, analytics to see which stories help rivers most. We never sell your data.",
|
|
2894
|
+
policyLink: page("/privacy/cookies", "Cookie policy"),
|
|
2895
|
+
labels: {
|
|
2896
|
+
acceptAll: "Accept all",
|
|
2897
|
+
rejectAll: "Reject all",
|
|
2898
|
+
customize: "Choose cookies",
|
|
2899
|
+
save: "Save my choices"
|
|
2900
|
+
},
|
|
2901
|
+
categories: [
|
|
2902
|
+
{
|
|
2903
|
+
_key: "c1",
|
|
2904
|
+
key: "essential",
|
|
2905
|
+
label: "Essential",
|
|
2906
|
+
description: "Keeps you signed in and remembers this choice. Always on.",
|
|
2907
|
+
isRequired: true
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
_key: "c2",
|
|
2911
|
+
key: "analytics",
|
|
2912
|
+
label: "Analytics",
|
|
2913
|
+
description: "Plausible and Google Analytics, to count visits and see which pages people read.",
|
|
2914
|
+
isRequired: false,
|
|
2915
|
+
isOnByDefault: false
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
_key: "c3",
|
|
2919
|
+
key: "marketing",
|
|
2920
|
+
label: "Marketing",
|
|
2921
|
+
description: "Meta Pixel, to show our campaigns to people who visited — and to measure if they work.",
|
|
2922
|
+
isRequired: false,
|
|
2923
|
+
isOnByDefault: false
|
|
2924
|
+
}
|
|
2925
|
+
],
|
|
2926
|
+
showTo: "everyone",
|
|
2927
|
+
useGpcSignal: true,
|
|
2928
|
+
renewMonths: 12
|
|
2929
|
+
},
|
|
2930
|
+
notes: {
|
|
2931
|
+
why: "Consent is legal copy plus a contract with the site's scripts, so the two halves are modelled differently: categories are an array of objects whose `key` is developer-locked (scripts gate on it) while labels and descriptions are localized and editable. 'Accept all' and 'Reject all' are STRINGS, not buttons — our `button` type is label + link and these don't navigate. Layout (bar/box/modal) is a block variant, not a field, because it changes the component not the copy. It's a global: one consent UI for the whole site.",
|
|
2932
|
+
sanity: "Singleton `cookieBanner` { title, body: text, policyLink, labels: object { acceptAll, rejectAll, customize, save: string }, categories: array<object { key: string (readOnly for non-admins), label, description, required, defaultOn }> }.",
|
|
2933
|
+
htmlSignals: [
|
|
2934
|
+
"A fixed bottom/corner element or role=dialog with the words cookie/consent/privacy and 2–3 buttons (Accept / Reject / Preferences)",
|
|
2935
|
+
"A second hidden panel with toggles (<input type=\"checkbox\" role=\"switch\">) per category",
|
|
2936
|
+
"Scripts with type=\"text/plain\" and data-category=\"analytics\" (a consent manager pattern)",
|
|
2937
|
+
"A cookie like cookie_consent / cc_cookie / OptanonConsent"
|
|
2938
|
+
],
|
|
2939
|
+
mcp: "Create ONE global `cookieBanner`. Banner heading/body → title/body; the policy <a> → policyLink. Button TEXTS → labels.* (never buttons). Each toggle in the preferences panel → categories[] { key from data-category/name attribute, label, description, required: disabled+checked toggle }. If the site uses a third-party CMP (OneTrust, Cookiebot) via a script tag, report it instead of modelling — that content lives in the vendor's dashboard.",
|
|
2940
|
+
pitfalls: ["Pre-ticked optional categories are unlawful in the EU — the converter must set isOnByDefault: false and report any source that pre-ticks.", "A 'Reject all' hidden behind 'Preferences' is a dark pattern; flag it, don't replicate it."]
|
|
2941
|
+
}
|
|
2942
|
+
},
|
|
2943
|
+
{
|
|
2944
|
+
id: "breadcrumb-config",
|
|
2945
|
+
previewLayout: "banner",
|
|
2946
|
+
label: "Breadcrumb trail",
|
|
2947
|
+
proves: "A mostly-automatic block: trail from the page tree, manual override only when needed · native JSON-LD",
|
|
2948
|
+
category: "Navigation & chrome",
|
|
2949
|
+
tags: [
|
|
2950
|
+
"conditional",
|
|
2951
|
+
"seo-structured",
|
|
2952
|
+
"a11y",
|
|
2953
|
+
"references",
|
|
2954
|
+
"repeater"
|
|
2955
|
+
],
|
|
2956
|
+
brand: "Caldera",
|
|
2957
|
+
schema: {
|
|
2958
|
+
name: "breadcrumbs",
|
|
2959
|
+
title: "Breadcrumbs",
|
|
2960
|
+
kind: "block",
|
|
2961
|
+
icon: "chevrons-right",
|
|
2962
|
+
category: "Navigation & chrome",
|
|
2963
|
+
description: "The trail of links back up to home. Built from where the page sits.",
|
|
2964
|
+
preview: {
|
|
2965
|
+
title: "source",
|
|
2966
|
+
subtitle: "homeLabel"
|
|
2967
|
+
},
|
|
2968
|
+
groups: DESIGN_GROUPS,
|
|
2969
|
+
fields: [
|
|
2970
|
+
{
|
|
2971
|
+
name: "source",
|
|
2972
|
+
title: "Trail comes from",
|
|
2973
|
+
type: "select",
|
|
2974
|
+
layout: "cards",
|
|
2975
|
+
initialValue: "auto",
|
|
2976
|
+
options: [{
|
|
2977
|
+
value: "auto",
|
|
2978
|
+
label: "Where the page sits",
|
|
2979
|
+
icon: "sitemap",
|
|
2980
|
+
description: "Folders and parent pages, updated on its own"
|
|
2981
|
+
}, {
|
|
2982
|
+
value: "manual",
|
|
2983
|
+
label: "Written here",
|
|
2984
|
+
icon: "pencil",
|
|
2985
|
+
description: "For pages whose address doesn't match the trail"
|
|
2986
|
+
}]
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
name: "parent",
|
|
2990
|
+
title: "Treat as a child of",
|
|
2991
|
+
type: "reference",
|
|
2992
|
+
reference: { to: ["page"] },
|
|
2993
|
+
hidden: {
|
|
2994
|
+
field: "source",
|
|
2995
|
+
equals: "auto"
|
|
2996
|
+
}
|
|
2997
|
+
},
|
|
2998
|
+
{
|
|
2999
|
+
name: "crumbs",
|
|
3000
|
+
title: "Crumbs",
|
|
3001
|
+
type: "array",
|
|
3002
|
+
itemNoun: "crumb",
|
|
3003
|
+
hidden: {
|
|
3004
|
+
field: "source",
|
|
3005
|
+
equals: "manual"
|
|
3006
|
+
},
|
|
3007
|
+
validation: {
|
|
3008
|
+
min: 1,
|
|
3009
|
+
max: 5
|
|
3010
|
+
},
|
|
3011
|
+
description: "Home and the current page are added for you.",
|
|
3012
|
+
of: [navLink(32)]
|
|
3013
|
+
},
|
|
3014
|
+
{
|
|
3015
|
+
name: "homeLabel",
|
|
3016
|
+
title: "Home label",
|
|
3017
|
+
type: "string",
|
|
3018
|
+
validation: { max: 16 },
|
|
3019
|
+
initialValue: "Home",
|
|
3020
|
+
description: "Leave empty to show a house icon."
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
name: "showCurrent",
|
|
3024
|
+
title: "Show the current page",
|
|
3025
|
+
type: "boolean",
|
|
3026
|
+
initialValue: true
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
name: "separator",
|
|
3030
|
+
title: "Separator",
|
|
3031
|
+
type: "select",
|
|
3032
|
+
group: "design",
|
|
3033
|
+
layout: "segmented",
|
|
3034
|
+
initialValue: "chevron",
|
|
3035
|
+
options: [
|
|
3036
|
+
{
|
|
3037
|
+
value: "chevron",
|
|
3038
|
+
label: "›"
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
value: "slash",
|
|
3042
|
+
label: "/"
|
|
3043
|
+
},
|
|
3044
|
+
{
|
|
3045
|
+
value: "dot",
|
|
3046
|
+
label: "·"
|
|
3047
|
+
}
|
|
3048
|
+
]
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
name: "collapseAfter",
|
|
3052
|
+
title: "Shorten trails longer than",
|
|
3053
|
+
type: "number",
|
|
3054
|
+
group: "design",
|
|
3055
|
+
numeric: {
|
|
3056
|
+
min: 3,
|
|
3057
|
+
max: 6,
|
|
3058
|
+
step: 1,
|
|
3059
|
+
unit: "crumbs"
|
|
3060
|
+
},
|
|
3061
|
+
initialValue: 4,
|
|
3062
|
+
description: "Phones only."
|
|
3063
|
+
}
|
|
3064
|
+
]
|
|
3065
|
+
},
|
|
3066
|
+
value: {
|
|
3067
|
+
source: "auto",
|
|
3068
|
+
parent: "pg_cablefold",
|
|
3069
|
+
homeLabel: "Home",
|
|
3070
|
+
showCurrent: true,
|
|
3071
|
+
separator: "chevron",
|
|
3072
|
+
collapseAfter: 4
|
|
3073
|
+
},
|
|
3074
|
+
notes: {
|
|
3075
|
+
why: "Breadcrumbs are derived data — the right default is 'from the page tree' with zero typing, so the block is mostly settings. A manual `crumbs` array appears only when `source` is manual (campaign pages whose URL doesn't match their place in the IA). `parent` is a reference so a product can sit under its collection even when the URL is flat. The BreadcrumbList JSON-LD is emitted natively from the SAME resolved trail, so there is no schema/SEO field here — modelling one would create a second truth.",
|
|
3076
|
+
sanity: "Usually not modelled in Sanity — computed in the frontend from slugs; when overridden: `breadcrumbs: array<object { label, link: reference }>` on the page document.",
|
|
3077
|
+
htmlSignals: [
|
|
3078
|
+
"<nav aria-label=\"Breadcrumb\"> with an <ol> of <a> separated by › or /",
|
|
3079
|
+
"The last item has aria-current=\"page\" and isn't a link",
|
|
3080
|
+
"JSON-LD script with \"@type\": \"BreadcrumbList\" (native — read it to verify, don't model it)",
|
|
3081
|
+
"Appears directly above the H1 on inner pages, absent on home"
|
|
3082
|
+
],
|
|
3083
|
+
mcp: "If every crumb's href is a prefix of the current URL path → source: 'auto' and model nothing else. Otherwise → source: 'manual', crumbs = the middle crumbs (drop Home and the current page). Read separator glyph → separator. Never create JSON-LD fields; verify the platform output matches the source's BreadcrumbList instead.",
|
|
3084
|
+
pitfalls: ["Importing the full trail per page as manual data — it rots the first time a page moves. Prefer auto.", "A breadcrumb that's really a 'Back to results' link is navigation state, not a trail."]
|
|
3085
|
+
}
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
id: "footer-multi-column",
|
|
3089
|
+
previewLayout: "footer",
|
|
3090
|
+
label: "Multi-column footer",
|
|
3091
|
+
proves: "Link groups · newsletter as a form reference · social profiles · legal row with a year token · payment badges as a fixed set",
|
|
3092
|
+
category: "Navigation & chrome",
|
|
3093
|
+
tags: [
|
|
3094
|
+
"global-synced",
|
|
3095
|
+
"repeater",
|
|
3096
|
+
"references",
|
|
3097
|
+
"form",
|
|
3098
|
+
"validation",
|
|
3099
|
+
"commerce"
|
|
3100
|
+
],
|
|
3101
|
+
brand: "Caldera",
|
|
3102
|
+
syncedFrom: "Footer",
|
|
3103
|
+
schema: {
|
|
3104
|
+
name: "footer",
|
|
3105
|
+
title: "Footer",
|
|
3106
|
+
kind: "global",
|
|
3107
|
+
icon: "layout-bottombar",
|
|
3108
|
+
category: "Navigation & chrome",
|
|
3109
|
+
description: "Link columns, newsletter sign-up, social links and the legal row. Synced across every page.",
|
|
3110
|
+
variants: [{
|
|
3111
|
+
id: "columns",
|
|
3112
|
+
name: "Columns",
|
|
3113
|
+
description: "Brand and newsletter left, link columns right"
|
|
3114
|
+
}, {
|
|
3115
|
+
id: "stacked",
|
|
3116
|
+
name: "Newsletter on top",
|
|
3117
|
+
description: "A full-width sign-up band above the columns"
|
|
3118
|
+
}],
|
|
3119
|
+
preview: {
|
|
3120
|
+
title: "tagline",
|
|
3121
|
+
media: "logo"
|
|
3122
|
+
},
|
|
3123
|
+
groups: [
|
|
3124
|
+
{
|
|
3125
|
+
name: "content",
|
|
3126
|
+
title: "Links",
|
|
3127
|
+
default: true
|
|
3128
|
+
},
|
|
3129
|
+
{
|
|
3130
|
+
name: "newsletter",
|
|
3131
|
+
title: "Newsletter"
|
|
3132
|
+
},
|
|
3133
|
+
{
|
|
3134
|
+
name: "legal",
|
|
3135
|
+
title: "Legal"
|
|
3136
|
+
},
|
|
3137
|
+
{
|
|
3138
|
+
name: "design",
|
|
3139
|
+
title: "Design"
|
|
3140
|
+
}
|
|
3141
|
+
],
|
|
3142
|
+
fields: [
|
|
3143
|
+
{
|
|
3144
|
+
name: "logo",
|
|
3145
|
+
title: "Logo",
|
|
3146
|
+
type: "image"
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
name: "tagline",
|
|
3150
|
+
title: "Tagline",
|
|
3151
|
+
type: "text",
|
|
3152
|
+
rows: 2,
|
|
3153
|
+
validation: { recommendedMax: 120 },
|
|
3154
|
+
ai: true
|
|
3155
|
+
},
|
|
3156
|
+
{
|
|
3157
|
+
name: "groups",
|
|
3158
|
+
title: "Link groups",
|
|
3159
|
+
type: "array",
|
|
3160
|
+
itemNoun: "group",
|
|
3161
|
+
validation: {
|
|
3162
|
+
min: 1,
|
|
3163
|
+
max: 5
|
|
3164
|
+
},
|
|
3165
|
+
of: [linkGroup(8)]
|
|
3166
|
+
},
|
|
3167
|
+
{
|
|
3168
|
+
name: "socialLinks",
|
|
3169
|
+
title: "Social profiles",
|
|
3170
|
+
type: "array",
|
|
3171
|
+
itemNoun: "profile",
|
|
3172
|
+
validation: { max: 6 },
|
|
3173
|
+
of: [socialItem]
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
name: "newsletterHeading",
|
|
3177
|
+
title: "Heading",
|
|
3178
|
+
type: "string",
|
|
3179
|
+
group: "newsletter",
|
|
3180
|
+
validation: { max: 48 }
|
|
3181
|
+
},
|
|
3182
|
+
{
|
|
3183
|
+
name: "newsletterBody",
|
|
3184
|
+
title: "Body",
|
|
3185
|
+
type: "text",
|
|
3186
|
+
group: "newsletter",
|
|
3187
|
+
rows: 2,
|
|
3188
|
+
validation: { recommendedMax: 140 }
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
name: "newsletterForm",
|
|
3192
|
+
title: "Form",
|
|
3193
|
+
type: "reference",
|
|
3194
|
+
group: "newsletter",
|
|
3195
|
+
reference: { to: ["form"] }
|
|
3196
|
+
},
|
|
3197
|
+
{
|
|
3198
|
+
name: "legalLinks",
|
|
3199
|
+
title: "Legal links",
|
|
3200
|
+
type: "array",
|
|
3201
|
+
group: "legal",
|
|
3202
|
+
itemNoun: "link",
|
|
3203
|
+
validation: { max: 6 },
|
|
3204
|
+
of: [navLink(28)]
|
|
3205
|
+
},
|
|
3206
|
+
{
|
|
3207
|
+
name: "copyright",
|
|
3208
|
+
title: "Copyright line",
|
|
3209
|
+
type: "string",
|
|
3210
|
+
group: "legal",
|
|
3211
|
+
validation: { max: 80 },
|
|
3212
|
+
description: "{year} becomes the current year.",
|
|
3213
|
+
placeholder: "© {year} Caldera Cookware Co."
|
|
3214
|
+
},
|
|
3215
|
+
{
|
|
3216
|
+
name: "payments",
|
|
3217
|
+
title: "Payment badges",
|
|
3218
|
+
type: "multiselect",
|
|
3219
|
+
group: "legal",
|
|
3220
|
+
options: [
|
|
3221
|
+
{
|
|
3222
|
+
value: "visa",
|
|
3223
|
+
label: "Visa"
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
value: "mastercard",
|
|
3227
|
+
label: "Mastercard"
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
value: "amex",
|
|
3231
|
+
label: "American Express"
|
|
3232
|
+
},
|
|
3233
|
+
{
|
|
3234
|
+
value: "apple-pay",
|
|
3235
|
+
label: "Apple Pay"
|
|
3236
|
+
},
|
|
3237
|
+
{
|
|
3238
|
+
value: "google-pay",
|
|
3239
|
+
label: "Google Pay"
|
|
3240
|
+
},
|
|
3241
|
+
{
|
|
3242
|
+
value: "shop-pay",
|
|
3243
|
+
label: "Shop Pay"
|
|
3244
|
+
},
|
|
3245
|
+
{
|
|
3246
|
+
value: "paypal",
|
|
3247
|
+
label: "PayPal"
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
value: "klarna",
|
|
3251
|
+
label: "Klarna"
|
|
3252
|
+
}
|
|
3253
|
+
]
|
|
3254
|
+
},
|
|
3255
|
+
themeField
|
|
3256
|
+
]
|
|
3257
|
+
},
|
|
3258
|
+
value: {
|
|
3259
|
+
_variant: "columns",
|
|
3260
|
+
logo: pic("caldera-mark-light", "Caldera", 240, 64),
|
|
3261
|
+
tagline: "Cookware built for the open fire. Forged in Bend, Oregon, and guaranteed for life.",
|
|
3262
|
+
groups: [
|
|
3263
|
+
{
|
|
3264
|
+
_key: "g1",
|
|
3265
|
+
title: "Shop",
|
|
3266
|
+
links: [
|
|
3267
|
+
{
|
|
3268
|
+
_key: "a1",
|
|
3269
|
+
label: "Skillets",
|
|
3270
|
+
link: page("/collections/skillets", "Skillets")
|
|
3271
|
+
},
|
|
3272
|
+
{
|
|
3273
|
+
_key: "a2",
|
|
3274
|
+
label: "Dutch ovens",
|
|
3275
|
+
link: page("/collections/dutch-ovens", "Dutch ovens")
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
_key: "a3",
|
|
3279
|
+
label: "Grills",
|
|
3280
|
+
link: page("/collections/grills", "Grills")
|
|
3281
|
+
},
|
|
3282
|
+
{
|
|
3283
|
+
_key: "a4",
|
|
3284
|
+
label: "Gift cards",
|
|
3285
|
+
link: page("/gift-cards", "Gift cards")
|
|
3286
|
+
}
|
|
3287
|
+
]
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
_key: "g2",
|
|
3291
|
+
title: "Help",
|
|
3292
|
+
links: [
|
|
3293
|
+
{
|
|
3294
|
+
_key: "b1",
|
|
3295
|
+
label: "Shipping & returns",
|
|
3296
|
+
link: page("/shipping", "Shipping & returns")
|
|
3297
|
+
},
|
|
3298
|
+
{
|
|
3299
|
+
_key: "b2",
|
|
3300
|
+
label: "Lifetime warranty",
|
|
3301
|
+
link: page("/warranty", "Warranty")
|
|
3302
|
+
},
|
|
3303
|
+
{
|
|
3304
|
+
_key: "b3",
|
|
3305
|
+
label: "Care guides",
|
|
3306
|
+
link: page("/journal/cast-iron-care", "Cast iron care")
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
_key: "b4",
|
|
3310
|
+
label: "Contact",
|
|
3311
|
+
link: page("/contact", "Contact")
|
|
3312
|
+
}
|
|
3313
|
+
]
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
_key: "g3",
|
|
3317
|
+
title: "Company",
|
|
3318
|
+
links: [
|
|
3319
|
+
{
|
|
3320
|
+
_key: "c1",
|
|
3321
|
+
label: "About",
|
|
3322
|
+
link: page("/about", "About")
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
_key: "c2",
|
|
3326
|
+
label: "Journal",
|
|
3327
|
+
link: page("/journal", "Journal")
|
|
3328
|
+
},
|
|
3329
|
+
{
|
|
3330
|
+
_key: "c3",
|
|
3331
|
+
label: "Wholesale",
|
|
3332
|
+
link: page("/wholesale", "Wholesale pricing")
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
_key: "c4",
|
|
3336
|
+
label: "Careers",
|
|
3337
|
+
link: page("/careers", "Careers")
|
|
3338
|
+
}
|
|
3339
|
+
]
|
|
3340
|
+
}
|
|
3341
|
+
],
|
|
3342
|
+
socialLinks: [
|
|
3343
|
+
{
|
|
3344
|
+
_key: "s1",
|
|
3345
|
+
platform: "instagram",
|
|
3346
|
+
url: "https://instagram.com/caldera.cookware"
|
|
3347
|
+
},
|
|
3348
|
+
{
|
|
3349
|
+
_key: "s2",
|
|
3350
|
+
platform: "youtube",
|
|
3351
|
+
url: "https://youtube.com/@calderacookware"
|
|
3352
|
+
},
|
|
3353
|
+
{
|
|
3354
|
+
_key: "s3",
|
|
3355
|
+
platform: "pinterest",
|
|
3356
|
+
url: "https://pinterest.com/calderacookware"
|
|
3357
|
+
}
|
|
3358
|
+
],
|
|
3359
|
+
newsletterHeading: "Recipes from the fire, monthly",
|
|
3360
|
+
newsletterBody: "One email a month: a new recipe, a care tip and first word on launches. No spam.",
|
|
3361
|
+
newsletterForm: "form_newsletter",
|
|
3362
|
+
legalLinks: [
|
|
3363
|
+
{
|
|
3364
|
+
_key: "l1",
|
|
3365
|
+
label: "Privacy",
|
|
3366
|
+
link: page("/privacy", "Privacy policy")
|
|
3367
|
+
},
|
|
3368
|
+
{
|
|
3369
|
+
_key: "l2",
|
|
3370
|
+
label: "Terms",
|
|
3371
|
+
link: page("/terms", "Terms of service")
|
|
3372
|
+
},
|
|
3373
|
+
{
|
|
3374
|
+
_key: "l3",
|
|
3375
|
+
label: "Accessibility",
|
|
3376
|
+
link: page("/accessibility", "Accessibility")
|
|
3377
|
+
}
|
|
3378
|
+
],
|
|
3379
|
+
copyright: "© {year} Caldera Cookware Co.",
|
|
3380
|
+
payments: [
|
|
3381
|
+
"visa",
|
|
3382
|
+
"mastercard",
|
|
3383
|
+
"amex",
|
|
3384
|
+
"apple-pay",
|
|
3385
|
+
"shop-pay"
|
|
3386
|
+
],
|
|
3387
|
+
theme: "dark"
|
|
3388
|
+
},
|
|
3389
|
+
notes: {
|
|
3390
|
+
why: "One global, grouped by what people change together: Links, Newsletter, Legal. Link groups are an array of objects each holding an array of links — two levels, so the converter never flattens columns into one list. The newsletter is a REFERENCE to a form in Forms: fields, consent text, double opt-in and the destination (Mailchimp, Klaviyo) live in Forms, so the footer can't silently drop the consent checkbox. Payment badges are a multiselect from a fixed set because the renderer owns the official logos — authors pick, never upload. `{year}` keeps the copyright from going stale every January.",
|
|
3391
|
+
sanity: "Singleton `footer` { tagline, groups: array<object { title, links: array<link> }>, social: array<object { platform, url }>, newsletter: object { heading, body, form: reference<form> }, legalLinks: array<link>, copyright: string, payments: array<string>(list) }.",
|
|
3392
|
+
htmlSignals: [
|
|
3393
|
+
"<footer> (role=contentinfo) with 3–5 lists of links, each under a small heading",
|
|
3394
|
+
"A newsletter <form> with a single email input and a submit button",
|
|
3395
|
+
"A row of social icons linking to instagram.com / youtube.com etc.",
|
|
3396
|
+
"A bottom row with © year, Privacy/Terms links and a strip of card logos (svg titled Visa, Mastercard…)"
|
|
3397
|
+
],
|
|
3398
|
+
mcp: "ONE global `footer`. Each heading + <ul> → groups[] { title, links }. Social anchors → socialLinks[] { platform from the hostname, url }. A form with one email input → create/reuse a newsletter form and set newsletterForm to its id; its heading/paragraph → newsletterHeading/Body. © text → copyright with the 4-digit year replaced by {year}. Card logo <svg>/<img> titles → payments (unknown brands → report). Never model the form's inputs or hidden list ids in the footer.",
|
|
3399
|
+
pitfalls: ["Flattening columns into one long links array loses the headings — keep groups.", "Uploading payment logos as images: they go stale and break brand rules. Use the fixed set."]
|
|
3400
|
+
}
|
|
3401
|
+
},
|
|
3402
|
+
{
|
|
3403
|
+
id: "footer-minimal",
|
|
3404
|
+
previewLayout: "footer",
|
|
3405
|
+
label: "Minimal footer",
|
|
3406
|
+
proves: "The smallest useful global: one line of links, an email, socials, the year token — nothing invented",
|
|
3407
|
+
category: "Navigation & chrome",
|
|
3408
|
+
tags: [
|
|
3409
|
+
"global-synced",
|
|
3410
|
+
"repeater",
|
|
3411
|
+
"validation"
|
|
3412
|
+
],
|
|
3413
|
+
brand: "Northbound Studio",
|
|
3414
|
+
syncedFrom: "Footer",
|
|
3415
|
+
schema: {
|
|
3416
|
+
name: "compactFooter",
|
|
3417
|
+
title: "Compact footer",
|
|
3418
|
+
kind: "global",
|
|
3419
|
+
icon: "layout-bottombar",
|
|
3420
|
+
category: "Navigation & chrome",
|
|
3421
|
+
description: "One row: a line of links, a contact email, social icons and the copyright.",
|
|
3422
|
+
preview: { title: "copyright" },
|
|
3423
|
+
groups: DESIGN_GROUPS,
|
|
3424
|
+
fields: [
|
|
3425
|
+
{
|
|
3426
|
+
name: "statement",
|
|
3427
|
+
title: "Sign-off",
|
|
3428
|
+
type: "string",
|
|
3429
|
+
validation: { max: 60 },
|
|
3430
|
+
ai: true
|
|
3431
|
+
},
|
|
3432
|
+
{
|
|
3433
|
+
name: "email",
|
|
3434
|
+
title: "Contact email",
|
|
3435
|
+
type: "link"
|
|
3436
|
+
},
|
|
3437
|
+
{
|
|
3438
|
+
name: "links",
|
|
3439
|
+
title: "Links",
|
|
3440
|
+
type: "array",
|
|
3441
|
+
itemNoun: "link",
|
|
3442
|
+
validation: { max: 5 },
|
|
3443
|
+
of: [navLink(20)]
|
|
3444
|
+
},
|
|
3445
|
+
{
|
|
3446
|
+
name: "socialLinks",
|
|
3447
|
+
title: "Social profiles",
|
|
3448
|
+
type: "array",
|
|
3449
|
+
itemNoun: "profile",
|
|
3450
|
+
validation: { max: 4 },
|
|
3451
|
+
of: [socialItem]
|
|
3452
|
+
},
|
|
3453
|
+
{
|
|
3454
|
+
name: "copyright",
|
|
3455
|
+
title: "Copyright line",
|
|
3456
|
+
type: "string",
|
|
3457
|
+
validation: { max: 60 },
|
|
3458
|
+
description: "{year} becomes the current year."
|
|
3459
|
+
},
|
|
3460
|
+
{
|
|
3461
|
+
name: "alignment",
|
|
3462
|
+
title: "Alignment",
|
|
3463
|
+
type: "select",
|
|
3464
|
+
group: "design",
|
|
3465
|
+
layout: "segmented",
|
|
3466
|
+
initialValue: "between",
|
|
3467
|
+
options: [{
|
|
3468
|
+
value: "between",
|
|
3469
|
+
label: "Spread"
|
|
3470
|
+
}, {
|
|
3471
|
+
value: "center",
|
|
3472
|
+
label: "Centered"
|
|
3473
|
+
}]
|
|
3474
|
+
},
|
|
3475
|
+
themeField
|
|
3476
|
+
]
|
|
3477
|
+
},
|
|
3478
|
+
value: {
|
|
3479
|
+
statement: "Let's make something worth keeping.",
|
|
3480
|
+
email: {
|
|
3481
|
+
type: "email",
|
|
3482
|
+
href: "mailto:hello@northbound.studio",
|
|
3483
|
+
email: "hello@northbound.studio",
|
|
3484
|
+
subject: "New project"
|
|
3485
|
+
},
|
|
3486
|
+
links: [
|
|
3487
|
+
{
|
|
3488
|
+
_key: "l1",
|
|
3489
|
+
label: "Work",
|
|
3490
|
+
link: page("/work", "Work")
|
|
3491
|
+
},
|
|
3492
|
+
{
|
|
3493
|
+
_key: "l2",
|
|
3494
|
+
label: "Studio",
|
|
3495
|
+
link: page("/studio", "Studio")
|
|
3496
|
+
},
|
|
3497
|
+
{
|
|
3498
|
+
_key: "l3",
|
|
3499
|
+
label: "Careers",
|
|
3500
|
+
link: page("/careers", "Careers")
|
|
3501
|
+
},
|
|
3502
|
+
{
|
|
3503
|
+
_key: "l4",
|
|
3504
|
+
label: "Privacy",
|
|
3505
|
+
link: page("/privacy", "Privacy")
|
|
3506
|
+
}
|
|
3507
|
+
],
|
|
3508
|
+
socialLinks: [
|
|
3509
|
+
{
|
|
3510
|
+
_key: "s1",
|
|
3511
|
+
platform: "instagram",
|
|
3512
|
+
url: "https://instagram.com/northbound.studio"
|
|
3513
|
+
},
|
|
3514
|
+
{
|
|
3515
|
+
_key: "s2",
|
|
3516
|
+
platform: "linkedin",
|
|
3517
|
+
url: "https://linkedin.com/company/northbound-studio"
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
_key: "s3",
|
|
3521
|
+
platform: "dribbble",
|
|
3522
|
+
url: "https://dribbble.com/northbound"
|
|
3523
|
+
}
|
|
3524
|
+
],
|
|
3525
|
+
copyright: "© {year} Northbound Studio, Lisbon",
|
|
3526
|
+
alignment: "between",
|
|
3527
|
+
theme: "light"
|
|
3528
|
+
},
|
|
3529
|
+
notes: {
|
|
3530
|
+
why: "Restraint is the point: a minimal footer still has structure (links, socials, email are three different things) but no groups and no form. Email is a `link` of type email so the renderer gets mailto: plus a subject line; storing a bare string would lose that. Same `{year}` token as the big footer. It's a global so a studio with 30 case-study pages edits it once.",
|
|
3531
|
+
sanity: "Singleton `footer` { statement, email: string, links: array<link>, social: array<object>, copyright }.",
|
|
3532
|
+
htmlSignals: [
|
|
3533
|
+
"A single-row <footer> under 120px tall with a few inline links and © text",
|
|
3534
|
+
"A visible mailto: link with the address as its text",
|
|
3535
|
+
"2–4 icon-only social links (aria-label or <title> naming the network)",
|
|
3536
|
+
"Optionally a large display-type sentence above the row"
|
|
3537
|
+
],
|
|
3538
|
+
mcp: "Footer with no headings and fewer than ~8 links → `compactFooter` (else use `footer-multi-column`). mailto: anchor → email { type: 'email', email, subject from ?subject= }. Inline anchors → links; icon anchors → socialLinks by hostname. © line → copyright with the year replaced by {year}.",
|
|
3539
|
+
pitfalls: ["Don't promote a minimal footer to the multi-column model just to have one footer type — the editor gets empty groups to ignore."]
|
|
3540
|
+
}
|
|
3541
|
+
},
|
|
3542
|
+
{
|
|
3543
|
+
id: "footer-mega-locations",
|
|
3544
|
+
previewLayout: "footer",
|
|
3545
|
+
label: "Mega footer with locations + app badges",
|
|
3546
|
+
proves: "Locations → opening hours at four levels · geopoints · store badges with URL patterns · LocalBusiness data from content",
|
|
3547
|
+
category: "Navigation & chrome",
|
|
3548
|
+
tags: [
|
|
3549
|
+
"global-synced",
|
|
3550
|
+
"deep-nesting",
|
|
3551
|
+
"repeater",
|
|
3552
|
+
"seo-structured",
|
|
3553
|
+
"validation",
|
|
3554
|
+
"media-heavy"
|
|
3555
|
+
],
|
|
3556
|
+
brand: "Hearth & Ash",
|
|
3557
|
+
syncedFrom: "Footer",
|
|
3558
|
+
schema: {
|
|
3559
|
+
name: "locationsFooter",
|
|
3560
|
+
title: "Locations footer",
|
|
3561
|
+
kind: "global",
|
|
3562
|
+
icon: "layout-bottombar",
|
|
3563
|
+
category: "Navigation & chrome",
|
|
3564
|
+
description: "Every location with its hours, app download badges, link columns and the legal row.",
|
|
3565
|
+
preview: {
|
|
3566
|
+
title: "tagline",
|
|
3567
|
+
media: "logo"
|
|
3568
|
+
},
|
|
3569
|
+
groups: [
|
|
3570
|
+
{
|
|
3571
|
+
name: "content",
|
|
3572
|
+
title: "Locations",
|
|
3573
|
+
default: true
|
|
3574
|
+
},
|
|
3575
|
+
{
|
|
3576
|
+
name: "links",
|
|
3577
|
+
title: "Links & app"
|
|
3578
|
+
},
|
|
3579
|
+
{
|
|
3580
|
+
name: "design",
|
|
3581
|
+
title: "Design"
|
|
3582
|
+
}
|
|
3583
|
+
],
|
|
3584
|
+
fields: [
|
|
3585
|
+
{
|
|
3586
|
+
name: "logo",
|
|
3587
|
+
title: "Logo",
|
|
3588
|
+
type: "image",
|
|
3589
|
+
group: "links"
|
|
3590
|
+
},
|
|
3591
|
+
{
|
|
3592
|
+
name: "tagline",
|
|
3593
|
+
title: "Tagline",
|
|
3594
|
+
type: "string",
|
|
3595
|
+
group: "links",
|
|
3596
|
+
validation: { max: 80 }
|
|
3597
|
+
},
|
|
3598
|
+
{
|
|
3599
|
+
name: "locations",
|
|
3600
|
+
title: "Locations",
|
|
3601
|
+
type: "array",
|
|
3602
|
+
itemNoun: "location",
|
|
3603
|
+
validation: {
|
|
3604
|
+
min: 1,
|
|
3605
|
+
max: 8
|
|
3606
|
+
},
|
|
3607
|
+
description: "Also feeds the details search engines show.",
|
|
3608
|
+
of: [{
|
|
3609
|
+
name: "location",
|
|
3610
|
+
title: "Location",
|
|
3611
|
+
type: "object",
|
|
3612
|
+
icon: "map-pin",
|
|
3613
|
+
preview: {
|
|
3614
|
+
title: "name",
|
|
3615
|
+
subtitle: "address",
|
|
3616
|
+
media: "photo",
|
|
3617
|
+
fallback: "Untitled location"
|
|
3618
|
+
},
|
|
3619
|
+
fields: [
|
|
3620
|
+
{
|
|
3621
|
+
name: "name",
|
|
3622
|
+
title: "Name",
|
|
3623
|
+
type: "string",
|
|
3624
|
+
validation: {
|
|
3625
|
+
required: true,
|
|
3626
|
+
max: 32
|
|
3627
|
+
},
|
|
3628
|
+
placeholder: "Pearl District"
|
|
3629
|
+
},
|
|
3630
|
+
{
|
|
3631
|
+
name: "photo",
|
|
3632
|
+
title: "Photo",
|
|
3633
|
+
type: "image"
|
|
3634
|
+
},
|
|
3635
|
+
{
|
|
3636
|
+
name: "address",
|
|
3637
|
+
title: "Address",
|
|
3638
|
+
type: "text",
|
|
3639
|
+
rows: 2,
|
|
3640
|
+
validation: { required: true }
|
|
3641
|
+
},
|
|
3642
|
+
{
|
|
3643
|
+
name: "geo",
|
|
3644
|
+
title: "Map pin",
|
|
3645
|
+
type: "geopoint",
|
|
3646
|
+
description: "Drop it on the entrance. Directions use it."
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
name: "phone",
|
|
3650
|
+
title: "Phone",
|
|
3651
|
+
type: "link"
|
|
3652
|
+
},
|
|
3653
|
+
{
|
|
3654
|
+
name: "hours",
|
|
3655
|
+
title: "Opening hours",
|
|
3656
|
+
type: "array",
|
|
3657
|
+
itemNoun: "row",
|
|
3658
|
+
validation: {
|
|
3659
|
+
min: 1,
|
|
3660
|
+
max: 7
|
|
3661
|
+
},
|
|
3662
|
+
description: "One row per set of days with the same hours.",
|
|
3663
|
+
of: [{
|
|
3664
|
+
name: "hoursRow",
|
|
3665
|
+
title: "Hours",
|
|
3666
|
+
type: "object",
|
|
3667
|
+
icon: "clock",
|
|
3668
|
+
preview: {
|
|
3669
|
+
title: "days",
|
|
3670
|
+
subtitle: "opens",
|
|
3671
|
+
fallback: "Untitled row"
|
|
3672
|
+
},
|
|
3673
|
+
fields: [
|
|
3674
|
+
{
|
|
3675
|
+
name: "days",
|
|
3676
|
+
title: "Days",
|
|
3677
|
+
type: "multiselect",
|
|
3678
|
+
validation: { required: true },
|
|
3679
|
+
options: [
|
|
3680
|
+
{
|
|
3681
|
+
value: "mon",
|
|
3682
|
+
label: "Mon"
|
|
3683
|
+
},
|
|
3684
|
+
{
|
|
3685
|
+
value: "tue",
|
|
3686
|
+
label: "Tue"
|
|
3687
|
+
},
|
|
3688
|
+
{
|
|
3689
|
+
value: "wed",
|
|
3690
|
+
label: "Wed"
|
|
3691
|
+
},
|
|
3692
|
+
{
|
|
3693
|
+
value: "thu",
|
|
3694
|
+
label: "Thu"
|
|
3695
|
+
},
|
|
3696
|
+
{
|
|
3697
|
+
value: "fri",
|
|
3698
|
+
label: "Fri"
|
|
3699
|
+
},
|
|
3700
|
+
{
|
|
3701
|
+
value: "sat",
|
|
3702
|
+
label: "Sat"
|
|
3703
|
+
},
|
|
3704
|
+
{
|
|
3705
|
+
value: "sun",
|
|
3706
|
+
label: "Sun"
|
|
3707
|
+
}
|
|
3708
|
+
]
|
|
3709
|
+
},
|
|
3710
|
+
{
|
|
3711
|
+
name: "isClosed",
|
|
3712
|
+
title: "Closed",
|
|
3713
|
+
type: "boolean"
|
|
3714
|
+
},
|
|
3715
|
+
{
|
|
3716
|
+
name: "opens",
|
|
3717
|
+
title: "Opens",
|
|
3718
|
+
type: "string",
|
|
3719
|
+
hidden: {
|
|
3720
|
+
field: "isClosed",
|
|
3721
|
+
equals: false
|
|
3722
|
+
},
|
|
3723
|
+
validation: {
|
|
3724
|
+
pattern: "^([01]\\d|2[0-3]):[0-5]\\d$",
|
|
3725
|
+
patternMessage: "Use 24-hour time, like 17:00."
|
|
3726
|
+
},
|
|
3727
|
+
placeholder: "17:00"
|
|
3728
|
+
},
|
|
3729
|
+
{
|
|
3730
|
+
name: "closes",
|
|
3731
|
+
title: "Closes",
|
|
3732
|
+
type: "string",
|
|
3733
|
+
hidden: {
|
|
3734
|
+
field: "isClosed",
|
|
3735
|
+
equals: false
|
|
3736
|
+
},
|
|
3737
|
+
validation: {
|
|
3738
|
+
pattern: "^([01]\\d|2[0-3]):[0-5]\\d$",
|
|
3739
|
+
patternMessage: "Use 24-hour time, like 22:30."
|
|
3740
|
+
},
|
|
3741
|
+
placeholder: "22:30"
|
|
3742
|
+
}
|
|
3743
|
+
]
|
|
3744
|
+
}]
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
name: "reserveButton",
|
|
3748
|
+
title: "Reserve button",
|
|
3749
|
+
type: "button"
|
|
3750
|
+
}
|
|
3751
|
+
]
|
|
3752
|
+
}]
|
|
3753
|
+
},
|
|
3754
|
+
{
|
|
3755
|
+
name: "appBadges",
|
|
3756
|
+
title: "App badges",
|
|
3757
|
+
type: "array",
|
|
3758
|
+
group: "links",
|
|
3759
|
+
itemNoun: "badge",
|
|
3760
|
+
validation: { max: 2 },
|
|
3761
|
+
of: [{
|
|
3762
|
+
name: "appBadge",
|
|
3763
|
+
title: "Badge",
|
|
3764
|
+
type: "object",
|
|
3765
|
+
icon: "device-mobile",
|
|
3766
|
+
openMode: "inline",
|
|
3767
|
+
preview: {
|
|
3768
|
+
title: "store",
|
|
3769
|
+
subtitle: "url"
|
|
3770
|
+
},
|
|
3771
|
+
fields: [{
|
|
3772
|
+
name: "store",
|
|
3773
|
+
title: "Store",
|
|
3774
|
+
type: "select",
|
|
3775
|
+
validation: {
|
|
3776
|
+
required: true,
|
|
3777
|
+
unique: true
|
|
3778
|
+
},
|
|
3779
|
+
options: [{
|
|
3780
|
+
value: "app-store",
|
|
3781
|
+
label: "App Store"
|
|
3782
|
+
}, {
|
|
3783
|
+
value: "google-play",
|
|
3784
|
+
label: "Google Play"
|
|
3785
|
+
}]
|
|
3786
|
+
}, {
|
|
3787
|
+
name: "url",
|
|
3788
|
+
title: "Store address",
|
|
3789
|
+
type: "url",
|
|
3790
|
+
validation: {
|
|
3791
|
+
required: true,
|
|
3792
|
+
pattern: "^https://(apps\\.apple\\.com|play\\.google\\.com)/",
|
|
3793
|
+
patternMessage: "Use an apps.apple.com or play.google.com address, like https://apps.apple.com/app/id123."
|
|
3794
|
+
}
|
|
3795
|
+
}]
|
|
3796
|
+
}]
|
|
3797
|
+
},
|
|
3798
|
+
{
|
|
3799
|
+
name: "groups",
|
|
3800
|
+
title: "Link groups",
|
|
3801
|
+
type: "array",
|
|
3802
|
+
group: "links",
|
|
3803
|
+
itemNoun: "group",
|
|
3804
|
+
validation: { max: 3 },
|
|
3805
|
+
of: [linkGroup(6)]
|
|
3806
|
+
},
|
|
3807
|
+
{
|
|
3808
|
+
name: "socialLinks",
|
|
3809
|
+
title: "Social profiles",
|
|
3810
|
+
type: "array",
|
|
3811
|
+
group: "links",
|
|
3812
|
+
itemNoun: "profile",
|
|
3813
|
+
validation: { max: 5 },
|
|
3814
|
+
of: [socialItem]
|
|
3815
|
+
},
|
|
3816
|
+
{
|
|
3817
|
+
name: "copyright",
|
|
3818
|
+
title: "Copyright line",
|
|
3819
|
+
type: "string",
|
|
3820
|
+
group: "links",
|
|
3821
|
+
validation: { max: 80 },
|
|
3822
|
+
description: "{year} becomes the current year."
|
|
3823
|
+
},
|
|
3824
|
+
{
|
|
3825
|
+
name: "locationLayout",
|
|
3826
|
+
title: "Locations shown as",
|
|
3827
|
+
type: "select",
|
|
3828
|
+
group: "design",
|
|
3829
|
+
layout: "segmented",
|
|
3830
|
+
initialValue: "cards",
|
|
3831
|
+
options: [{
|
|
3832
|
+
value: "cards",
|
|
3833
|
+
label: "Photo cards"
|
|
3834
|
+
}, {
|
|
3835
|
+
value: "list",
|
|
3836
|
+
label: "Compact list"
|
|
3837
|
+
}]
|
|
3838
|
+
},
|
|
3839
|
+
themeField
|
|
3840
|
+
]
|
|
3841
|
+
},
|
|
3842
|
+
value: {
|
|
3843
|
+
logo: pic("hearth-ash-mark", "Hearth & Ash", 240, 64),
|
|
3844
|
+
tagline: "Wood-fired cooking in three Portland neighbourhoods.",
|
|
3845
|
+
locations: [{
|
|
3846
|
+
_key: "loc1",
|
|
3847
|
+
name: "Pearl District",
|
|
3848
|
+
photo: pic("hearth-pearl", "Long wooden bar lit by the open hearth", 1200, 900),
|
|
3849
|
+
address: "1120 NW Couch St\nPortland, OR 97209",
|
|
3850
|
+
geo: {
|
|
3851
|
+
label: "Hearth & Ash Pearl",
|
|
3852
|
+
address: "1120 NW Couch St, Portland, OR 97209",
|
|
3853
|
+
lat: 45.5236,
|
|
3854
|
+
lng: -122.6833
|
|
3855
|
+
},
|
|
3856
|
+
phone: {
|
|
3857
|
+
type: "phone",
|
|
3858
|
+
href: "tel:+15035550142",
|
|
3859
|
+
phone: "+1 503 555 0142",
|
|
3860
|
+
title: "(503) 555-0142"
|
|
3861
|
+
},
|
|
3862
|
+
hours: [
|
|
3863
|
+
{
|
|
3864
|
+
_key: "h1",
|
|
3865
|
+
days: [
|
|
3866
|
+
"tue",
|
|
3867
|
+
"wed",
|
|
3868
|
+
"thu"
|
|
3869
|
+
],
|
|
3870
|
+
isClosed: false,
|
|
3871
|
+
opens: "17:00",
|
|
3872
|
+
closes: "22:00"
|
|
3873
|
+
},
|
|
3874
|
+
{
|
|
3875
|
+
_key: "h2",
|
|
3876
|
+
days: ["fri", "sat"],
|
|
3877
|
+
isClosed: false,
|
|
3878
|
+
opens: "17:00",
|
|
3879
|
+
closes: "23:00"
|
|
3880
|
+
},
|
|
3881
|
+
{
|
|
3882
|
+
_key: "h3",
|
|
3883
|
+
days: ["sun"],
|
|
3884
|
+
isClosed: false,
|
|
3885
|
+
opens: "10:00",
|
|
3886
|
+
closes: "14:30"
|
|
3887
|
+
},
|
|
3888
|
+
{
|
|
3889
|
+
_key: "h4",
|
|
3890
|
+
days: ["mon"],
|
|
3891
|
+
isClosed: true
|
|
3892
|
+
}
|
|
3893
|
+
],
|
|
3894
|
+
reserveButton: btn("Reserve", "https://resy.com/cities/pdx/hearth-and-ash-pearl", "outline", { newTab: true })
|
|
3895
|
+
}, {
|
|
3896
|
+
_key: "loc2",
|
|
3897
|
+
name: "Alberta Arts",
|
|
3898
|
+
photo: pic("hearth-alberta", "Patio tables under string lights", 1200, 900),
|
|
3899
|
+
address: "2940 NE Alberta St\nPortland, OR 97211",
|
|
3900
|
+
geo: {
|
|
3901
|
+
label: "Hearth & Ash Alberta",
|
|
3902
|
+
address: "2940 NE Alberta St, Portland, OR 97211",
|
|
3903
|
+
lat: 45.559,
|
|
3904
|
+
lng: -122.6365
|
|
3905
|
+
},
|
|
3906
|
+
phone: {
|
|
3907
|
+
type: "phone",
|
|
3908
|
+
href: "tel:+15035550178",
|
|
3909
|
+
phone: "+1 503 555 0178",
|
|
3910
|
+
title: "(503) 555-0178"
|
|
3911
|
+
},
|
|
3912
|
+
hours: [{
|
|
3913
|
+
_key: "h5",
|
|
3914
|
+
days: [
|
|
3915
|
+
"wed",
|
|
3916
|
+
"thu",
|
|
3917
|
+
"fri",
|
|
3918
|
+
"sat",
|
|
3919
|
+
"sun"
|
|
3920
|
+
],
|
|
3921
|
+
isClosed: false,
|
|
3922
|
+
opens: "16:30",
|
|
3923
|
+
closes: "22:00"
|
|
3924
|
+
}, {
|
|
3925
|
+
_key: "h6",
|
|
3926
|
+
days: ["mon", "tue"],
|
|
3927
|
+
isClosed: true
|
|
3928
|
+
}],
|
|
3929
|
+
reserveButton: btn("Reserve", "https://resy.com/cities/pdx/hearth-and-ash-alberta", "outline", { newTab: true })
|
|
3930
|
+
}],
|
|
3931
|
+
appBadges: [{
|
|
3932
|
+
_key: "b1",
|
|
3933
|
+
store: "app-store",
|
|
3934
|
+
url: "https://apps.apple.com/us/app/hearth-ash/id6450012345"
|
|
3935
|
+
}, {
|
|
3936
|
+
_key: "b2",
|
|
3937
|
+
store: "google-play",
|
|
3938
|
+
url: "https://play.google.com/store/apps/details?id=com.hearthandash.order"
|
|
3939
|
+
}],
|
|
3940
|
+
groups: [{
|
|
3941
|
+
_key: "g1",
|
|
3942
|
+
title: "Visit",
|
|
3943
|
+
links: [
|
|
3944
|
+
{
|
|
3945
|
+
_key: "a1",
|
|
3946
|
+
label: "Menus",
|
|
3947
|
+
link: page("/menus", "Menus")
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
_key: "a2",
|
|
3951
|
+
label: "Private dining",
|
|
3952
|
+
link: page("/private-dining", "Private dining")
|
|
3953
|
+
},
|
|
3954
|
+
{
|
|
3955
|
+
_key: "a3",
|
|
3956
|
+
label: "Gift cards",
|
|
3957
|
+
link: page("/gift-cards", "Gift cards")
|
|
3958
|
+
}
|
|
3959
|
+
]
|
|
3960
|
+
}, {
|
|
3961
|
+
_key: "g2",
|
|
3962
|
+
title: "Company",
|
|
3963
|
+
links: [
|
|
3964
|
+
{
|
|
3965
|
+
_key: "a4",
|
|
3966
|
+
label: "Our story",
|
|
3967
|
+
link: page("/story", "Our story")
|
|
3968
|
+
},
|
|
3969
|
+
{
|
|
3970
|
+
_key: "a5",
|
|
3971
|
+
label: "Jobs",
|
|
3972
|
+
link: page("/jobs", "Jobs")
|
|
3973
|
+
},
|
|
3974
|
+
{
|
|
3975
|
+
_key: "a6",
|
|
3976
|
+
label: "Press",
|
|
3977
|
+
link: page("/press", "Press")
|
|
3978
|
+
}
|
|
3979
|
+
]
|
|
3980
|
+
}],
|
|
3981
|
+
socialLinks: [{
|
|
3982
|
+
_key: "s1",
|
|
3983
|
+
platform: "instagram",
|
|
3984
|
+
url: "https://instagram.com/hearthandash.pdx"
|
|
3985
|
+
}],
|
|
3986
|
+
copyright: "© {year} Hearth & Ash Restaurant Group",
|
|
3987
|
+
locationLayout: "cards",
|
|
3988
|
+
theme: "dark"
|
|
3989
|
+
},
|
|
3990
|
+
notes: {
|
|
3991
|
+
why: "Locations are the deep part: Footer › Locations › Location › Hours › Row is four drill levels, which is exactly what real multi-site restaurants and retailers need (split hours, brunch-only Sundays, closed Mondays). Hours are structured rows (days multiselect + 24-hour times), never a free-text 'Tue–Thu 5–10', because the same data feeds LocalBusiness/Restaurant JSON-LD and 'Open now' — natively, without SEO fields. `geo` is a geopoint so directions and maps never geocode a typo. App badges are a select + URL with a pattern, rendered with official artwork. If the site also has a Locations collection, this array should become `array<reference>` to it (see pitfalls).",
|
|
3992
|
+
sanity: "Singleton `footer` { locations: array<reference<location>> } where `location` is a document { name, address, geo: geopoint, phone, hours: array<object { days: array<string>, opens, closes, closed }> }; appBadges: array<object { store, url }>.",
|
|
3993
|
+
htmlSignals: [
|
|
3994
|
+
"Footer columns each with a place name, a street address, a phone link and a small hours table",
|
|
3995
|
+
"Google Maps links (maps.google.com/?q=… or /maps/place/) or embedded map iframes per location",
|
|
3996
|
+
"App Store / Google Play badge images linking to apps.apple.com and play.google.com",
|
|
3997
|
+
"JSON-LD with @type Restaurant/LocalBusiness and openingHoursSpecification"
|
|
3998
|
+
],
|
|
3999
|
+
mcp: "Per location block: name, address (keep line breaks), tel: → phone, maps link coordinates (@lat,lng or ?ll=) → geo, the hours table → hours[] (group days with identical times; 'Closed' → isClosed: true; convert 5pm → 17:00). If a /locations/* page exists per place, create a Locations collection and store references here instead. Badge <a> by hostname → appBadges { store, url }; never import the badge images. Verify the platform's JSON-LD matches the source's openingHoursSpecification.",
|
|
4000
|
+
pitfalls: [
|
|
4001
|
+
"Free-text hours ('Tue–Thu 5–10ish') can't power Open now or structured data — always normalize to rows.",
|
|
4002
|
+
"Duplicating locations in the footer AND a Locations collection — two truths; use references once a collection exists.",
|
|
4003
|
+
"Times without a zone are read in the site's time zone; a chain spanning zones needs a zone per location (not modelled)."
|
|
4004
|
+
]
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
];
|
|
4008
|
+
//#endregion
|
|
4009
|
+
export { ENTRIES };
|
|
4010
|
+
|
|
4011
|
+
//# sourceMappingURL=navigation.entries.js.map
|