@forgebuilding/hotal-ui (法语)
可重新使用的Web组件,用于预订和预订体验.
安装
npm install @forgebuild/hotal-ui
没有对等的依赖性——斯维尔特运行时间被捆绑入. 通过平地工作 <script type="module"> 标记也:
<script type="module" src="https://esm.sh/@forgebuild/hotal-ui"></script>
构成部分
<reservation-bar>
一个日期 / 来宾 / 有保留链接的取时栏, 弹出器通过 Floating UI 定位, 以及日历/列表面板 与本地的 Popover API 在一个开阔的阴影根内制成 .
<script type="module">
import '@forgebuild/hotal-ui';
const bar = document.querySelector('reservation-bar');
// Function-valued and explicitly-false-when-default-is-true props must be
// set as JS properties, not HTML attributes (attributes are strings only,
// and HTML boolean-attribute semantics can't express "false" when the
// prop's own default is true).
bar.showReserveButton = false;
bar.fetchAvailableDates = async (partySize) => {
const res = await fetch(`/api/available-dates?party_size=${partySize}`);
const { dates } = await res.json();
return dates;
};
bar.addEventListener('reservationchange', (e) => console.log(e.detail));
</script>
<reservation-bar reserve-href="/reservations" sync-with-url closed-weekdays="[0]"></reservation-bar>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
minGuests | min-guests | number | 1 | 最小可选缔约方规模 |
maxGuests | max-guests | number | 20 | 最大可选政党规模 |
defaultGuests | default-guests | number | 2 | 初始缔约方规模 |
timeSlots | time-slots (JSON阵列) | string[] | '7:00 PM'– 说吧 –'11:00 PM' 30分步骤 | 可选择的时间选项 |
defaultTime | default-time | string | '7:00 PM' | 初次时间 |
showReserveButton | show-reserve-button | boolean | true | 是否提供了后备链接 . 设定为 JS 属性以获取 false ——见上文注. |
reserveHref | reserve-href | string | '/reservations' | 基础 URL 构建的查询字符串被附加到 |
applyOnSelect | apply-on-select | boolean | !showReserveButton | 是否选择日期/客人/时间导航 reserveHref 马上 默认反向 showReserveButton (隐藏按钮使酒吧自动导航,它的历史行为); 设定明确解开两者——例如隐藏按钮但保留 applyOnSelect={false} 当主机页面同步内线预订时 reservationchange 不应该有导航. 设定为 JS 属性以获取 false。 。 。 . |
syncWithUrl | sync-with-url | boolean | false | 从 location.search 挂载 |
closedWeekdays | closed-weekdays (JSON阵列) | number[] | [] | 工作日数0太阳6=Sat)在日历中禁用 |
fetchAvailableDates | - (仅限财产) | (partySize: number) => Promise<string[]> | undefined | 可选择日历日期的驱动器 |
活动
reservationchange —— 说 CustomEvent<{ date: string; guests: number; time: string }>,在选择变更时点火.
他们
reservation-bar {
--rb-bar-bg: rgba(14, 58, 62, 0.9);
--rb-bar-border: rgba(212, 154, 60, 0.4);
--rb-text-color: #fff;
--rb-accent-bg: #d49a3c;
--rb-accent-hover-bg: #e0ac57;
--rb-trigger-hover-bg: rgba(212, 154, 60, 0.15);
--rb-panel-bg: rgba(10, 10, 12, 0.92);
--rb-radius: 2px;
}
<booking-flow>
搜索结果,一个条纹支付表,预订提交,和确认——加一个没有可用日期的空状态日历. 与 <reservation-bar>: 页镜 date编号 :guests编号 :time 输入 <booking-flow> 本身,一般来自 reservation-bar是因为 reservationchange 事件或从页面本身的 URL.
<script type="module">
import '@forgebuild/hotal-ui';
const flow = document.querySelector('booking-flow');
flow.closedWeekdays = [0];
flow.fetchAvailableDates = async (partySize) => {
const res = await fetch(`/api/available-dates?party_size=${partySize}`);
const { dates } = await res.json();
return dates;
};
flow.searchSlots = async (date, guests) => {
const res = await fetch(`/api/search?date=${date}&party_size=${guests}`);
const { slots } = await res.json();
return slots;
};
flow.stripePublishableKey = 'pk_test_...';
flow.stripeAppearance = { theme: 'stripe' };
flow.createPaymentIntent = async (date, guests) => {
const res = await fetch('/api/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date, party_size: guests }),
});
return res.json();
};
flow.book = async (details) => {
const res = await fetch('/api/book', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(details),
});
return res.status === 201 ? { ok: true } : { ok: false, status: res.status, message: 'Something went wrong.' };
};
flow.date = '2026-08-20';
flow.guests = 2;
flow.addEventListener('booking-confirmed', (e) => console.log(e.detail));
</script>
<booking-flow></booking-flow>
属性( 所有 JS- 只使用 —— 没有 HTML 属性, 因为这里的每个属性要么是动态状态, 要么是回调)
| 名称 | 类型 | 说明 |
|---|---|---|
date | string | 当前搜索日期( ISO) |
guests | number | 当前政党规模 |
preferredTime | string | 用于预选最接近可用的槽 |
closedWeekdays | number[] | 公约与 reservation-bar数字 : 0太阳6=Sat,用于空态日历 |
fetchAvailableDates | (partySize: number) => Promise<string[]> | 驱动空状态取日期器:最多14个开放日期显示为药丸(例如停留日),更多显示为日历 |
searchSlots | (date: string, guests: number) => Promise<BookingSlot[]> | 返回可用的时段 |
stripePublishableKey | string | 条形可公布的密钥 |
stripeAppearance | object | 直通史莱克家 elements({ appearance }) |
createPaymentIntent | (date: string, guests: number) => Promise<PaymentIntentResult> | 创建条纹支付意向以持有 |
book | (details: BookingDetails) => Promise<BookResult> | 提出保留 |
何时 stripePublishableKey 编号 : createPaymentIntent 未设定,付款部分未交付,没有扣卡 stripePaymentIntentId 删除 BookingDetails。 。 。 .
interface BookingSlot { time: string; availableTables: number; }
interface PaymentIntentResult { clientSecret: string; paymentIntentId: string; amountCents: number; }
interface BookingDetails {
date: string; startTime: string; timeDisplay: string; partySize: number;
contactName: string; contactEmail?: string; contactPhone?: string; notes?: string;
stripePaymentIntentId: string; clerkUserId?: string;
}
type BookResult = { ok: true } | { ok: false; status: number; message: string };
活动
booking-confirmed —— 说 CustomEvent<{ date: string; time: string; guests: number }>一旦订房成功就开枪.
他们
booking-flow {
--bf-text-color: #1a1a1a;
--bf-muted-text: rgba(0, 0, 0, 0.6);
--bf-border-color: rgba(0, 0, 0, 0.15);
--bf-accent-bg: #1a1a1a;
--bf-accent-text: #fff;
--bf-spinner-track: rgba(0, 0, 0, 0.15);
--bf-spinner-active: #1a1a1a;
}
<menu-section>
包含分类标签(动画滑动指示器)的餐厅菜单,信息网格,带有工具提示的每个项目阳性图标,以及可选的PDF-菜单对话框. 两个视觉变体: 经典的中心设计,和 variant="panel" ——以右侧为分页的眉相上方头条,全宽箱形信息网格,并相接的项卡网格,以即时分类切换为静态.
<script type="module">import '@forgebuild/hotal-ui';</script>
<menu-section
variant="panel"
title="On the menu"
eyebrow="ZUMA"
categories='[{"label":"Bites","items":[{"name":"BBQ Chicken Sliders","description":"Jerk-glazed, brioche bun","price":"$17"}]}]'
info='[{"label":"Hours","value":"12:00 pm – late"}]'
></menu-section>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
title | title | string | 'MENU' | 主要标题 |
eyebrow | eyebrow | string | '' | 二级线(标题上方为 panel,否则) |
categories | categories (杰森语) | MenuCategory[] | [] | { label, aside?, note?, items: [{ name, description?, price?, tagline?, group?, country?, allergens?, badges?: [{ label, tone?: 'accent' }], notes?: string[] }] } |
info | info (杰森语) | {label, value}[] | [] | 信息行(小时,着装码, |
allergens | allergens (杰森语) | {id, label, icon}[] | [] | 图例解决项目 allergens 图标的编号 |
pdfUrl 编号 : pdfLabel | pdf-url 编号 : pdf-label | string | —— 说 | 在对话框中打开的可选 PDF 菜单 |
headingAlign | heading-align | 'center' | 'left' | 'center' | 只有经典变体 |
layout | layout | 'stacked' | 'split' | 'stacked' | 只有经典变体 |
variant | variant | 'default' | 'panel' | 'default' | 视觉变体 |
theme | theme (杰森语) | object | {} | 色彩覆盖, 作为 CSS 自定义属性应用 |
groupFilter | group-filter | boolean | false | 按项目过滤活动类别 group (例如,在酒类名单上的国家) |
countryFilter | country-filter | boolean | false | 按项目过滤药片行 country (如区域),缩小至活动群体 |
groupFilterAllLabel 编号 : countryFilterAllLabel | group-filter-all-label 编号 : country-filter-all-label | string | 'All' | "展示一切"药丸的标签 |
footnote | footnote | string | '' | 面板下的小印数 |
headingLevel | heading-level | 1 | 2 | 3 | h1 (经典), h2(平面) | 标题级别; 在页上设置 2 有自己的 h1 |
prerendered | prerendered | boolean | false | 设定 renderMenuSection();跳过入口动议 |
槽数 : heading-title, (中文(简体) ), eyebrow, (中文(简体) ), info,以及 intro (轻-DOM内容在标题下,在标签上方).
移交
@forgebuild/hotal-ui/server 使构建时的元素变成一个声明式的阴影根,因此菜单在页面的HTML中,在脚本到达时不移动(脚本后只添加分页切换) :
import { renderMenuSection } from '@forgebuild/hotal-ui/server';
const { attributes, shadowRoot } = renderMenuSection({ title: 'Menu', headingLevel: 2, categories });
// <menu-section {...attributes}>{shadowRoot}<span slot="heading-title">Menu</span></menu-section>
attributes 未脱逃(让模板逃离)并包含 prerendered。 。 。 . title 也成为宿主的本土 title 属性,浏览器将它作为工具提示显示在整个元素上;将它自己插槽的主机 heading-title 可以保留它未设置(标签列表由标题任意命名)。 装入元素的脚本( R)@forgebuild/hotal-ui/menu-section每当诉讼; 预发内容自成一体直到那时。 设置颜色与 --ms-* cSS 中的自定义属性,而非 theme 属性 : theme 由脚本应用,所以预发菜单会从默认中闪出。 按脚本互换的页面(如Astro的客户端Router)不解析声明阴影根;做一个完整的页面负载.
这个 panel 变量预期主机页面会加载Syne和Inter字体.
<room-grid>
室型上市:过滤分页,取出入/出出行范围分表取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取取 后端-可知性——由主机解决图像URL和价格,并传递平面数据.
<script type="module">
import '@forgebuild/hotal-ui';
const grid = document.querySelector('room-grid');
// Sync dates with storage / other pages
grid.checkIn = localStorage.getItem('checkin') ?? '';
grid.addEventListener('dates-change', (e) => console.log(e.detail)); // { checkIn, checkOut }
// Pair with <island-map>
grid.addEventListener('room-hover', (e) => map.highlightPin(e.detail.slug));
grid.addEventListener('room-leave', (e) => map.unhighlightPin(e.detail.slug));
</script>
<room-grid
rooms='[{"slug":"beachfront","name":"Beach Front Room","description":"Steps from the shore.","price":1200,"image":"/img/beachfront.jpg","tag":"Beach Front","href":"/accommodations/beachfront"}]'
filter-tabs='[{"label":"Beach Front","slug":"beachfront"}]'
currency="USD"
></room-grid>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
rooms | rooms (杰森语) | RoomGridRoom[] | [] | { slug, name, description?, price?, image?, tag?, href? } |
filterTabs | filter-tabs (杰森语) | {label, slug}[] | 来源于 tag编号 | 明确标签,每张标签以弹片匹配到一个房间 |
checkIn 编号 : checkOut | check-in 编号 : check-out | string (国际标准化组织) | '' | 选定日期;确定为JS属性 |
currency | currency | string | 'USD' | 价格上的后缀 |
allLabel | all-label | string | 'All' | 第一个标签的标签 |
活动
dates-change—— 说CustomEvent<{ checkIn: string; checkOut: string }>在每个日历选择中room-hover编号 :room-leave—— 说CustomEvent<{ slug: string }>纸牌指针的出入口
期望主机页面装入 Syne 和 Inter 字体 .
<island-map>
有名牌弹出针卡的谷歌地图. 自我启动:重新使用网页上的Google Maps加载器(gmpx-api-loader, (中文(简体) ), @googlemaps/js-api-loader,另一个 island-map),并给正式的靴子本身注入 api-key 否则。 柱被隐藏到突出显示( 有弹性的流行) —— 配对 <room-grid>徘徊事件 - 或永久显示 pins-visible。 。 。 .
<script type="module">import '@forgebuild/hotal-ui';</script>
<island-map
class="h-[480px] w-full"
pins='[{"label":"Beach Front Room","sub":"From USD 1,200","lat":18.3465,"lng":-64.5905,"slug":"beachfront"}]'
center="18.352,-64.591"
zoom="14"
map-id="YOUR_CLOUD_MAP_ID"
api-key="YOUR_MAPS_KEY"
></island-map>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
pins | pins (杰森语) | IslandMapPin[] | [] | { label, sub?, lat, lng, slug? } |
center | center | string | '0,0' | "lat,lng" |
zoom | zoom | number | 14 | 初始缩放 |
mapId | map-id | string | '' | Google Cloud 映射id (样式) |
apiKey | api-key | string | '' | 仅在页面上没有地图加载器时使用 |
colorScheme | color-scheme | '' | 'DARK' | 'LIGHT' | 'FOLLOW_SYSTEM' | '' | 保留样式 map-id 设置它以 Google 的基础主题取代了云的样式 |
fitPins | fit-pins | boolean | true | 将取景口装入负载的针口 |
fitPadding 编号 : panY | fit-padding 编号 : pan-y | number | -50 编号 : -100 | 适值绑定和后适值垂直平板 |
interactionLock | interaction-lock | boolean | true | 吞下地图,直到访客点击一次 |
pinsVisible | pins-visible | boolean | false | 永久显示所有 Pin 牌 |
lazy | lazy | boolean | false | 推迟映射创建直至元素接近视图端口 |
zoomControl | zoom-control | boolean | false | 显示 +/ - 缩放按钮 |
方法
highlightPin(slug) 编号 : unhighlightPin(slug) 弹出或藏起那发子弹下注册的接头卡.
默认高度是 480px 通过 :host; 在元素上带有类或内置样式的覆盖.
<floor-plan-hero>
制定方案管理系统的平面计划room_type.floor_plan数字 : { rooms, items, w, h })作为SVG——带标签/m2,家具资产,可选建筑背景的区域矩. 默认形式是英雄覆盖面板(标题行,关闭按钮,滑动过渡,开始隐藏——用主机类定位并打开自触发器); bare 只需要把计划 嵌入一个主机型的容器。 区域悬浮释放事件,主机可以用来交换英雄图像.
<script type="module">
import '@forgebuild/hotal-ui';
const fp = document.querySelector('floor-plan-hero');
layoutButton.addEventListener('click', () => fp.toggle());
fp.addEventListener('zone-hover', (e) => { if (e.detail.img) heroImg.src = e.detail.img; });
fp.addEventListener('zone-leave', () => (heroImg.src = defaultSrc));
</script>
<floor-plan-hero
class="absolute right-[120px] bottom-[200px] w-[520px]"
plan='{"w":760,"h":480,"rooms":[{"id":"r1","type":"bedroom","label":"Bedroom","x":60,"y":60,"w":180,"h":140}],"items":[{"id":"i1","asset":"asset4","x":0,"y":0,"w":71,"h":80}]}'
></floor-plan-hero>
<!-- Chromeless, on a light surface -->
<floor-plan-hero theme="light" bare plan="..."></floor-plan-hero>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
plan | plan (杰森语) | FloorPlan | {} | { rooms: [{id, type?, label?, x, y, w, h, sqm?, imgUrl?}], items: [{id, asset, x, y, w, h}], w?, h? } |
bgUrl | bg-url | string | —— 说 | 建筑背景图像,以30%的不透明方式渲染 |
assetBase | asset-base | string | '/SVG/' | 项目资产 ${assetBase}Asset {n}.svg (资产密钥) "asset4" → Asset 4.svg(中文(简体) ) |
label 编号 : closeLabel | label 编号 : close-label | string | 'Floor plan' 编号 : 'Close ×' | 面板头复制件 |
theme | theme | 'dark' | 'light' | 'dark' | 区域/ 标签调色板 |
bare | bare | boolean | false | 计划 SVG 仅 - 没有标题、 铬 或 打开/关闭状态 |
方法与活动
open() 编号 : close() 编号 : toggle() ——面板能见度(无 bare 模式,总是可见的).
zone-hover 编号 : zone-leave —— 说 CustomEvent<{ img?: string; label?: string; type?: string }> 区域指针进入/离开. panel-close 当使用内部关闭按钮时起火.
<stay-search>
英雄可用性搜索:玻璃入站/出站吧,两个月范围日历,搜索状态回旋/tick,以及房间卡(图像,姓名,描述,停留总计)的结果倒放. 结果为: 合并对 rooms pMS室型编号的道具 ——任何不是上市房间的搜索回文被删除,所以PMS-side测试数据从未到达来宾. 卡片图像和拷贝来自主机(CMS),检索的费率.
<script type="module">
import '@forgebuild/hotal-ui';
const search = document.querySelector('stay-search');
search.checkIn = localStorage.getItem('checkin') ?? '';
search.searchStays = async (checkIn, checkOut, guests) => {
const res = await fetch(`/api/ota/availability/search?check_in=${checkIn}&check_out=${checkOut}&guests=${guests}`);
return res.json(); // [{ room_type_id, from_rate, ... }]
};
search.addEventListener('dates-change', (e) => console.log(e.detail));
</script>
<stay-search
class="block w-full max-w-[680px]"
rooms='[{"roomTypeId":"92fc4ca8-…","name":"Beach Front Room","description":"Steps from the shore.","image":"/img/beachfront.jpg","href":"/accommodations/beachfront"}]'
currency="USD"
></stay-search>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
rooms | rooms (杰森语) | StaySearchRoom[] | [] | { roomTypeId, name, description?, image?, href? } ——结果合并过滤器 |
checkIn 编号 : checkOut | check-in 编号 : check-out | string (国际标准化组织) | '' | 选定日期;确定为JS属性 |
guests | guests | number | 2 | 传到 searchStays |
currency | currency | string | 'USD' | ISO 4217 代码;卡片总计格式化于其中 |
locale | locale | string | 浏览器 | 日期、月/周名称和价格格式 |
strings | strings (杰森语) | object | 联合国 | 界面文本 : checkIn, (中文(简体) ), checkOut, (中文(简体) ), addDate, (中文(简体) ), guests, (中文(简体) ), fewerGuests, (中文(简体) ), moreGuests, (中文(简体) ), chooseDates, (中文(简体) ), previousMonth, (中文(简体) ), nextMonth, (中文(简体) ), loadError, (中文(简体) ), none, (中文(简体) ), availableOne, (中文(简体) ), availableMany, (中文(简体) ), totalOne, (中文(简体) ), totalMany (单位:千美元){n} = 夜间) |
appendDatesToHref | append-dates-to-href | boolean | true | 附录 ?check_in=…&check_out=… 切入到卡片内 |
showGuests | show-guests | boolean | false | 显示来宾步进; 更改它会重运行搜索 |
showRooms | show-rooms | boolean | false | 显示一个“客人和房间”字段:成人和儿童每间,增加/去掉房间(最多 max-rooms,默认 4) |
searchStays | - (仅限财产) | (checkIn, checkOut, guests, rooms) => Promise<StaySearchResult[]> | —— 说 | 选择完整范围时运行; 结果需要 room_type_id 财务报告和审定财务报表 from_rate。 。 。 . rooms 实值 {adults, children}[],每个房间一个 |
活动
dates-change —— 说 CustomEvent<{ checkIn: string; checkOut: string }> 每个选择.
search-results —— 说 CustomEvent<{ checkIn, checkOut, guests, count }> 在每次搜索完成之后.
期望主机页面装入 Syne 和 Inter 字体 .
<price-grid>
价格提示表:每间酒店或房间类型一行,每晚一行一列,每间牢房的夜价. 箭头会将窗口(在今天之前从未出现过),本地日期输入会跳到任何日期,每个列头会将行排序. 每个价格链接到行 href 留守一夜间?room=<id>&check_in=…&check_out=…&guests=…),或当行无时是按钮 href。在狭长的屏幕上,表格会用被钉住的名称栏向后滚动.
<price-grid currency="GBP" days="7">
<strong slot="heading">Best prices this week</strong>
</price-grid>
<script type="module">
import '@forgebuild/hotal-ui/price-grid';
const grid = document.querySelector('price-grid');
grid.rows = [{ id: '92fc4ca8-…', name: 'Beach Front Room', meta: 'Sleeps 4', href: '/book' }];
grid.fetchRates = async (from, to) => (await fetch(`/api/rates?from=${from}&to=${to}`)).json(); // [{ id, date, rate, available? }]
grid.addEventListener('rate-select', (e) => console.log(e.detail)); // { id, date, rate }
</script>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
rows | rows (杰森语) | PriceGridRow[] | [] | { id, name, meta?, href? } 显示顺序; meta 填入可选第二列 |
startDate | start-date | string (国际标准化组织) | {\fn华文楷体\fs16\1che0e0e0}今天 | 首个晚上显示; 夹住今天 |
days | days | number | 7 | 一次显示的夜晚和箭的步(1-31) |
guests | guests | number | 2 | 运入售出价格链接 |
currency | currency | string | 'USD' | ISO 4217 代码 |
locale | locale | string | 浏览器 | 日期和价格格式 |
strings | strings (杰森语) | object | 联合国 | 界面文本 : caption, (中文(简体) ), name, (中文(简体) ), meta, (中文(简体) ), from, (中文(简体) ), previous, (中文(简体) ), next, (中文(简体) ), sortBy (单位:千美元){x} =栏, unavailable, (中文(简体) ), loadError, (中文(简体) ), retry |
fetchRates | - (仅限财产) | (fromISO, toISO) => Promise<PriceGridRate[]> | —— 说 | 两个日期都包括在内;每个窗口都呼叫一次。 一个晚上,没有,没有率,或者有 available 0/假显示为不可用 . room 替换交接器中的行 id 当行集成一些东西时(当晚酒店最便宜的房间) |
活动
rate-select —— 说 CustomEvent<{ id: string; date: string; rate: number; room: string }> 当一个价格被点击时(room = 费率 room,否则行号.
window-change —— 说 CustomEvent<{ from: string; to: string }> 当窗口移动时.
槽 heading 坐在日期控件的左边。 主题 --pg-* 自定义属性( 完整列表于 llms.txt; 继承主机页面的字体.
<availability-calendar>
停留的内含范围日历:每个日期的每日夜薪(PMS) min_rate),无可用性的日期被禁用, tal-渐变范围选择。 两个月并肩在桌面上,一个在移动上.
<script type="module">
import '@forgebuild/hotal-ui';
const cal = document.querySelector('availability-calendar');
cal.fetchAvailability = async (from, to) => {
const res = await fetch(`/api/availability?room_type_id=…&from=${from}&to=${to}`);
return res.json(); // [{ date, available_rooms, min_rate }]
};
cal.addEventListener('dates-change', (e) => console.log(e.detail)); // { checkIn, checkOut }
</script>
<availability-calendar></availability-calendar>
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
checkIn 编号 : checkOut | check-in 编号 : check-out | string (国际标准化组织) | '' | 选定范围;作为 JS 属性设置 |
horizonMonths | horizon-months | number | 12 | 请求加载器提供的窗口 |
fetchAvailability | - (仅限财产) | (fromISO, toISO) => Promise<AvailabilityDay[]> | —— 说 | 因无结果而缺席的日期 |
输出 dates-change 每个选择.
<booking-card>
室型页面的预留卡:从价格头到有白/teal弹出日历的报到/出站字段(通过与装入器合同相同的可使用性-意识) <availability-calendar>),嘉宾步道,活价分解(小计,度假地费,总额),有绿色擦拭悬浮的储备CTA,精细的印花,并有收押脚.
<script type="module">
import '@forgebuild/hotal-ui';
const card = document.querySelector('booking-card');
card.fetchAvailability = async (from, to) => (await fetch(`/api/availability?…`)).json();
card.addEventListener('dates-change', (e) => console.log(e.detail));
card.addEventListener('reserve', (e) => {
const { checkIn, checkOut, guests } = e.detail;
location.href = `/book?checkin=${checkIn}&checkout=${checkOut}&guests=${guests}`;
});
</script>
<booking-card class="sticky top-[100px] block" price="3800" max-guests="4" currency="USD"></booking-card>
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
price | price | number | 0 | 夜从率;每晚按此率计价 |
maxGuests | max-guests | number | 4 | B. 宾客台阶上限 |
resortFeeRate | resort-fee-rate | number | 0.03 | 费用作为小计的一小部分 |
ratingText 编号 : finePrint | rating-text 编号 : fine-print | string | —— 说 | 页眉和页脚副本 |
conciergeTitle 编号 : conciergeSub 编号 : conciergeCtaLabel 编号 : conciergeCtaHref | 弹匣 | string | 岛礁岛默认值 | 页脚块 |
checkIn 编号 : checkOut 编号 : guests | check-in 编号 : check-out 编号 : guests | ISO / 编号 | '' 编号 : 2 | 目前停留;作为JS财产固定 |
fetchAvailability | - (仅限财产) | 见上文 | —— 说 | 弹出时禁用无法使用的日期 |
输出 dates-change, (中文(简体) ), guests-change,以及 reserve (单位:千美元){ checkIn, checkOut, guests } ——导航是主持人的工作.
<stay-booking-flow>
一个房间的停留预订页,或几个一起预订的页&rooms=<room>:<adults>:<children>,… 在 URL 中,以 selection.rooms:可选的外加步骤,然后是细节和支付,然后是确认. 读取 room, (中文(简体) ), check_in, (中文(简体) ), check_out, (中文(简体) ), guests 从 URL (或匹配属性, 胜出) 显示重新开始的链接 start-href 如果他们失踪,无效,或者 room 没有在 rooms所有价格来自您 quote 召回(callback),当来宾或外出者改变时,重新召回(被取消,最新回应获胜).
<stay-booking-flow currency="USD" start-href="/rooms"></stay-booking-flow>
<script type="module">
import '@forgebuild/hotal-ui';
const flow = document.querySelector('stay-booking-flow');
flow.rooms = [{ id: 'junior-suite', name: 'Junior Suite', image: '/img/junior.jpg', maxGuests: 3 }];
flow.extras = [{ id: 'spa-1', name: 'Couples Massage', category: 'Wellness', price: 220 }];
flow.quote = async (sel) => (await fetch('/api/quote', { method: 'POST', body: JSON.stringify(sel) })).json(); // { lines, total, currency }
flow.stripePublishableKey = 'pk_live_…';
// Server: recalculate the amount from `sel` (never trust a client total) and create the intent with
// automatic_payment_methods: { enabled: true, allow_redirects: 'never' }.
flow.createPaymentIntent = async (sel) => (await fetch('/api/stay-intent', { method: 'POST', body: JSON.stringify(sel) })).json(); // { clientSecret }
flow.submitBooking = async (payload) => (await fetch('/api/stay-book', { method: 'POST', body: JSON.stringify(payload) })).json(); // { ok: true, ref }
flow.addEventListener('booking-confirmed', (e) => console.log(e.detail)); // { ref, selection, total }
</script>
道具 : room, (中文(简体) ), checkIn编号 :check-in, (中文(简体) ), checkOut编号 :check-out, (中文(简体) ), guests, (中文(简体) ), rooms, (中文(简体) ), extras, (中文(简体) ), categories, (中文(简体) ), fallbackCategory, (中文(简体) ), currency, (中文(简体) ), startHref编号 :start-href, (中文(简体) ), quote, (中文(简体) ), createPaymentIntent, (中文(简体) ), submitBooking (只有JS属性), strings附加时 booking-checkout 通行证(stripePublishableKey, (中文(简体) ), stripeAppearance, (中文(简体) ), termsHref, (中文(简体) ), cancellationPolicy, (中文(简体) ), locations, (中文(简体) ), allowBookingForOther, (中文(简体) ), showBookingFor, (中文(简体) ), prefill, (中文(简体) ), resolveInitialLocation),并给嵌入式摘要和进步栏打上通关牌(resortName, (中文(简体) ), logoSrc, (中文(简体) ), logoHref, (中文(简体) ), secureLabel, (中文(简体) ), needHelpHref) (中文(简体) ). createPaymentIntent 也可以还原意图 total; 与引文重引不匹配。 输出 step-change (单位:千美元){ step }, (中文(简体) ), quote-change (单位:千美元){ quote }和 booking-confirmed (单位:千美元){ ref, selection, total }) (中文(简体) ). 浏览器后移在步数之间; 步数和外值在刷新后幸存 。 在确认后,背面无法重启支付,刷新显示确认的屏幕. 如果付款成功,但 submitBooking 失败时,流会锁定在重新发送相同有效载荷的"再试一次"上(因此制作 submitBooking 专辑: paymentIntentId幸存的新鲜。 全文参见 llms.txt。 。 。 .
room-grid 行和特征变体
<room-grid variant="rows" rooms='…' currency="USD"> 将同紧凑的横向卡片(图像左出、标记/名称/描述、每晚价格右出)相同的房间数据放在光面上——两列放在桌面上,没有过滤栏、日期或入口运动。 为"更多住宿"风格上市.
<room-grid variant="feature" rooms='…' currency="USD"> 制作主页特性卡:在图像上贴上标记徽章,全卡链接,移动上的一个卷轴-snap 旋转木马,成为桌面上3个栏的网格,以及一个卷轴触发的已淡出入口. 发音相同 room-hover编号 :room-leave 事件——与 <island-map>。 。 。 .
<dining-teaser>
双栏主页取笑器:标题+身体复制,可悬浮/可聚焦的有色码状态点的场地列表,以及CTA——旁边的交叉分解相片可以跟踪任何活动地点. 滚动起的淡出/滑出入口,跳过 prefers-reduced-motion。 。 。 .
<script type="module">
import '@forgebuild/hotal-ui';
const teaser = document.querySelector('dining-teaser');
teaser.addEventListener('venue-change', (e) => console.log(e.detail)); // { index, key }
</script>
<dining-teaser
class="mx-auto max-w-[1530px]"
heading="Restaurants."
heading-accent="No compromise."
body="From open-fire dry-aged steaks to fresh catch and rum cocktails — dining here is never an afterthought."
cta-label="Explore dining"
cta-href="/dining"
venues='[{"key":"beefbar","name":"beefbar","tag":"Dinner · Fine Dining","image":"/img/beefbar.jpg"}]'
></dining-teaser>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
venues | venues (杰森语) | {key, name, tag?, image}[] | [] | 按顺序渲染; 徘徊、 聚焦或点击一行使其活动 |
heading 编号 : headingAccent | heading 编号 : heading-accent | string | 'Restaurants.' 编号 : 'No compromise.' | 二行标题, 二行口音颜色 |
body | body | string | —— 说 | 标题下的段落 |
ctaLabel 编号 : ctaHref | cta-label 编号 : cta-href | string | 'Explore dining' 编号 : '/dining' | 以下会议地点清单概述的链接 |
imageLabel | image-label | string | —— 说 | 固定的照片标题。 让它追踪活动地点 name 换成 |
activeIndex | active-index | number | 0 | 活动场地指数;作为联谊会财产设置,在访客互动时保持同步 |
活动
venue-change —— 说 CustomEvent<{ index: number; key: string }>当活动地点改变时.
他们
dining-teaser {
--dt-bg: #f5f2ec;
--dt-heading-color: #0a0814;
--dt-accent-color: rgb(180, 20, 80);
--dt-body-color: rgba(10, 8, 20, 0.65);
--dt-venue-bg: rgba(10, 8, 20, 0.04);
--dt-venue-bg-hover: rgba(10, 8, 20, 0.07);
--dt-venue-border: rgba(10, 8, 20, 0.08);
--dt-venue-border-hover: rgba(10, 8, 20, 0.14);
--dt-venue-name-color: #0a0814;
--dt-venue-type-color: rgba(10, 8, 20, 0.35);
--dt-dot-color: rgb(155, 93, 229);
--dt-dot-active-color: rgb(247, 37, 133);
--dt-cta-border: rgba(10, 8, 20, 0.15);
--dt-cta-text: #0a0814;
--dt-cta-hover-bg: #1a1a18;
--dt-cta-hover-text: #f0eeff;
--dt-image-height: 480px;
}
期望主机页面可以装入Syne,Inter,和Cormorant Garamond字体.
<booking-checkout>
取出表:"为自己订入/别人"取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取出取的取出取出取出取的取出取出取出取出取出取的取出取出取出取出取出取的取出取出取取出取出取的取出取出取出取出取出取出取出取出取出取的取出取出取的取出取出取出取出取出取的取出取出取出取出取出取的取出取出取出取出取出取出取出取的取出取出取出取出取出取的取出取出取的取出取出取的取出取取出取出取出取的取的取出取出取的取取出取 <searchable-select>,双向交叉,加上电话类型的国家推论),一个条纹支付元素,提交按钮,和一个可选的取消政策块. 后端- 不可知—— 主机创建 Paying Intent并进行实际预订 POST; 此组件只调用 stripe.confirmPayment 将结果交给 submitBooking。 。 。 .
<script type="module">
import '@forgebuild/hotal-ui';
const checkout = document.querySelector('booking-checkout');
checkout.resolveInitialLocation = async () => {
const res = await fetch('https://api.geoapify.com/v1/ipinfo?apiKey=...');
const data = await res.json();
return data?.country?.name ? { countryName: data.country.name, iso2: data.country.iso_code } : null;
};
checkout.beforeSubmit = async () => {
// e.g. re-price the PaymentIntent for extras added after it was created
};
checkout.submitBooking = async (payload) => {
const res = await fetch('/api/book', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await res.json();
return data.success ? { ok: true, ref: data.ref } : { ok: false, message: data.error ?? 'Something went wrong.' };
};
checkout.addEventListener('checkout-confirmed', (e) => {
window.location.href = `/book/confirm?ref=${e.detail.ref}`;
});
</script>
<booking-checkout
total="3800"
currency="USD"
client-secret="pi_..._secret_..."
stripe-publishable-key="pk_test_..."
cancellation-policy="Full refund up to 14 days before check-in."
locations='[{"iso2":"GB","countryName":"United Kingdom","dialCode":"+44"}]'
default-phone-code="+44"
></booking-checkout>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
total | total | number | 0 | 驱动提交按钮的金额标签; 设置为 JS 属性, 这样主机可以在额外更改后更新 |
currency | currency | string | 'USD' | 用于量标签 |
clientSecret | client-secret | string | —— 说 | Strape Paying Intent 客户端秘密, 由主机创建服务器侧 |
stripePublishableKey | stripe-publishable-key | string | —— 说 | 条形可公布的密钥 |
stripeAppearance | stripe-appearance (杰森语) | object | {} | 传到 stripe.elements({ appearance }) |
cancellationPolicy | cancellation-policy | string | —— 说 | 显示在显示时的专栏; 省略以完全隐藏 |
locations | locations (杰森语) | {iso2, countryName, dialCode}[] | 每个国家 | 两种选择(空取=每个国家,在浏览器的语言中命名)的出处 – 国家选项保留每个条目,电话编码选项由 dialCode |
defaultPhoneCode | default-phone-code | string | '+1' | 选择两个电话编码的初始值 |
allowBookingForOther | allow-booking-for-other | boolean | true | 隐藏切换和预订子窗体 false |
termsHref | terms-href | string | '/terms' | 术语链接和“全面政策细节”链接目标 |
prefill | - (仅限财产) | {firstName?, lastName?, email?} | —— 说 | 设置后挂载( 如一旦 Aync 认证检查解析后); 只填入空出字段 |
resolveInitialLocation | - (仅限财产) | () => Promise<{countryName, iso2?, dialCode?} | null> | —— 说 | 在挂载时被调用为所选择的国家/电话编码种子 |
beforeSubmit | - (仅限财产) | () => Promise<void> | —— 说 | 在卡片充电前等着 |
submitBooking | - (仅限财产) | (payload) => Promise<{ok:true,ref?} | {ok:false,message}> | —— 说 | 在成功收费后调用; 组件从未与预订的API本身交谈 |
strings | strings (杰森语) | object | —— 说 | 标签覆盖 – 完整的密钥列表参见组件源 |
活动
checkout-confirmed —— 说 CustomEvent<{ ref?: string }>有一次会起火 submitBooking 决定 { ok: true }。导航是东道主的工作. submitting-change —— 说 CustomEvent<{ submitting: boolean }>, (中文(简体) ), true 从付费点击(在验证之后),直到尝试结束.
ready ——在初始挂载尝试后放出一次( 配置或不配置、 成功或失败) —— 不与 Pay Element 的自挂载相匹配而丢弃页面级加载状态的钩子 .
他们
booking-checkout {
--bc-text-color: #05040f;
--bc-muted-text: rgba(5, 4, 15, 0.4);
--bc-muted-text-strong: rgba(5, 4, 15, 0.55);
--bc-border-color: rgba(5, 4, 15, 0.15);
--bc-input-bg: #f5f2ec;
--bc-input-border: rgba(5, 4, 15, 0.12);
--bc-input-border-focus: rgba(5, 4, 15, 0.4);
--bc-accent-bg: #05040f;
--bc-accent-text: #f5f2ec;
}
期望主机页面装入 Inter 字体,并且需要 <searchable-select> 注册(自动导入此软件包).
<confirmation-card>
中心确认卡:梯度检查图标,眉毛,标题,机构副本,一个可选的预订参考框,最多可有2个动作链接,以及围绕一个嵌入式链接构建的可选页脚注. 静态——没有道具是回叫,没有事件被释放. 主办方拥有全页中枢( display: flex; align-items: center; justify-content: center围绕它.
<script type="module">import '@forgebuild/hotal-ui';</script>
<confirmation-card
eyebrow="Booking Confirmed"
heading="You're all set."
body="Your reservation at HOTAL has been confirmed. A confirmation email is on its way to you."
reference="d82cdc93-54fe-4f6a-ae77-8fb2dc7ad1a8"
primary-label="View My Bookings"
primary-href="/account"
secondary-label="Back to Home"
secondary-href="/"
note-prefix="Questions about your stay?"
note-link-label="Contact our concierge team"
note-link-href="/contact"
note-suffix="— we're here to help with anything you need before you arrive."
></confirmation-card>
属性/ 属性
| 名称 | 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|---|
eyebrow | eyebrow | string | 'Booking Confirmed' | 标题上行; 省略以隐藏 |
heading | heading | string | "You're all set." | |
body | body | string | —— 说 | |
reference 编号 : referenceLabel | reference 编号 : reference-label | string | '' 编号 : 'Booking Reference' | 框式引用编号;当 reference 为空 |
primaryLabel 编号 : primaryHref | primary-label 编号 : primary-href | string | —— 说 | 已填充按钮; 省略 primaryLabel 为空 |
secondaryLabel 编号 : secondaryHref | secondary-label 编号 : secondary-href | string | —— 说 | 外观按钮; 两者必须设定为渲染 |
notePrefix 编号 : noteLinkLabel 编号 : noteLinkHref 编号 : noteSuffix | 弹匣 | string | —— 说 | 脚注文本围绕一个嵌入式链接进行分拆; 当两者兼有时完全省略 notePrefix 链接为空 |
他们
confirmation-card {
--cc-card-bg: #fff;
--cc-border-color: rgba(5, 4, 15, 0.08);
--cc-icon-bg: linear-gradient(135deg, rgb(0, 245, 212) 0%, rgb(79, 142, 247) 100%);
--cc-text-color: #05040f;
--cc-muted-text: rgba(5, 4, 15, 0.4);
--cc-muted-text-strong: rgba(5, 4, 15, 0.55);
--cc-ref-bg: rgba(5, 4, 15, 0.03);
--cc-accent-bg: #05040f;
--cc-accent-text: #fff;
--cc-note-border: rgba(5, 4, 15, 0.06);
--cc-max-width: 560px;
}
期望主机页面装入 Syne 和 Inter 字体 。 不要被混淆 与 <booking-confirmed> ——其中一项是用于离职的紧凑内在步骤 模式 (icon + 标题 + 将事件发回主机的“ 完成” 按钮); 此张是独立的全页卡片, 没有主机调回, 为专用 /confirm- 典型的路线.
<menu-items-editor>
按类别分类的餐厅菜单项目工作人员侧编辑。 每个字段都通过 saveItem,如果拒绝,则乐观地应用,取消。 Allergen and language chips 为 Alergen 切换和每个语言名称/描述扩展行. 每行的"打开"开关保存 { sold_out }所有回电都只是JS的属性.
<menu-items-editor></menu-items-editor>
<script type="module">
const el = document.querySelector('menu-items-editor');
el.items = menuItems; // [{ id, name, description, category, price, allergens, translations, sold_out }]
el.saveItem = (item, patch) => api.put(`/menu/${item.id}`, patch); // reject to undo + flag
el.createItem = (fields) => api.post('/menu', fields); // omit to hide add buttons
el.deleteItem = async (item) => (confirm(`Delete ${item.name}?`) ? api.delete(`/menu/${item.id}`) : false);
el.reorderItems = (items) => api.put('/menu/reorder', { ids: items.map((i) => i.id) }); // one category, first to last
el.reorderCategories = (categories) => api.put('/menu/category-order', { categories }); // omit either to hide its drag handles and move buttons
</script>
主题 --mie-* (见 llms.txt) (中文(简体) ). 默认出自所继承的文字颜色,因此在光和暗页上作为-is工作.
发展
npm install
npm run dev # serves demo/
npm run check # type-check
npm run build # library build → dist/
出版
npm run build
npm publish
需要登录到 npm (npm whoami(a) 作为拥有出版权的账户 @forgebuild 范围.