@edmicro/olkit 1.1.5 → 1.1.6

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.
@@ -246,6 +246,106 @@ declare class OlScrollArea {
246
246
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlScrollArea, "ol-scroll-area", never, { "scrollbar": { "alias": "scrollbar"; "required": false; "isSignal": true; }; "axis": { "alias": "axis"; "required": false; "isSignal": true; }; "smooth": { "alias": "smooth"; "required": false; "isSignal": true; }; "innerClass": { "alias": "innerClass"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
247
247
  }
248
248
 
249
+ /** Một mục trong mục lục. */
250
+ interface OlScrollspyItem {
251
+ /** `id` của phần tử trên trang mà mục này trỏ tới. */
252
+ id: string;
253
+ label: string;
254
+ /** Cấp thụt lề: 1 là mục chính, 2 là mục con. */
255
+ level?: 1 | 2;
256
+ }
257
+ type OlScrollspyOrientation = 'vertical' | 'horizontal';
258
+ /**
259
+ * Mục lục tự đánh dấu phần đang xem.
260
+ *
261
+ * Chưa có node Figma — dựng theo design system, mục đang xem dùng
262
+ * `text-fg-brand` và một vạch `bg-brand` bên trái (dọc) hoặc dưới (ngang).
263
+ *
264
+ * ## Vì sao `IntersectionObserver` chứ không nghe sự kiện `scroll`
265
+ *
266
+ * Nghe `scroll` thì mỗi lần cuộn là một lần chạy tay tính `getBoundingClientRect()`
267
+ * cho MỌI phần — trang 63 mục như docs olkit là 63 phép đo mỗi khung hình.
268
+ * `IntersectionObserver` để trình duyệt tự báo, và chỉ báo phần nào đổi trạng
269
+ * thái.
270
+ *
271
+ * ## `rootMargin` quyết định "đang xem" nghĩa là gì
272
+ *
273
+ * Mặc định `-20% 0px -70% 0px` thu vùng quan sát thành một dải ngang mỏng ở
274
+ * khoảng 1/4 trên màn hình. Không thu thì phần nào chạm mép dưới màn hình cũng
275
+ * tính là đang xem, và cuộn một nhịp có thể sáng ba mục cùng lúc.
276
+ *
277
+ * Dải mỏng cũng xử lý được phần NGẮN hơn màn hình: nếu đợi phần chiếm trọn
278
+ * viewport mới tính, mục ngắn không bao giờ được đánh dấu.
279
+ *
280
+ * ## Phần cuối trang không bao giờ chạm dải
281
+ *
282
+ * Cuộn hết cỡ mà phần cuối vẫn nằm dưới dải quan sát thì nó không sáng. Nên
283
+ * khi chạm đáy trang, component đánh dấu thẳng mục cuối — `atBottom()`.
284
+ *
285
+ * ## Lưu ý khi viết test
286
+ *
287
+ * jsdom KHÔNG có `IntersectionObserver` (đo 2026-09-26: `typeof` trả
288
+ * `'undefined'`). Component tự bảo vệ bằng cách kiểm trước khi dùng, nên spec
289
+ * dựng được và khoá được phần hiển thị; còn việc đánh dấu theo cuộn phải
290
+ * nghiệm thu trên trình duyệt. Đừng giả lập `IntersectionObserver` rồi tưởng
291
+ * đã kiểm hành vi thật.
292
+ */
293
+ declare class OlScrollspy {
294
+ private readonly elementRef;
295
+ private readonly destroyRef;
296
+ readonly items: _angular_core.InputSignal<OlScrollspyItem[]>;
297
+ readonly orientation: _angular_core.InputSignal<OlScrollspyOrientation>;
298
+ readonly ariaLabel: _angular_core.InputSignal<string>;
299
+ /**
300
+ * Vùng quan sát, theo cú pháp `rootMargin` của `IntersectionObserver`.
301
+ *
302
+ * Đổi giá trị này khi trang có thanh trên cố định: `-80px 0px -70% 0px` đẩy
303
+ * mép trên xuống dưới thanh đó.
304
+ */
305
+ readonly rootMargin: _angular_core.InputSignal<string>;
306
+ /** Cuộn mượt khi bấm mục. Tắt nếu trang tự lo việc cuộn. */
307
+ readonly smoothScroll: _angular_core.InputSignal<boolean>;
308
+ /**
309
+ * Khung cuộn chứa nội dung. `null` (mặc định) nghĩa là cả trang.
310
+ *
311
+ * BẮT BUỘC truyền khi nội dung nằm trong một khung `overflow-y-auto` riêng:
312
+ * `IntersectionObserver` mặc định quan sát theo VIEWPORT, nên phần nào cũng
313
+ * "đang hiện" khi khung con nằm trong tầm mắt — đo được: cuộn khung con từ
314
+ * đầu tới đáy mà không mục nào sáng.
315
+ */
316
+ readonly scrollRoot: _angular_core.InputSignal<HTMLElement | null>;
317
+ private readonly currentId;
318
+ /** `id` của phần đang xem. Rỗng khi chưa phần nào vào vùng quan sát. */
319
+ readonly activeId: _angular_core.Signal<string>;
320
+ readonly activeChange: _angular_core.OutputEmitterRef<string>;
321
+ private observer;
322
+ constructor();
323
+ private startObserving;
324
+ /** Khung (hoặc trang) đã cuộn tới sát đáy chưa — sai số 4px cho tròn số lẻ. */
325
+ private atBottom;
326
+ private stopObserving;
327
+ private setActive;
328
+ /**
329
+ * Bấm mục thì cuộn tới phần tương ứng và đánh dấu NGAY.
330
+ *
331
+ * Không đợi `IntersectionObserver` báo: cuộn mượt mất vài trăm mili giây, và
332
+ * trong quãng đó mục vừa bấm chưa sáng — người dùng tưởng bấm hụt.
333
+ */
334
+ protected onClick(event: MouseEvent, item: OlScrollspyItem): void;
335
+ protected readonly navClasses: _angular_core.Signal<"block" | "block w-full overflow-x-auto">;
336
+ protected readonly listClasses: _angular_core.Signal<"flex flex-col gap-0.5 border-s border-default ps-0" | "flex items-center gap-1 border-b border-default">;
337
+ /**
338
+ * Class của một mục.
339
+ *
340
+ * Gom vào MỘT chuỗi qua ba-ngôi thay vì nhiều `[class.x]` rời: hai class
341
+ * cùng thuộc tính thì trình duyệt chọn theo thứ tự luật trong file CSS chứ
342
+ * không theo thứ tự viết, kết quả đổi theo bản build.
343
+ */
344
+ protected linkClasses(item: OlScrollspyItem): string;
345
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlScrollspy, never>;
346
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlScrollspy, "ol-scrollspy", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "rootMargin": { "alias": "rootMargin"; "required": false; "isSignal": true; }; "smoothScroll": { "alias": "smoothScroll"; "required": false; "isSignal": true; }; "scrollRoot": { "alias": "scrollRoot"; "required": false; "isSignal": true; }; }, { "activeChange": "activeChange"; }, never, never, true, never>;
347
+ }
348
+
249
349
  type ButtonColor = 'default' | 'brand' | 'primary' | 'alternative' | 'secondary' | 'tertiary' | 'ghost' | 'success' | 'green' | 'danger' | 'red' | 'warning' | 'yellow' | 'dark' | 'light';
250
350
  type ButtonSize = 'xs' | 'sm' | 'base' | 'l' | 'xl';
251
351
  type OlButtonType = 'button' | 'submit' | 'reset';
@@ -1629,6 +1729,8 @@ declare class OlDropdown {
1629
1729
  private detachMenu;
1630
1730
  /** Hàm gỡ khỏi ngăn xếp Escape. `null` khi đang đóng. */
1631
1731
  private releaseEscape;
1732
+ /** Hàm gỡ listener Tab trên panel. `null` khi đang đóng. */
1733
+ private releaseMenuKeydown;
1632
1734
  readonly label: _angular_core.InputSignal<string | null>;
1633
1735
  readonly isOpen: _angular_core.ModelSignal<boolean>;
1634
1736
  /**
@@ -1679,6 +1781,75 @@ declare class OlDropdown {
1679
1781
  onTriggerClick(): void;
1680
1782
  onMouseEnter(): void;
1681
1783
  onMouseLeave(): void;
1784
+ /**
1785
+ * Danh sách phần tử nhận được tiêu điểm, theo thứ tự nhìn thấy.
1786
+ *
1787
+ * `tabindex="-1"` bị loại: đó là dấu hiệu tác giả cố ý đưa phần tử ra khỏi
1788
+ * đường Tab. `:disabled` cũng loại — trình duyệt không cho nó nhận tiêu điểm.
1789
+ *
1790
+ * Lọc phần ẩn bằng `[hidden]`/`[aria-hidden]` chứ KHÔNG `offsetParent`:
1791
+ * `offsetParent` là `null` với mọi phần tử trong `position: fixed`, và luôn
1792
+ * `null` trong jsdom — dùng nó thì danh sách rỗng, hàm thoát sớm, và toàn bộ
1793
+ * xử lý Tab lặng lẽ không chạy. Đã mắc: spec đỏ 3 ca trong khi trình duyệt
1794
+ * thật vẫn đúng, vì hai môi trường cho `offsetParent` khác nhau.
1795
+ */
1796
+ private focusableItems;
1797
+ /**
1798
+ * Nút mở panel, kể cả khi nơi dùng tự khai qua slot `[trigger]`.
1799
+ *
1800
+ * `triggerButtonRef` CHỈ có giá trị với nút mặc định của component — hai
1801
+ * nhánh `@if` trong template vẽ ra nó. Nơi dùng truyền nút riêng qua
1802
+ * `<ng-content select="[trigger]">` thì cả hai nhánh đều không chạy và ref
1803
+ * là `undefined`. Đo 2026-09-25: header onluyenv4 (streak, chuông) đều dùng
1804
+ * nút slot, nên mọi xử lý bám vào ref đó lặng lẽ không chạy — gồm cả việc
1805
+ * trả tiêu điểm về nút sau khi Escape, vốn có từ trước.
1806
+ *
1807
+ * Nên tìm nút thật trong cây DOM của host khi không có ref.
1808
+ */
1809
+ private triggerElement;
1810
+ /**
1811
+ * Phần tử nhận tiêu điểm đứng ngay sau `element` trong thứ tự Tab của trang.
1812
+ *
1813
+ * Quét từ `<body>` và loại chính panel ra: panel treo cuối `<body>` nên nếu
1814
+ * tính cả nó thì "phần tử sau nút trigger" lại rơi vào panel vừa đóng.
1815
+ */
1816
+ private nextFocusableAfter;
1817
+ /**
1818
+ * Tab từ nút trigger khi panel đang mở → nhảy VÀO mục đầu panel.
1819
+ *
1820
+ * Vì sao cần: `appendTo="body"` treo panel ra cuối `<body>`, nên trong thứ
1821
+ * tự DOM nó nằm SAU toàn bộ nội dung trang. Bàn phím đi theo thứ tự DOM chứ
1822
+ * không theo vị trí nhìn thấy, nên Tab từ nút trigger nhảy sang phần tử kế
1823
+ * tiếp trong header rồi xuống thân trang — panel mở ngay trước mắt mà không
1824
+ * bao giờ tới được. Đo 2026-09-25: 8 lượt Tab, không lượt nào vào panel.
1825
+ *
1826
+ * KHÔNG cuốn tiêu điểm lúc mở: người dùng chuột không cần, và nhảy tiêu điểm
1827
+ * khi họ chưa yêu cầu là cướp quyền điều khiển. Chỉ khi họ chủ động nhấn Tab
1828
+ * mới dẫn vào — đúng lúc họ đang tìm đường đi tiếp.
1829
+ *
1830
+ * Shift+Tab bỏ qua: người dùng đang muốn lùi RA, giữ họ lại là bẫy.
1831
+ */
1832
+ onTriggerKeydown(event: KeyboardEvent): void;
1833
+ /**
1834
+ * Tab BÊN TRONG panel — giữ đường đi liền mạch với chỗ panel đang hiện.
1835
+ *
1836
+ * Panel nằm cuối `<body>` nên nếu để trình duyệt tự xử, Tab ở mục cuối sẽ
1837
+ * rơi xuống phần tử sau panel trong DOM (thường là hết trang), còn Shift+Tab
1838
+ * ở mục đầu nhảy ngược lên thân trang — cả hai đều bỏ qua nút trigger, tức
1839
+ * người dùng mất mốc quay về.
1840
+ *
1841
+ * KHÔNG bẫy vòng tròn như hộp thoại: chuẩn WAI-ARIA cho menu quy định Tab
1842
+ * phải THOÁT được ra trang. Nên:
1843
+ *
1844
+ * - Tab ở mục CUỐI → đóng panel, trả tiêu điểm về nút trigger rồi để trình
1845
+ * duyệt đi tiếp bình thường (phần tử sau nút trong header)
1846
+ * - Shift+Tab ở mục ĐẦU → về thẳng nút trigger, panel vẫn mở để người dùng
1847
+ * còn thấy mình vừa rời khỏi đâu
1848
+ *
1849
+ * Giữa các mục thì không can thiệp — panel là một khối liền trong DOM nên
1850
+ * trình duyệt tự đi đúng.
1851
+ */
1852
+ onMenuKeydown(event: KeyboardEvent): void;
1682
1853
  show(): void;
1683
1854
  hide(): void;
1684
1855
  toggle(): void;
@@ -1702,6 +1873,19 @@ declare class OlDropdownItem {
1702
1873
  readonly itemClick: _angular_core.OutputEmitterRef<MouseEvent>;
1703
1874
  private readonly parentDropdown;
1704
1875
  readonly isItemActive: _angular_core.Signal<boolean>;
1876
+ /**
1877
+ * Hình học theo Figma `ol-dropdown-item` (node 1149:39652, đo 2026-09-28):
1878
+ * lề 8px bốn cạnh (`px-2 py-2`, cao 36px), icon 16px cách chữ 6px, bo 8px
1879
+ * (`rounded` = `--radius: 8px`), chữ 14px medium. Bản trước `px-3` (12px),
1880
+ * icon cách chữ 10px, bo 6px.
1881
+ *
1882
+ * Độ đậm nằm TRONG từng nhánh, không gắn vô điều kiện — hai class cùng thuộc
1883
+ * tính thì thắng thua theo thứ tự luật CSS, không theo thứ tự trong chuỗi.
1884
+ *
1885
+ * Trạng thái đang chọn: Figma KHÔNG vẽ (chỉ có Initial / Hover / Disabled).
1886
+ * Bản trước tự đặt `bg-brand-softer` (#F5F8FF trên nền trắng — gần như không
1887
+ * thấy, báo 2026-09-28). Nay `bg-brand-soft` (#E3EAFF) + chữ brand đậm.
1888
+ */
1705
1889
  readonly itemClasses: _angular_core.Signal<string>;
1706
1890
  handleClick(event: MouseEvent): void;
1707
1891
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlDropdownItem, never>;
@@ -1733,6 +1917,51 @@ declare class OlFooter {
1733
1917
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFooter, "ol-footer", never, { "container": { "alias": "container"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
1734
1918
  }
1735
1919
 
1920
+ type OlFrameSpacing = 'xs' | 'sm' | 'base' | 'lg';
1921
+ type OlFrameRounded = 'sm' | 'md' | 'lg' | 'xl';
1922
+ type OlFrameVariant = 'default' | 'ghost';
1923
+ /**
1924
+ * Khung bọc nhiều mảng nội dung, khoảng cách đều nhau.
1925
+ *
1926
+ * Chưa có node Figma — dựng theo design system.
1927
+ *
1928
+ * ## Vì sao KHÔNG dùng `ol-card` cho việc này
1929
+ *
1930
+ * `ol-card` là MỘT thẻ nội dung: có ảnh bìa, có `href`, có trạng thái chọn.
1931
+ * Còn đây là cái khung xếp NHIỀU mảng cạnh nhau với khoảng cách đều — như cột
1932
+ * thiết lập, bảng điều khiển, hay danh sách nhóm. Lồng nhiều `ol-card` vào một
1933
+ * `div` rồi tự đặt `space-y-*` cũng ra kết quả, nhưng mỗi nơi dùng lại chọn
1934
+ * một khoảng cách khác và trang trông lộn xộn.
1935
+ *
1936
+ * ## `stacked` bỏ viền giữa, không phải bỏ viền hết
1937
+ *
1938
+ * Xếp chồng thì các mảng dính thành một khối liền, chỉ còn một viền ngoài và
1939
+ * kẻ ngăn mảnh ở giữa — giống `ol-list-group`. Tách rời thì mỗi mảng là một
1940
+ * thẻ riêng có viền đủ bốn cạnh.
1941
+ *
1942
+ * Làm bằng `divide-y` cộng `overflow-hidden` trên khung, KHÔNG bằng
1943
+ * `[&>*:not(:last-child)]:border-b`: biến thể `:last-child` trong chuỗi class
1944
+ * của component con sẽ khớp MỌI thể hiện của con đó, kể cả khi nó là con duy
1945
+ * nhất ở nơi khác — lỗi đã gặp trong repo này.
1946
+ *
1947
+ * ## `ghost` bỏ nền và viền, giữ khoảng cách
1948
+ *
1949
+ * Dùng khi khung nằm trong một thẻ đã có viền rồi — hai lớp viền lồng nhau
1950
+ * trông như lỗi hiển thị.
1951
+ */
1952
+ declare class OlFrame {
1953
+ readonly spacing: _angular_core.InputSignal<OlFrameSpacing>;
1954
+ readonly rounded: _angular_core.InputSignal<OlFrameRounded>;
1955
+ readonly variant: _angular_core.InputSignal<OlFrameVariant>;
1956
+ /** Dính các mảng thành một khối liền, chỉ còn kẻ ngăn ở giữa. */
1957
+ readonly stacked: _angular_core.InputSignal<boolean>;
1958
+ /** Xếp ngang thay vì dọc. Khoảng cách và kẻ ngăn đổi trục theo. */
1959
+ readonly horizontal: _angular_core.InputSignal<boolean>;
1960
+ protected readonly frameClasses: _angular_core.Signal<string>;
1961
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlFrame, never>;
1962
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFrame, "ol-frame", never, { "spacing": { "alias": "spacing"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "stacked": { "alias": "stacked"; "required": false; "isSignal": true; }; "horizontal": { "alias": "horizontal"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
1963
+ }
1964
+
1736
1965
  /**
1737
1966
  * Bảng chuẩn kích thước và trạng thái cho MỌI ô nhập của thư viện.
1738
1967
  *
@@ -3031,6 +3260,176 @@ declare class OlToggleGroup {
3031
3260
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlToggleGroup, "ol-toggle-group", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
3032
3261
  }
3033
3262
 
3263
+ /**
3264
+ * Một lựa chọn của `ol-segmented`. `value` là thứ đổ vào model — dùng chuỗi
3265
+ * hoặc số, so bằng `===`.
3266
+ */
3267
+ interface SegmentedOption<T = string | number> {
3268
+ value: T;
3269
+ label: string;
3270
+ icon?: string;
3271
+ disabled?: boolean;
3272
+ }
3273
+ type SegmentedSize = 'sm' | 'base';
3274
+ /**
3275
+ * Chọn MỘT trong vài chế độ loại trừ nhau, đặt cạnh nhau trên một rãnh xám;
3276
+ * mục đang chọn nổi lên nền trắng (segmented control).
3277
+ *
3278
+ * Khác `ol-tabs`: tabs chuyển PANEL nội dung, đây chọn GIÁ TRỊ (có CVA, dùng
3279
+ * được trong form). Khác `ol-toggle`: công tắc chỉ có hai trạng thái bật/tắt.
3280
+ *
3281
+ * Trợ năng theo mẫu radio group của WAI-ARIA: `role="radiogroup"`, mỗi mục
3282
+ * `role="radio"`, chỉ mục đang chọn nằm trong thứ tự Tab (roving tabindex),
3283
+ * mũi tên trái/phải/lên/xuống chuyển VÀ chọn luôn, Home/End về đầu/cuối.
3284
+ *
3285
+ * Figma chưa có thiết kế (tra `lip4g3...` 2026-09-28: không có component nào
3286
+ * tên segmented) — dựng theo token của hệ thống.
3287
+ */
3288
+ declare class OlSegmented implements ControlValueAccessor {
3289
+ readonly options: _angular_core.InputSignal<SegmentedOption<string | number>[]>;
3290
+ readonly value: _angular_core.ModelSignal<string | number | null>;
3291
+ readonly size: _angular_core.InputSignal<SegmentedSize>;
3292
+ /** Giãn hết bề ngang khung, các mục chia đều. */
3293
+ readonly fullWidth: _angular_core.InputSignal<boolean>;
3294
+ /** `model()` để `setDisabledState()` của Reactive Forms đặt được. */
3295
+ readonly disabled: _angular_core.ModelSignal<boolean>;
3296
+ /** Tên nhóm cho bộ đọc màn hình — nên đặt, vì nhóm không có nhãn nhìn thấy. */
3297
+ readonly ariaLabel: _angular_core.InputSignal<string>;
3298
+ private readonly host;
3299
+ private onChange;
3300
+ private onTouched;
3301
+ protected readonly hostClasses: _angular_core.Signal<"block w-full" | "inline-block max-w-full">;
3302
+ protected readonly trackClasses: _angular_core.Signal<string>;
3303
+ /** Mục đang Tab vào được: mục đang chọn, không có thì mục bật đầu tiên. */
3304
+ protected readonly focusIndex: _angular_core.Signal<number>;
3305
+ protected itemClasses(option: SegmentedOption): string;
3306
+ select(option: SegmentedOption): void;
3307
+ protected onKeydown(event: KeyboardEvent, index: number): void;
3308
+ /** Mục bật kế tiếp theo chiều `step`, vòng quanh. -1 khi không có. */
3309
+ private nextEnabled;
3310
+ writeValue(v: string | number | null): void;
3311
+ registerOnChange(fn: (v: string | number | null) => void): void;
3312
+ registerOnTouched(fn: () => void): void;
3313
+ setDisabledState(isDisabled: boolean): void;
3314
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlSegmented, never>;
3315
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlSegmented, "ol-segmented", never, { "options": { "alias": "options"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "fullWidth": { "alias": "fullWidth"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; "disabled": "disabledChange"; }, never, never, true, never>;
3316
+ }
3317
+
3318
+ /**
3319
+ * Loại tệp — nhận biết để chọn icon, màu và chữ mô tả.
3320
+ *
3321
+ * Mười loại phủ gần hết tệp giáo viên/học sinh tải lên: bài làm (ảnh, PDF,
3322
+ * Word), bảng điểm (Excel), bài giảng (PowerPoint, video), nghe (audio), nộp
3323
+ * gộp (nén), bài tin học (mã nguồn), ghi chú (văn bản thô).
3324
+ */
3325
+ type FileKind = 'image' | 'pdf' | 'word' | 'excel' | 'powerpoint' | 'video' | 'audio' | 'archive' | 'code' | 'text' | 'other';
3326
+ interface FileKindMeta {
3327
+ label: string;
3328
+ /** Họ lucide `file-*` — một họ nét cho mọi loại, nhìn đồng bộ. */
3329
+ icon: string;
3330
+ /**
3331
+ * Màu ô icon, lấy từ bảng màu `ol-icon-shape` — không tự tô. KHÔNG dùng
3332
+ * `dark`: nó là ô ĐẶC đen giữa các ô nền nhạt, nặng hơn hẳn (đo 2026-09-28).
3333
+ */
3334
+ color: IconShapeColor;
3335
+ }
3336
+ declare const FILE_KIND_META: Record<FileKind, FileKindMeta>;
3337
+ /** Phần mở rộng, chữ thường, không dấu chấm. Không có → chuỗi rỗng. */
3338
+ declare function fileExtension(name: string): string;
3339
+ /**
3340
+ * Nhận biết loại tệp — ưu tiên PHẦN MỞ RỘNG, lùi về MIME.
3341
+ *
3342
+ * Không tin MIME trước: trình duyệt đoán MIME theo đuôi và hệ điều hành, nên
3343
+ * cùng một tệp `.docx` có máy báo `application/vnd.openxml…`, có máy báo rỗng
3344
+ * (Windows thiếu Office), và `.csv` có máy báo `application/vnd.ms-excel`.
3345
+ */
3346
+ declare function detectFileKind(name: string, mime?: string): FileKind;
3347
+ /**
3348
+ * Byte → chữ ngắn theo cách người Việt đọc: `850 B`, `12,4 KB`, `3,2 MB`.
3349
+ * Dấu phẩy thập phân; số nguyên thì bỏ phần lẻ (`2 MB`, không `2,0 MB`).
3350
+ */
3351
+ declare function formatFileSize(bytes: number): string;
3352
+ /**
3353
+ * Tệp có khớp chuỗi `accept` của thẻ `<input type="file">` không.
3354
+ *
3355
+ * Hiểu đủ ba dạng của chuẩn HTML: đuôi (`.pdf`), MIME đầy đủ
3356
+ * (`application/pdf`), MIME đại diện (`image/*`). `*` hoặc rỗng → nhận hết.
3357
+ *
3358
+ * Cần tự kiểm vì `accept` chỉ LỌC hộp thoại chọn tệp — kéo thả thì trình duyệt
3359
+ * không lọc gì, và người dùng đổi bộ lọc hộp thoại sang "Tất cả tệp" là chọn
3360
+ * được tệp bất kỳ.
3361
+ */
3362
+ declare function matchesAccept(file: {
3363
+ name: string;
3364
+ type: string;
3365
+ }, accept: string): boolean;
3366
+ type FileRejectReason = 'type' | 'size' | 'count';
3367
+ interface FileRejection {
3368
+ file: File;
3369
+ reason: FileRejectReason;
3370
+ /** Câu báo lỗi tiếng Việt, hiện được ngay. */
3371
+ message: string;
3372
+ }
3373
+ interface FileValidationRules {
3374
+ accept?: string;
3375
+ /** Byte. 0 hoặc không truyền → không giới hạn. */
3376
+ maxSize?: number;
3377
+ /** Tổng số tệp tối đa, TÍNH CẢ tệp đã có. 0 → không giới hạn. */
3378
+ maxFiles?: number;
3379
+ /** Số tệp đang có sẵn trước lượt chọn này. */
3380
+ existingCount?: number;
3381
+ }
3382
+ interface FileValidationResult {
3383
+ accepted: File[];
3384
+ rejected: FileRejection[];
3385
+ }
3386
+ /**
3387
+ * Chia tệp thành nhận / loại, kèm lý do từng tệp bị loại.
3388
+ *
3389
+ * Thứ tự kiểm: định dạng → dung lượng → số lượng. Tệp sai định dạng không
3390
+ * chiếm chỗ trong hạn mức số lượng — chọn 3 tệp, 1 tệp `.exe`, giới hạn 2 thì
3391
+ * nhận 2 tệp hợp lệ chứ không loại oan tệp thứ ba vì tệp `.exe` đứng trước.
3392
+ */
3393
+ declare function validateFiles(files: File[], rules?: FileValidationRules): FileValidationResult;
3394
+ /** Trạng thái tải của một tệp — nơi gọi cập nhật theo tiến trình HTTP thật. */
3395
+ type UploadStatus = 'idle' | 'uploading' | 'success' | 'error';
3396
+ /**
3397
+ * Một tệp trong danh sách tải lên.
3398
+ *
3399
+ * `file` có thể vắng: tệp đã có sẵn trên máy chủ (mở lại bài đã nộp) chỉ có
3400
+ * tên, dung lượng và `url`, không có đối tượng `File`.
3401
+ */
3402
+ interface OlUploadFile {
3403
+ id: string;
3404
+ name: string;
3405
+ size: number;
3406
+ file?: File;
3407
+ /** Ảnh xem trước hoặc liên kết tải về. */
3408
+ url?: string;
3409
+ status?: UploadStatus;
3410
+ /** 0–100, chỉ dùng khi `status = 'uploading'`. */
3411
+ progress?: number;
3412
+ /** Câu báo lỗi khi `status = 'error'`. */
3413
+ error?: string;
3414
+ }
3415
+ /** File → OlUploadFile, gán id duy nhất để `@for track` không lẫn hai tệp trùng tên. */
3416
+ declare function toUploadFile(file: File): OlUploadFile;
3417
+ /**
3418
+ * Tệp trong một lần dán (Ctrl+V / ⌘V).
3419
+ *
3420
+ * Đọc cả `clipboardData.files` lẫn `items` loại `file`: Chrome đặt ảnh chụp
3421
+ * màn hình ở cả hai, Safari có lúc chỉ ở `items`. Gộp theo đối tượng để không
3422
+ * nhận một ảnh hai lần.
3423
+ *
3424
+ * Ảnh chụp màn hình dán vào LUÔN tên `image.png` — dán ba lần là ba tệp trùng
3425
+ * tên, không phân biệt được trong danh sách. Đổi thành "Ảnh dán 14-32-05.png".
3426
+ */
3427
+ declare function filesFromClipboard(data: DataTransfer | null, now?: Date): File[];
3428
+ /** Phím tắt dán theo hệ điều hành — để gợi ý đúng chữ người dùng thấy trên bàn phím. */
3429
+ declare function pasteShortcutLabel(platform?: string): string;
3430
+ /** Đang gõ trong ô nhập chữ — dán ở đó là dán CHỮ, không được cướp. */
3431
+ declare function isEditableTarget(target: EventTarget | null): boolean;
3432
+
3034
3433
  /**
3035
3434
  * Thang cỡ theo Figma: `sm | base | lg`. `'md'` là BÍ DANH của `'base'`,
3036
3435
  * giữ cho mã cũ chạy tiếp và sẽ bỏ ở bản sau — dùng `'base'` cho mã mới.
@@ -3061,15 +3460,325 @@ declare class OlFileInput {
3061
3460
  readonly accept: _angular_core.InputSignal<string>;
3062
3461
  readonly multiple: _angular_core.InputSignal<boolean>;
3063
3462
  readonly id: _angular_core.InputSignal<string>;
3463
+ /** Byte. 0 → không giới hạn. Có giá trị thì vùng kéo thả ghi "Dung lượng tối đa". */
3464
+ readonly maxSize: _angular_core.InputSignal<number>;
3465
+ /** Nhãn nút trong vùng kéo thả (Figma Advanced). Rỗng → không vẽ nút. */
3466
+ readonly ctaLabel: _angular_core.InputSignal<string>;
3467
+ /** Figma State: Danger / Success. Tệp bị loại tự chuyển sang `error`. */
3468
+ readonly state: _angular_core.InputSignal<"error" | "success" | "default">;
3469
+ /** Tệp đã nhận (sau khi kiểm). */
3064
3470
  readonly selectedFiles: _angular_core.WritableSignal<File[]>;
3065
- readonly isDragging: _angular_core.WritableSignal<boolean>;
3471
+ protected readonly items: _angular_core.WritableSignal<OlUploadFile[]>;
3472
+ protected readonly errors: _angular_core.WritableSignal<string[]>;
3066
3473
  readonly filesChanged: _angular_core.OutputEmitterRef<File[]>;
3474
+ /** Tệp bị loại kèm lý do — component đã tự hiện câu lỗi. */
3475
+ readonly rejected: _angular_core.OutputEmitterRef<FileRejection[]>;
3476
+ private readonly dropInput;
3477
+ private readonly host;
3478
+ protected readonly zoneState: _angular_core.Signal<"error" | "success" | "default">;
3479
+ /**
3480
+ * Dòng phụ trong vùng: "Dung lượng tối đa" nếu có `maxSize`, không thì
3481
+ * `description` — giữ nguyên chữ của mã cũ đang truyền `description`.
3482
+ */
3483
+ protected readonly zoneHint: _angular_core.Signal<string>;
3067
3484
  onFileSelected(event: Event): void;
3068
- onDragOver(event: DragEvent): void;
3069
- onDragLeave(event: DragEvent): void;
3070
- onDrop(event: DragEvent): void;
3485
+ protected openPicker(): void;
3486
+ /** Nhận tệp từ hộp chọn hoặc kéo thả: kiểm định dạng + dung lượng rồi phát. */
3487
+ protected receive(incoming: File[]): void;
3488
+ /** Dán tệp (Ctrl+V / ⌘V) khi tiêu điểm ở ô hoặc vùng kéo thả. */
3489
+ protected onPaste(event: ClipboardEvent): void;
3490
+ /**
3491
+ * Ô thường (không dropzone) hiện tên tệp bằng CHÍNH `<input type="file">` —
3492
+ * nhận tệp dán mà không gán vào nó thì ô vẫn ghi "Chưa chọn tệp" dù đã nhận.
3493
+ * `DataTransfer` là cách duy nhất gán `files` cho ô tệp.
3494
+ */
3495
+ private syncNativeInput;
3496
+ protected removeItem(item: OlUploadFile): void;
3071
3497
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlFileInput, never>;
3072
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFileInput, "ol-file-input", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "helperText": { "alias": "helperText"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "dropzone": { "alias": "dropzone"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "accept": { "alias": "accept"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; }, { "filesChanged": "filesChanged"; }, never, never, true, never>;
3498
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFileInput, "ol-file-input", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "helperText": { "alias": "helperText"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "dropzone": { "alias": "dropzone"; "required": false; "isSignal": true; }; "title": { "alias": "title"; "required": false; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "accept": { "alias": "accept"; "required": false; "isSignal": true; }; "multiple": { "alias": "multiple"; "required": false; "isSignal": true; }; "id": { "alias": "id"; "required": false; "isSignal": true; }; "maxSize": { "alias": "maxSize"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; }, { "filesChanged": "filesChanged"; "rejected": "rejected"; }, never, never, true, never>;
3499
+ }
3500
+
3501
+ /**
3502
+ * Ba dạng của một tệp:
3503
+ *
3504
+ * - `row` — dòng trong danh sách: icon loại tệp, tên, dung lượng, thanh tiến
3505
+ * trình khi đang tải, nút thử lại khi lỗi. Figma không vẽ dạng này; dựng theo
3506
+ * token để danh sách tải nhiều tệp có chỗ báo tiến trình từng tệp.
3507
+ * - `chip` — Figma "Button header + Dropbox (uploaded no limit)": viên bo góc,
3508
+ * nút xem 👁 + tên + ✕.
3509
+ * - `card` — Figma "Button header + Dropbox (uploaded max 1 file)": khung viền,
3510
+ * icon + tên ở giữa, ✕ ở góc.
3511
+ */
3512
+ type FileItemVariant = 'row' | 'chip' | 'card';
3513
+ declare class OlFileItem {
3514
+ readonly file: _angular_core.InputSignal<OlUploadFile>;
3515
+ readonly variant: _angular_core.InputSignal<FileItemVariant>;
3516
+ readonly removable: _angular_core.InputSignal<boolean>;
3517
+ /** Hiện nút xem 👁 — nơi gọi tự mở tệp khi nhận `(preview)`. */
3518
+ readonly previewable: _angular_core.InputSignal<boolean>;
3519
+ readonly disabled: _angular_core.InputSignal<boolean>;
3520
+ readonly remove: _angular_core.OutputEmitterRef<OlUploadFile>;
3521
+ readonly preview: _angular_core.OutputEmitterRef<OlUploadFile>;
3522
+ readonly retry: _angular_core.OutputEmitterRef<OlUploadFile>;
3523
+ protected readonly kind: _angular_core.Signal<_edmicro_olkit.FileKind>;
3524
+ protected readonly meta: _angular_core.Signal<_edmicro_olkit.FileKindMeta>;
3525
+ protected readonly sizeText: _angular_core.Signal<string>;
3526
+ protected readonly status: _angular_core.Signal<_edmicro_olkit.UploadStatus>;
3527
+ protected readonly progress: _angular_core.Signal<number>;
3528
+ /** Dòng phụ dưới tên: dung lượng + trạng thái, đọc được cả khi không nhìn màu. */
3529
+ protected readonly metaText: _angular_core.Signal<string>;
3530
+ protected readonly metaClass: _angular_core.Signal<"text-xs text-fg-danger" | "text-xs text-body-subtle">;
3531
+ protected readonly rowClass: _angular_core.Signal<string>;
3532
+ /** Nút hành động nhỏ — cùng hình với nút đóng của drawer/modal. */
3533
+ protected readonly actionClass = "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-body-subtle transition-colors hover:bg-neutral-tertiary hover:text-heading focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-brand-subtle disabled:pointer-events-none disabled:opacity-50";
3534
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlFileItem, never>;
3535
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFileItem, "ol-file-item", never, { "file": { "alias": "file"; "required": true; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "removable": { "alias": "removable"; "required": false; "isSignal": true; }; "previewable": { "alias": "previewable"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; }, { "remove": "remove"; "preview": "preview"; "retry": "retry"; }, never, never, true, never>;
3536
+ }
3537
+
3538
+ /**
3539
+ * Vùng kéo–thả, theo Figma `ol-file-input-form` Type=Advanced (1149:48977):
3540
+ * icon tải lên · "Nhấn để tải lên hoặc kéo thả" · "Dung lượng tối đa: …" ·
3541
+ * (tuỳ chọn) nút "Chọn tệp".
3542
+ *
3543
+ * ## Cả vùng là MỘT `<button>`
3544
+ *
3545
+ * Bản cũ trong `ol-file-input` đặt `<input type="file" class="hidden">` —
3546
+ * `display: none` nên Tab không tới được, bàn phím không mở được hộp chọn tệp.
3547
+ * Ở đây vùng là nút thật: Tab tới, Enter/Space mở hộp chọn (qua `(pick)`, nơi
3548
+ * gọi gọi `input.click()`), và trình đọc màn hình đọc nhãn của nút.
3549
+ *
3550
+ * Nút "Chọn tệp" bên trong chỉ là HÌNH (`<span>`), không lồng `<button>` trong
3551
+ * `<button>` — HTML cấm, và hai nút lồng nhau làm một lần bấm bắn hai sự kiện.
3552
+ */
3553
+ declare class OlFileDropzone {
3554
+ readonly title: _angular_core.InputSignal<string>;
3555
+ /** Dòng phụ, ví dụ "Dung lượng tối đa: 30MB". Rỗng → không vẽ. */
3556
+ readonly hint: _angular_core.InputSignal<string>;
3557
+ /** Nhãn nút "Chọn tệp" trong vùng (Figma Advanced). Rỗng → không vẽ nút. */
3558
+ readonly ctaLabel: _angular_core.InputSignal<string>;
3559
+ readonly icon: _angular_core.InputSignal<string>;
3560
+ readonly disabled: _angular_core.InputSignal<boolean>;
3561
+ /** Trạng thái viền: lỗi / thành công (Figma State=Danger / Success). */
3562
+ readonly state: _angular_core.InputSignal<"error" | "success" | "default">;
3563
+ /** Chiều cao tối thiểu — vùng trong khung có tiêu đề thấp hơn vùng đứng riêng. */
3564
+ readonly compact: _angular_core.InputSignal<boolean>;
3565
+ readonly describedBy: _angular_core.InputSignal<string>;
3566
+ /** Dòng "hoặc dán ⌘V / Ctrl+V". Tắt khi nơi gọi không nhận tệp dán. */
3567
+ readonly pasteHint: _angular_core.InputSignal<boolean>;
3568
+ protected readonly shortcut: string;
3569
+ /** Bấm vào vùng (chuột hoặc bàn phím) — nơi gọi mở hộp chọn tệp. */
3570
+ readonly pick: _angular_core.OutputEmitterRef<void>;
3571
+ /** Tệp thả hoặc DÁN vào vùng. CHƯA kiểm định dạng — việc của nơi gọi. */
3572
+ readonly filesDropped: _angular_core.OutputEmitterRef<File[]>;
3573
+ protected readonly dragging: _angular_core.WritableSignal<boolean>;
3574
+ protected readonly zoneClass: _angular_core.Signal<string>;
3575
+ protected onDragOver(event: DragEvent): void;
3576
+ protected onDragLeave(event: DragEvent): void;
3577
+ /**
3578
+ * Dán khi tiêu điểm đang ở vùng. Có tệp thì nhận và `stopPropagation` — khung
3579
+ * cha (`ol-file-upload`) cũng nghe `paste`, không chặn là nhận hai lần. Không
3580
+ * có tệp (dán chữ) thì để yên cho trình duyệt.
3581
+ */
3582
+ protected onPaste(event: ClipboardEvent): void;
3583
+ protected onDrop(event: DragEvent): void;
3584
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlFileDropzone, never>;
3585
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFileDropzone, "ol-file-dropzone", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "hint": { "alias": "hint"; "required": false; "isSignal": true; }; "ctaLabel": { "alias": "ctaLabel"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "state": { "alias": "state"; "required": false; "isSignal": true; }; "compact": { "alias": "compact"; "required": false; "isSignal": true; }; "describedBy": { "alias": "describedBy"; "required": false; "isSignal": true; }; "pasteHint": { "alias": "pasteHint"; "required": false; "isSignal": true; }; }, { "pick": "pick"; "filesDropped": "filesDropped"; }, never, never, true, never>;
3586
+ }
3587
+
3588
+ /**
3589
+ * Toán phóng/kéo ảnh — hàm thuần, không đụng DOM, để test chính xác từng px.
3590
+ *
3591
+ * Ảnh vẽ bằng `transform: translate(x, y) scale(s)`, gốc biến đổi ở TÂM ảnh.
3592
+ * Một điểm `p` của ảnh (tính từ tâm, đơn vị px lúc chưa phóng) hiện ra ở
3593
+ * `q = t + s·p` so với tâm khung. Phóng quanh điểm `q` (chỗ bấm / con trỏ /
3594
+ * giữa hai ngón) nghĩa là giữ `q` đứng yên: `p = (q − t) / s`, `t' = q − s'·p`.
3595
+ */
3596
+ interface ZoomState {
3597
+ scale: number;
3598
+ x: number;
3599
+ y: number;
3600
+ }
3601
+
3602
+ /** Một ảnh trong trình xem. */
3603
+ interface OlPreviewImage {
3604
+ src: string;
3605
+ /** Chữ thay thế — cũng hiện làm chú thích dưới ảnh. */
3606
+ alt?: string;
3607
+ }
3608
+ /**
3609
+ * Xem ảnh phóng to trên lớp phủ toàn màn (lightbox) — một ảnh hoặc lướt qua
3610
+ * nhiều ảnh.
3611
+ *
3612
+ * - Nhiều ảnh: nút ‹ › hai bên, số thứ tự "2 / 5", phím ← → chuyển ảnh, vòng quanh.
3613
+ * - Phóng to/thu nhỏ (1×–4×): bấm ảnh để phóng 2× đúng chỗ bấm, bấm lại để về;
3614
+ * nút − / + trên thanh, phím + − 0; cuộn chuột / chụm trackpad quanh con
3615
+ * trỏ; chụm hai ngón trên điện thoại. Đang phóng thì kéo để xem chỗ khác.
3616
+ * - Escape, nút ✕ hoặc bấm ra nền tối để đóng. Tiêu điểm bị giữ trong lớp phủ
3617
+ * và trả về chỗ cũ khi đóng; nền trang khoá cuộn (`OlFocusTrapService`).
3618
+ * - Escape đi qua ngăn xếp `OlOverlayContainer`: mở trình xem trong modal rồi
3619
+ * nhấn Escape chỉ đóng trình xem, modal còn nguyên.
3620
+ *
3621
+ * Dùng riêng được (`[(open)]` + `[images]` + `[(index)]`); `ol-file-upload`
3622
+ * chế độ `image`/`images` dùng nó để xem ảnh vừa chọn.
3623
+ */
3624
+ declare class OlImagePreview {
3625
+ readonly images: _angular_core.InputSignal<OlPreviewImage[]>;
3626
+ readonly open: _angular_core.ModelSignal<boolean>;
3627
+ /** Ảnh đang xem, tính từ 0. */
3628
+ readonly index: _angular_core.ModelSignal<number>;
3629
+ readonly closed: _angular_core.OutputEmitterRef<void>;
3630
+ private readonly panel;
3631
+ private readonly thumbStrip;
3632
+ private readonly imageEl;
3633
+ protected readonly zoom: _angular_core.WritableSignal<ZoomState>;
3634
+ protected readonly dragging: _angular_core.WritableSignal<boolean>;
3635
+ protected readonly zoomPercent: _angular_core.Signal<number>;
3636
+ protected readonly canZoomIn: _angular_core.Signal<boolean>;
3637
+ protected readonly canZoomOut: _angular_core.Signal<boolean>;
3638
+ protected readonly imageStyle: _angular_core.Signal<string>;
3639
+ protected readonly imageClasses: _angular_core.Signal<string>;
3640
+ /** Ngón tay / chuột đang ấn trên ảnh, theo pointerId. */
3641
+ private readonly pointers;
3642
+ private pinchStart;
3643
+ private moved;
3644
+ /** Vừa kéo xong: cú `click` theo sau không được tính là bấm (phóng / đóng). */
3645
+ private suppressClick;
3646
+ private readonly focusTrap;
3647
+ private readonly overlayContainer;
3648
+ private releaseEscape;
3649
+ protected readonly count: _angular_core.Signal<number>;
3650
+ /** Kẹp chỉ số: danh sách ảnh có thể ngắn lại (vừa xoá ảnh) trong lúc đang mở. */
3651
+ protected readonly current: _angular_core.Signal<number>;
3652
+ protected readonly image: _angular_core.Signal<OlPreviewImage>;
3653
+ constructor();
3654
+ /** Ô ảnh nhỏ: ảnh đang xem rõ + viền trắng; ảnh khác mờ, rê chuột thì rõ. */
3655
+ protected thumbClasses(i: number): string;
3656
+ close(): void;
3657
+ next(): void;
3658
+ prev(): void;
3659
+ private go;
3660
+ protected onKeydown(event: KeyboardEvent): void;
3661
+ /** Bấm đúng nền tối (không phải ảnh hay nút) thì đóng. */
3662
+ protected onBackdropClick(event: MouseEvent): void;
3663
+ zoomIn(): void;
3664
+ zoomOut(): void;
3665
+ /** Bàn phím trên ảnh (Enter / Space): phóng 2× quanh tâm, hoặc về cỡ gốc. */
3666
+ toggleZoom(): void;
3667
+ resetZoom(): void;
3668
+ /** Kích thước ảnh khi CHƯA phóng (offsetWidth không tính transform). */
3669
+ private imageSize;
3670
+ /**
3671
+ * Điểm màn hình → toạ độ tính từ TÂM ảnh lúc chưa phóng. Hộp đã biến đổi có
3672
+ * tâm = tâm gốc + (x, y) — phóng quanh tâm không dời tâm.
3673
+ */
3674
+ private toCenter;
3675
+ private zoomAround;
3676
+ /** Bấm ảnh: đang ở cỡ gốc thì phóng 2× đúng chỗ bấm, đang phóng thì về. */
3677
+ protected onImageClick(event: MouseEvent): void;
3678
+ /** Cuộn chuột / chụm trackpad (trình duyệt gửi wheel + ctrlKey): phóng quanh con trỏ. */
3679
+ protected onWheel(event: WheelEvent): void;
3680
+ protected onPointerDown(event: PointerEvent): void;
3681
+ protected onPointerMove(event: PointerEvent): void;
3682
+ protected onPointerUp(event: PointerEvent): void;
3683
+ private pinchDistance;
3684
+ private releaseTraps;
3685
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlImagePreview, never>;
3686
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlImagePreview, "ol-image-preview", never, { "images": { "alias": "images"; "required": false; "isSignal": true; }; "open": { "alias": "open"; "required": false; "isSignal": true; }; "index": { "alias": "index"; "required": false; "isSignal": true; }; }, { "open": "openChange"; "index": "indexChange"; "closed": "closed"; }, never, never, true, never>;
3687
+ }
3688
+
3689
+ /**
3690
+ * - `single` — một tệp; đã có thì thẻ tệp + nút "Thay đổi"
3691
+ * - `multiple` — nhiều tệp dạng chip (👁 + tên + ✕), cuộn khi dài
3692
+ * - `image` — một ảnh bìa phủ kín khung; bấm ảnh để xem phóng to
3693
+ * - `images` — nhiều ảnh dạng lưới ô vuông; bấm ô để xem, lướt ‹ › qua các ảnh
3694
+ * - `list` — nhiều tệp dạng DÒNG có thanh tiến trình — Figma không có,
3695
+ * dùng khi tải tệp lớn cần báo tiến trình từng tệp
3696
+ */
3697
+ type FileUploadMode = 'single' | 'multiple' | 'image' | 'images' | 'list';
3698
+ /**
3699
+ * Khung tải tệp có tiêu đề — Figma `ol-file-input-form` "Button header + …"
3700
+ * (3587:6834 · 3592:6990 · 3603:2606 · 3611:3494).
3701
+ *
3702
+ * ## Component KHÔNG tự tải lên máy chủ
3703
+ *
3704
+ * Nó nhận tệp, kiểm định dạng/dung lượng/số lượng, thêm vào `files` với
3705
+ * `status: 'idle'` rồi phát `(filesAdded)`. Nơi gọi gửi HTTP và cập nhật
3706
+ * `status`/`progress` của từng tệp qua `[(files)]`. Để component tự gọi HTTP
3707
+ * thì nó phải biết URL, header xác thực, dạng phản hồi — ba thứ khác nhau ở
3708
+ * mỗi màn.
3709
+ */
3710
+ declare class OlFileUpload {
3711
+ readonly title: _angular_core.InputSignal<string>;
3712
+ readonly required: _angular_core.InputSignal<boolean>;
3713
+ readonly mode: _angular_core.InputSignal<FileUploadMode>;
3714
+ /** Chuỗi `accept` của `<input type="file">` — cũng dùng để KIỂM tệp kéo thả. */
3715
+ readonly accept: _angular_core.InputSignal<string>;
3716
+ /** Byte. 0 → không giới hạn. */
3717
+ readonly maxSize: _angular_core.InputSignal<number>;
3718
+ /** Chỉ cho `multiple`/`list`. 0 → không giới hạn. */
3719
+ readonly maxFiles: _angular_core.InputSignal<number>;
3720
+ /** Nhãn nút ở đầu khung. Rỗng → tự chọn theo chế độ và trạng thái. */
3721
+ readonly buttonLabel: _angular_core.InputSignal<string>;
3722
+ /** Hiện nút 👁 trên từng tệp. */
3723
+ readonly previewable: _angular_core.InputSignal<boolean>;
3724
+ readonly disabled: _angular_core.InputSignal<boolean>;
3725
+ /**
3726
+ * Nhận tệp dán (Ctrl+V / ⌘V) ở BẤT KỲ ĐÂU trên trang, không chỉ khi tiêu
3727
+ * điểm ở trong khung. Chỉ bật khi màn có MỘT khung tải — hai khung cùng bật
3728
+ * thì một lần dán vào cả hai. Đang gõ trong ô nhập chữ thì bỏ qua.
3729
+ */
3730
+ readonly pasteFromPage: _angular_core.InputSignal<boolean>;
3731
+ /** Dòng gợi ý dưới khung, ví dụ định dạng cho phép. */
3732
+ readonly helperText: _angular_core.InputSignal<string>;
3733
+ readonly files: _angular_core.ModelSignal<OlUploadFile[]>;
3734
+ /** Tệp VỪA nhận, đã qua kiểm — nơi gọi bắt đầu tải lên từ đây. */
3735
+ readonly filesAdded: _angular_core.OutputEmitterRef<File[]>;
3736
+ readonly fileRemove: _angular_core.OutputEmitterRef<OlUploadFile>;
3737
+ readonly filePreview: _angular_core.OutputEmitterRef<OlUploadFile>;
3738
+ readonly fileRetry: _angular_core.OutputEmitterRef<OlUploadFile>;
3739
+ /** Tệp bị loại kèm lý do — component đã tự hiện câu lỗi dưới khung. */
3740
+ readonly rejected: _angular_core.OutputEmitterRef<FileRejection[]>;
3741
+ private readonly fileInput;
3742
+ private readonly host;
3743
+ protected readonly uid: string;
3744
+ protected readonly errors: _angular_core.WritableSignal<string[]>;
3745
+ protected readonly isMultiple: _angular_core.Signal<boolean>;
3746
+ protected readonly isImageMode: _angular_core.Signal<boolean>;
3747
+ protected readonly hasFiles: _angular_core.Signal<boolean>;
3748
+ protected readonly limitReached: _angular_core.Signal<boolean>;
3749
+ protected readonly resolvedButtonLabel: _angular_core.Signal<string>;
3750
+ protected readonly hintText: _angular_core.Signal<string>;
3751
+ protected readonly describedBy: _angular_core.Signal<string>;
3752
+ /**
3753
+ * Ảnh xem trước cho chế độ `image`/`images`, theo id tệp: `url` có sẵn (ảnh
3754
+ * đã trên máy chủ) hoặc object URL tạo từ `File`.
3755
+ *
3756
+ * Object URL phải THU HỒI khi tệp bị xoá / component huỷ, không thì mỗi ảnh
3757
+ * rò một khối bộ nhớ bằng cả ảnh. Giữ theo id để thêm một ảnh không tạo lại
3758
+ * URL của các ảnh cũ (ảnh cũ sẽ nháy vì `src` đổi).
3759
+ */
3760
+ protected readonly imageUrls: _angular_core.WritableSignal<Record<string, string>>;
3761
+ private readonly objectUrls;
3762
+ /** Ảnh cho trình xem — chỉ tệp đã có URL. */
3763
+ protected readonly previewImages: _angular_core.Signal<OlPreviewImage[]>;
3764
+ protected readonly previewOpen: _angular_core.WritableSignal<boolean>;
3765
+ protected readonly previewIndex: _angular_core.WritableSignal<number>;
3766
+ constructor();
3767
+ /** Tạo URL cho ảnh mới, thu hồi URL của ảnh đã bị xoá. */
3768
+ private syncImageUrls;
3769
+ /** Mở trình xem tại ảnh của tệp này, và phát `(filePreview)`. */
3770
+ openPreview(file: OlUploadFile): void;
3771
+ openPicker(): void;
3772
+ protected onInputChange(event: Event): void;
3773
+ /** Nhận tệp từ hộp chọn hoặc kéo thả: kiểm, thêm vào `files`, phát sự kiện. */
3774
+ addFiles(incoming: File[]): void;
3775
+ /** Dán tệp khi tiêu điểm trong khung (nút, chip, dòng tệp). */
3776
+ protected onPaste(event: ClipboardEvent): void;
3777
+ /** Thả thêm tệp vào vùng chip đã có tệp. */
3778
+ protected onListDrop(event: DragEvent): void;
3779
+ protected removeFile(file: OlUploadFile): void;
3780
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlFileUpload, never>;
3781
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlFileUpload, "ol-file-upload", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "required": { "alias": "required"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "accept": { "alias": "accept"; "required": false; "isSignal": true; }; "maxSize": { "alias": "maxSize"; "required": false; "isSignal": true; }; "maxFiles": { "alias": "maxFiles"; "required": false; "isSignal": true; }; "buttonLabel": { "alias": "buttonLabel"; "required": false; "isSignal": true; }; "previewable": { "alias": "previewable"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "pasteFromPage": { "alias": "pasteFromPage"; "required": false; "isSignal": true; }; "helperText": { "alias": "helperText"; "required": false; "isSignal": true; }; "files": { "alias": "files"; "required": false; "isSignal": true; }; }, { "files": "filesChange"; "filesAdded": "filesAdded"; "fileRemove": "fileRemove"; "filePreview": "filePreview"; "fileRetry": "fileRetry"; "rejected": "rejected"; }, never, ["[actions]"], true, never>;
3073
3782
  }
3074
3783
 
3075
3784
  declare class OlRange implements ControlValueAccessor {
@@ -4095,8 +4804,34 @@ declare class OlNavbar {
4095
4804
  readonly onToggleMobileMenu: _angular_core.OutputEmitterRef<boolean>;
4096
4805
  searchQuery: string;
4097
4806
  private readonly elementRef;
4807
+ private readonly mobilePanel;
4808
+ private readonly navWrap;
4809
+ private readonly destroyRef;
4810
+ /** Nơi gọi có khai `slot="mobile-links"` (hoặc `slot="mobile"`) riêng. */
4811
+ readonly hasMobileLinks: _angular_core.WritableSignal<boolean>;
4812
+ /** Panel ☰ có gì để hiện — không thì đừng vẽ một dải trống có padding. */
4813
+ readonly hasPanelContent: _angular_core.WritableSignal<boolean>;
4814
+ constructor();
4815
+ /** Chiều cao khối mục xếp dọc (0 khi không xếp dọc) → `--ol-nav-stack-h`. */
4816
+ private observeStackHeight;
4817
+ private isStacked;
4818
+ /**
4819
+ * Đo lại mỗi lần mở ☰, không chỉ lúc đầu: nội dung chiếu vào có thể nằm
4820
+ * trong @if của nơi gọi và xuất hiện muộn.
4821
+ */
4822
+ private detectMobileSlots;
4823
+ /**
4824
+ * Khối mục menu. Khung ≥ 78rem: nằm ngang cạnh logo.
4825
+ *
4826
+ * Khung hẹp mà ☰ đang mở và KHÔNG có menu di động riêng: xuống dòng
4827
+ * (`col-span-full row-start-2`) và xếp dọc — chính các mục desktop thành menu
4828
+ * di động. Bản trước chỉ ẩn chúng, nên 5/7 thanh trên /navbar và /blocks bấm
4829
+ * ☰ ra bảng trống (đo iPhone 13 giả lập, 2026-09-28).
4830
+ */
4831
+ readonly navWrapClasses: _angular_core.Signal<string>;
4832
+ readonly mobilePanelClasses: _angular_core.Signal<"hidden" | "@[78rem]:hidden absolute inset-x-0 top-[calc(100%+var(--ol-nav-stack-h,0px))] z-dropdown max-h-[calc(100dvh-5rem)] overflow-y-auto border-t border-default bg-neutral-primary px-4 pt-4 pb-6 space-y-4 animate-in fade-in slide-in-from-top-2 duration-150 shadow-xl">;
4098
4833
  readonly navbarClasses: _angular_core.Signal<string>;
4099
- readonly containerClasses: _angular_core.Signal<"w-full px-4 sm:px-6" | "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">;
4834
+ readonly containerClasses: _angular_core.Signal<"w-full px-4 @[40rem]:px-6" | "max-w-7xl mx-auto px-4 @[40rem]:px-6 @5xl:px-8">;
4100
4835
  toggleMobileMenu(): void;
4101
4836
  handleSearchSubmit(): void;
4102
4837
  handleSearchInput(event: Event): void;
@@ -4125,7 +4860,40 @@ declare class OlNavDropdown {
4125
4860
  readonly label: _angular_core.InputSignal<string>;
4126
4861
  readonly icon: _angular_core.InputSignal<string | null>;
4127
4862
  readonly alignRight: _angular_core.InputSignal<boolean>;
4863
+ /**
4864
+ * Đang ở một trang CON của menu này (ví dụ "Lớp học" khi đang xem 10A1).
4865
+ * Cùng hình với `ol-navbar-item` active — nền brand nhạt, chữ brand đậm —
4866
+ * để thanh điều hướng luôn chỉ ra người dùng đang ở phân hệ nào.
4867
+ */
4868
+ readonly active: _angular_core.InputSignal<boolean>;
4869
+ protected readonly triggerClasses: _angular_core.Signal<string>;
4870
+ /**
4871
+ * Số cột của panel. `1` (mặc định) là menu thường; từ `2` là mega-menu.
4872
+ *
4873
+ * Mega-menu dùng cho menu có nhiều nhóm mục — thay vì một cột dài phải cuộn,
4874
+ * chia thành cột đứng cạnh nhau và người dùng quét mắt một lượt là thấy hết.
4875
+ *
4876
+ * Dưới `md` LUÔN về một cột bất kể giá trị: hai cột trên màn 390px là mỗi
4877
+ * cột 150px, chữ vỡ dòng giữa từ. Ngưỡng theo breakpoint chứ không theo bề
4878
+ * ngang panel vì panel treo tự do, không có container để đo.
4879
+ */
4880
+ readonly columns: _angular_core.InputSignal<1 | 2 | 3 | 4>;
4881
+ /**
4882
+ * Panel rộng bằng cả navbar thay vì bám mép nút.
4883
+ *
4884
+ * Mega-menu nhiều cột mà vẫn bám mép nút thì nửa panel tràn ra ngoài màn
4885
+ * hình khi nút nằm bên phải. Trải hết chiều ngang thì luôn nằm trong khung.
4886
+ */
4887
+ readonly fullWidth: _angular_core.InputSignal<boolean>;
4128
4888
  readonly isOpen: _angular_core.WritableSignal<boolean>;
4889
+ /**
4890
+ * Class của panel.
4891
+ *
4892
+ * Gom vào MỘT chuỗi thay vì nhiều `[class.x]` rời: hai class cùng thuộc tính
4893
+ * thì trình duyệt chọn theo thứ tự luật trong file CSS chứ không theo thứ tự
4894
+ * viết, kết quả đổi theo bản build.
4895
+ */
4896
+ protected readonly panelClasses: _angular_core.Signal<string>;
4129
4897
  private readonly elementRef;
4130
4898
  private readonly overlayContainer;
4131
4899
  constructor();
@@ -4133,9 +4901,120 @@ declare class OlNavDropdown {
4133
4901
  close(): void;
4134
4902
  handleClickOutside(event: MouseEvent): void;
4135
4903
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlNavDropdown, never>;
4136
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlNavDropdown, "ol-nav-dropdown", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "alignRight": { "alias": "alignRight"; "required": false; "isSignal": true; }; }, {}, never, ["[slot=trigger], [trigger]", "*"], true, never>;
4904
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlNavDropdown, "ol-nav-dropdown", never, { "label": { "alias": "label"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "alignRight": { "alias": "alignRight"; "required": false; "isSignal": true; }; "active": { "alias": "active"; "required": false; "isSignal": true; }; "columns": { "alias": "columns"; "required": false; "isSignal": true; }; "fullWidth": { "alias": "fullWidth"; "required": false; "isSignal": true; }; }, {}, never, ["[slot=trigger], [trigger]", "*"], true, never>;
4905
+ }
4906
+
4907
+ /** Một mục trong menu thả xuống. */
4908
+ interface OlMenubarItem {
4909
+ id: string;
4910
+ label: string;
4911
+ /** Chữ phụ bên phải: phím tắt, ví dụ "⌘S". */
4912
+ shortcut?: string;
4913
+ disabled?: boolean;
4914
+ /** Kẻ ngăn NGAY TRƯỚC mục này. */
4915
+ separatorBefore?: boolean;
4916
+ }
4917
+ /** Một menu trên thanh: nhãn trên thanh + danh sách mục. */
4918
+ interface OlMenubarMenu {
4919
+ id: string;
4920
+ label: string;
4921
+ items: OlMenubarItem[];
4922
+ disabled?: boolean;
4923
+ }
4924
+ /**
4925
+ * Thanh menu kiểu ứng dụng để bàn — Tệp · Sửa · Xem.
4926
+ *
4927
+ * Chưa có node Figma — dựng theo design system, dùng lại cách trình bày của
4928
+ * `ol-dropdown-item`.
4929
+ *
4930
+ * ## Khác `ol-dropdown` ở ba điểm, và đó là lý do có component riêng
4931
+ *
4932
+ * 1. **Mở một menu rồi RÊ sang menu kế là mở luôn**, không phải bấm lại. Đây
4933
+ * là hành vi người dùng đã quen từ phần mềm để bàn; thiếu nó thì thanh menu
4934
+ * chỉ là mấy cái dropdown đặt cạnh nhau.
4935
+ * 2. **Mũi tên trái/phải đi giữa các menu**, lên/xuống đi trong menu đang mở.
4936
+ * WAI-ARIA quy định `role="menubar"` phải làm vậy.
4937
+ * 3. **Một menu mở thì menu khác đóng** — không bao giờ có hai panel cùng lúc.
4938
+ *
4939
+ * ## Vì sao KHÔNG treo panel ra `<body>`
4940
+ *
4941
+ * Thanh menu thường nằm ngay dưới tiêu đề cửa sổ, không bị `overflow: hidden`
4942
+ * cắt như dropdown trong bảng. Giữ panel trong cây DOM thì Tab đi vào được tự
4943
+ * nhiên, không phải xử lý riêng như `ol-dropdown` đã phải làm.
4944
+ *
4945
+ * ## Lưu ý khi viết test
4946
+ *
4947
+ * Rê chuột giữa các menu cần bố cục thật để tính vị trí. jsdom không có, nên
4948
+ * spec khoá phần bàn phím và trạng thái mở/đóng; hành vi rê nghiệm thu trên
4949
+ * trình duyệt.
4950
+ */
4951
+ declare class OlMenubar {
4952
+ private readonly elementRef;
4953
+ private readonly destroyRef;
4954
+ private readonly overlayContainer;
4955
+ readonly menus: _angular_core.InputSignal<OlMenubarMenu[]>;
4956
+ readonly ariaLabel: _angular_core.InputSignal<string>;
4957
+ /** `id` của menu đang mở. Rỗng nghĩa là không menu nào mở. */
4958
+ private readonly openId;
4959
+ readonly openMenuId: _angular_core.Signal<string>;
4960
+ readonly itemSelect: _angular_core.OutputEmitterRef<{
4961
+ menu: OlMenubarMenu;
4962
+ item: OlMenubarItem;
4963
+ }>;
4964
+ private releaseEscape;
4965
+ constructor();
4966
+ protected toggle(menu: OlMenubarMenu): void;
4967
+ /**
4968
+ * Rê sang menu khác khi ĐANG có menu mở thì mở luôn menu đó.
4969
+ *
4970
+ * Không mở khi chưa menu nào mở — nếu không, lướt chuột ngang thanh là bung
4971
+ * ra một panel người dùng không định mở.
4972
+ */
4973
+ protected onMenuHover(menu: OlMenubarMenu): void;
4974
+ private open;
4975
+ private closeAll;
4976
+ protected onItemClick(menu: OlMenubarMenu, item: OlMenubarItem): void;
4977
+ /**
4978
+ * Mũi tên trái/phải đi giữa các menu — WAI-ARIA quy định cho `role="menubar"`.
4979
+ *
4980
+ * Vòng lại đầu khi tới cuối: thanh menu là một dải kín, không phải danh sách
4981
+ * có điểm dừng. Người dùng nhấn phải liên tục thì quay về menu đầu.
4982
+ *
4983
+ * Dùng `(keydown)` trần rồi tự lọc phím vì thẻ đã có `(click)` — hai listener
4984
+ * khác kiểu sự kiện trên cùng thẻ thì Angular suy `$event` theo cái đầu.
4985
+ */
4986
+ protected onTriggerKeydown(event: KeyboardEvent, index: number): void;
4987
+ /**
4988
+ * Menu kế tiếp **còn dùng được** theo hướng `step`, vòng lại khi tới biên.
4989
+ *
4990
+ * Phải bỏ qua menu `disabled`: nút gốc có thuộc tính `disabled` nên
4991
+ * `.focus()` trên nó KHÔNG có tác dụng nào — tiêu điểm đứng nguyên chỗ cũ và
4992
+ * người dùng tưởng bàn phím hỏng. Spec bắt đúng lỗi này khi menu cuối bị vô
4993
+ * hiệu: nhấn End là tiêu điểm rơi về `<body>`.
4994
+ *
4995
+ * Trả `null` khi KHÔNG có menu nào dùng được — lúc đó không `preventDefault`,
4996
+ * để phím về lại hành vi mặc định của trình duyệt.
4997
+ *
4998
+ * Nhận `from` ngoài dải (-1 hoặc `length`) để Home/End dùng chung một phép:
4999
+ * Home là "từ trước mục đầu, đi sang phải", End là "từ sau mục cuối, đi sang
5000
+ * trái".
5001
+ */
5002
+ private nextEnabled;
5003
+ protected triggerClasses(menu: OlMenubarMenu): string;
5004
+ protected itemClasses(item: OlMenubarItem): string;
5005
+ protected readonly hasMenus: _angular_core.Signal<boolean>;
5006
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlMenubar, never>;
5007
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlMenubar, "ol-menubar", never, { "menus": { "alias": "menus"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, { "itemSelect": "itemSelect"; }, never, never, true, never>;
4137
5008
  }
4138
5009
 
5010
+ /**
5011
+ * Bố cục thanh phân trang (Figma `ol-pagination` #1149:81850).
5012
+ *
5013
+ * `with-dropdown` là BÍ DANH: ≡ `text-buttons` + `[withDropdown]="true"`. Ô
5014
+ * chọn cỡ trang nay là cờ riêng gắn được vào MỌI type; giữ giá trị này vì gói
5015
+ * đã publish. (Chú thích đặt ở đây, không chen giữa các giá trị: guard
5016
+ * `check:gia-tri-ma-mau` đọc union sẽ đứt ở khối chú thích.)
5017
+ */
4139
5018
  type PaginationType = 'default' | 'arrows' | 'dropdown-buttons' | 'text-buttons' | 'two-buttons' | 'buttons-helper' | 'with-dropdown' | 'with-input' | 'simple-input'
4140
5019
  /**
4141
5020
  * `grouped` và `separated` trước chỉ có ở `PaginationVariant`. Thêm vào đây
@@ -4180,6 +5059,17 @@ declare class OlPagination {
4180
5059
  readonly showText: _angular_core.InputSignal<boolean>;
4181
5060
  readonly text: _angular_core.InputSignal<string>;
4182
5061
  readonly showInfo: _angular_core.InputSignal<boolean>;
5062
+ /**
5063
+ * Thêm ô chọn số dòng mỗi trang ("10 mỗi trang") vào BẤT KỲ type nào — cạnh
5064
+ * dãy số, cạnh hai nút, cạnh ô nhập trang… Trước đây nó chỉ có ở một type
5065
+ * riêng (`with-dropdown`), muốn nó cạnh `arrows` hay `two-buttons` là không
5066
+ * được. Danh sách lấy từ `pageSizeOptions`, đổi thì phát `(pageChange)` về 1.
5067
+ */
5068
+ readonly withDropdown: _angular_core.InputSignal<boolean>;
5069
+ /**
5070
+ * @deprecated Dùng `withDropdown`. Bí danh: bật là thêm ô chọn cỡ trang. Khi
5071
+ * không đặt `type`, nó vẫn chọn bố cục `with-dropdown` như trước.
5072
+ */
4183
5073
  readonly showSizeChanger: _angular_core.InputSignal<boolean>;
4184
5074
  readonly showQuickJumper: _angular_core.InputSignal<boolean>;
4185
5075
  readonly pageChange: _angular_core.OutputEmitterRef<number>;
@@ -4193,6 +5083,10 @@ declare class OlPagination {
4193
5083
  * xảy ra. `type` đặt tường minh vẫn thắng; cờ chỉ điền vào chỗ trống.
4194
5084
  */
4195
5085
  readonly effectiveType: _angular_core.Signal<PaginationType>;
5086
+ /** Bố cục vẽ ra: `with-dropdown` là bí danh nên quy về `text-buttons`. */
5087
+ readonly layoutType: _angular_core.Signal<PaginationType>;
5088
+ /** Có vẽ ô chọn cỡ trang không — cờ riêng, độc lập với type. */
5089
+ readonly hasPageSizeDropdown: _angular_core.Signal<boolean>;
4196
5090
  /**
4197
5091
  * Trang đang xem — gộp `pageIndex` và bí danh `currentPage`.
4198
5092
  *
@@ -4264,7 +5158,7 @@ declare class OlPagination {
4264
5158
  onPageSizeSelect(event: Event): void;
4265
5159
  handleJump(): void;
4266
5160
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlPagination, never>;
4267
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlPagination, "ol-pagination", never, { "type": { "alias": "type"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "total": { "alias": "total"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageIndex": { "alias": "pageIndex"; "required": false; "isSignal": true; }; "currentPage": { "alias": "currentPage"; "required": false; "isSignal": true; }; "totalPagesInput": { "alias": "totalPages"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "showText": { "alias": "showText"; "required": false; "isSignal": true; }; "text": { "alias": "text"; "required": false; "isSignal": true; }; "showInfo": { "alias": "showInfo"; "required": false; "isSignal": true; }; "showSizeChanger": { "alias": "showSizeChanger"; "required": false; "isSignal": true; }; "showQuickJumper": { "alias": "showQuickJumper"; "required": false; "isSignal": true; }; }, { "pageSize": "pageSizeChange"; "pageIndex": "pageIndexChange"; "currentPage": "currentPageChange"; "pageChange": "pageChange"; }, never, never, true, never>;
5161
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlPagination, "ol-pagination", never, { "type": { "alias": "type"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "total": { "alias": "total"; "required": false; "isSignal": true; }; "pageSize": { "alias": "pageSize"; "required": false; "isSignal": true; }; "pageIndex": { "alias": "pageIndex"; "required": false; "isSignal": true; }; "currentPage": { "alias": "currentPage"; "required": false; "isSignal": true; }; "totalPagesInput": { "alias": "totalPages"; "required": false; "isSignal": true; }; "pageSizeOptions": { "alias": "pageSizeOptions"; "required": false; "isSignal": true; }; "showText": { "alias": "showText"; "required": false; "isSignal": true; }; "text": { "alias": "text"; "required": false; "isSignal": true; }; "showInfo": { "alias": "showInfo"; "required": false; "isSignal": true; }; "withDropdown": { "alias": "withDropdown"; "required": false; "isSignal": true; }; "showSizeChanger": { "alias": "showSizeChanger"; "required": false; "isSignal": true; }; "showQuickJumper": { "alias": "showQuickJumper"; "required": false; "isSignal": true; }; }, { "pageSize": "pageSizeChange"; "pageIndex": "pageIndexChange"; "currentPage": "currentPageChange"; "pageChange": "pageChange"; }, never, never, true, never>;
4268
5162
  }
4269
5163
 
4270
5164
  type PopoverPlacement = 'top' | 'bottom' | 'left' | 'right';
@@ -4320,6 +5214,29 @@ declare class OlPopover {
4320
5214
  private cleanupAutoUpdate;
4321
5215
  /** Hàm trả panel về chỗ cũ khi đóng. */
4322
5216
  private detachPanel;
5217
+ /**
5218
+ * Selector cho phần tử nhận được tiêu điểm.
5219
+ *
5220
+ * `tabindex="-1"` bị loại: đó là dấu hiệu tác giả cố ý đưa phần tử ra khỏi
5221
+ * đường Tab. `:disabled` cũng loại — trình duyệt không cho nó nhận tiêu điểm.
5222
+ */
5223
+ private static readonly FOCUSABLE;
5224
+ /**
5225
+ * Phần tử nhận tiêu điểm bên trong `root`, theo thứ tự nhìn thấy.
5226
+ *
5227
+ * Lọc phần ẩn bằng `[hidden]`/`[aria-hidden]` chứ KHÔNG `offsetParent`:
5228
+ * `offsetParent` là `null` với mọi phần tử trong `position: fixed` — mà panel
5229
+ * nổi hay dùng đúng kiểu định vị đó — nên danh sách sẽ rỗng và toàn bộ xử lý
5230
+ * Tab lặng lẽ không chạy.
5231
+ */
5232
+ private focusablesIn;
5233
+ /**
5234
+ * Phần tử nhận tiêu điểm đứng ngay sau `element` trong thứ tự Tab của trang.
5235
+ *
5236
+ * Loại chính panel ra: nó treo cuối `<body>` nên nếu tính cả thì "phần tử sau
5237
+ * nút trigger" lại rơi vào panel vừa đóng.
5238
+ */
5239
+ private nextFocusableAfter;
4323
5240
  constructor();
4324
5241
  toggle(): void;
4325
5242
  open(): void;
@@ -4607,6 +5524,99 @@ declare class OlRating {
4607
5524
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlRating, "ol-rating", never, { "rating": { "alias": "rating"; "required": false; "isSignal": true; }; "max": { "alias": "max"; "required": false; "isSignal": true; }; "type": { "alias": "type"; "required": false; "isSignal": true; }; "score": { "alias": "score"; "required": false; "isSignal": true; }; "badgeText": { "alias": "badgeText"; "required": false; "isSignal": true; }; "minLabel": { "alias": "minLabel"; "required": false; "isSignal": true; }; "maxLabel": { "alias": "maxLabel"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "name": { "alias": "name"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, never>;
4608
5525
  }
4609
5526
 
5527
+ type OlResizableDirection = 'horizontal' | 'vertical';
5528
+ /**
5529
+ * Hai khung chia nhau một vùng, kéo vạch giữa để đổi tỉ lệ.
5530
+ *
5531
+ * Chưa có node Figma — dựng theo design system: vạch kéo dùng `bg-default`,
5532
+ * sáng lên `bg-brand` khi rê chuột hoặc đang kéo.
5533
+ *
5534
+ * ## Vì sao tỉ lệ phần trăm chứ không pixel
5535
+ *
5536
+ * Người dùng đổi cỡ cửa sổ thì hai khung phải giãn theo. Lưu pixel thì khung
5537
+ * trái giữ nguyên còn khung phải nuốt toàn bộ phần thêm — tỉ lệ người dùng
5538
+ * chọn biến mất.
5539
+ *
5540
+ * ## Bắt chuột trên `document`, không trên vạch
5541
+ *
5542
+ * Kéo nhanh thì con trỏ rời khỏi vạch trước khi trình duyệt kịp gửi sự kiện
5543
+ * tiếp theo; nghe trên chính vạch thì kéo đứt đoạn. Nghe trên `document` lúc
5544
+ * đang kéo và gỡ ngay khi thả.
5545
+ *
5546
+ * `setPointerCapture` giải quyết đúng việc này nhưng không có trong jsdom, nên
5547
+ * spec sẽ không chạy được nhánh kéo — dùng `document` thì cả hai môi trường
5548
+ * cùng một đường mã.
5549
+ *
5550
+ * ## Bàn phím
5551
+ *
5552
+ * Vạch là `role="separator"` có `tabindex`, mũi tên đổi 2% mỗi lần, Home/End
5553
+ * về hai biên. Không có phần này thì người dùng bàn phím không đổi được tỉ lệ
5554
+ * — và một khung chia đôi không kéo được thì chỉ là hai `<div>` cạnh nhau.
5555
+ *
5556
+ * ## Lưu ý khi viết test
5557
+ *
5558
+ * jsdom không tính bố cục: `getBoundingClientRect()` trả 0 cho mọi thứ, nên
5559
+ * phép quy đổi toạ độ chuột sang phần trăm luôn ra `NaN` hoặc 0. Spec khoá
5560
+ * phần bàn phím và ràng buộc min/max (tính thuần số); việc kéo chuột nghiệm
5561
+ * thu trên trình duyệt.
5562
+ */
5563
+ declare class OlResizable {
5564
+ private readonly elementRef;
5565
+ private readonly destroyRef;
5566
+ readonly direction: _angular_core.InputSignal<OlResizableDirection>;
5567
+ /** Tỉ lệ khung ĐẦU, tính bằng phần trăm. Hai chiều để nơi dùng lưu lại được. */
5568
+ readonly ratio: _angular_core.ModelSignal<number>;
5569
+ /** Chặn dưới và chặn trên, phần trăm. Khung nào cũng không nhỏ hơn `minRatio`. */
5570
+ readonly minRatio: _angular_core.InputSignal<number>;
5571
+ readonly maxRatio: _angular_core.InputSignal<number>;
5572
+ /** Bước đổi mỗi lần nhấn mũi tên, phần trăm. */
5573
+ readonly keyboardStep: _angular_core.InputSignal<number>;
5574
+ readonly disabled: _angular_core.InputSignal<boolean>;
5575
+ readonly ariaLabel: _angular_core.InputSignal<string>;
5576
+ /**
5577
+ * Bắn khi thả chuột hoặc nhấn phím — KHÔNG bắn liên tục lúc đang kéo.
5578
+ *
5579
+ * Tên là `resizeEnd` chứ không `ratioChange`: `model()` đã tự sinh
5580
+ * `ratioChange` cho binding hai chiều, khai thêm output trùng tên là lỗi
5581
+ * biên dịch NG1054. Hai thứ cũng khác nghĩa — `ratioChange` bắn mỗi lần giá
5582
+ * trị đổi (kể cả từng nhịp kéo), còn `resizeEnd` chỉ bắn khi người dùng đã
5583
+ * thả tay, đúng lúc nên lưu tỉ lệ xuống máy chủ.
5584
+ */
5585
+ readonly resizeEnd: _angular_core.OutputEmitterRef<number>;
5586
+ private readonly dragging;
5587
+ private releaseDrag;
5588
+ constructor();
5589
+ /** Ép tỉ lệ vào khoảng cho phép. Tách ra để spec khoá được mà không cần DOM. */
5590
+ clamp(value: number): number;
5591
+ protected onPointerDown(event: PointerEvent): void;
5592
+ private stopDrag;
5593
+ private applyFromPointer;
5594
+ protected onKeydown(event: KeyboardEvent): void;
5595
+ protected readonly rootClasses: _angular_core.Signal<"flex h-full w-full" | "flex h-full w-full flex-col">;
5596
+ /** Kích thước khung đầu — `flex-basis` phần trăm, khung sau lấp phần còn lại. */
5597
+ protected readonly firstStyle: _angular_core.Signal<string>;
5598
+ /**
5599
+ * Vạch chia.
5600
+ *
5601
+ * ## Vùng bấm rộng hơn vạch nhìn thấy
5602
+ *
5603
+ * Vạch chỉ dày 6px thì đúng thẩm mỹ nhưng vùng bấm 6px vi phạm WCAG 2.5.8
5604
+ * (ngưỡng 24px) — đo trên trình duyệt ra `6×188px`. Nên vạch nhận
5605
+ * `w-6`/`h-6` để vùng bấm đủ 24px, còn phần MÀU nằm ở `::before` chỉ
5606
+ * 6px ở giữa. Người dùng thấy vạch mảnh, tay và con trỏ thì có 24px để bắt.
5607
+ *
5608
+ * `::before` khai trong `handleBarClasses()` và đặt qua một `<span>` con chứ
5609
+ * không dùng biến thể `before:` của Tailwind: `before:` cần nhiều class cho
5610
+ * cùng một hình và khó đọc hơn một phần tử thật.
5611
+ */
5612
+ protected readonly handleClasses: _angular_core.Signal<string>;
5613
+ /** Phần MÀU của vạch — mảnh 6px, nằm giữa vùng bấm 24px. */
5614
+ protected readonly handleBarClasses: _angular_core.Signal<string>;
5615
+ protected readonly ariaOrientation: _angular_core.Signal<"vertical" | "horizontal">;
5616
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlResizable, never>;
5617
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlResizable, "ol-resizable", never, { "direction": { "alias": "direction"; "required": false; "isSignal": true; }; "ratio": { "alias": "ratio"; "required": false; "isSignal": true; }; "minRatio": { "alias": "minRatio"; "required": false; "isSignal": true; }; "maxRatio": { "alias": "maxRatio"; "required": false; "isSignal": true; }; "keyboardStep": { "alias": "keyboardStep"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; }, { "ratio": "ratioChange"; "resizeEnd": "resizeEnd"; }, never, ["[first]", "[second]"], true, never>;
5618
+ }
5619
+
4610
5620
  /**
4611
5621
  * Kiểu dữ liệu cho `ol-app-nav` — menu điều hướng Ôn Luyện.
4612
5622
  *
@@ -5402,13 +6412,13 @@ declare class OlToastContainer {
5402
6412
  * cùng một union, tách thành hai kiểu riêng là để người dùng phải nhớ chúng
5403
6413
  * bằng nhau.
5404
6414
  */
5405
- type OlTooltipTheme = 'dark' | 'light' | 'brand';
6415
+ type OlTooltipTheme = 'subtle' | 'dark' | 'light' | 'brand';
5406
6416
  /** Hướng hiện tooltip so với phần tử đích. */
5407
6417
  type OlTooltipPosition = 'top' | 'bottom' | 'left' | 'right';
5408
6418
  declare class OlTooltip {
5409
6419
  /** Tooltip content text */
5410
6420
  readonly content: _angular_core.InputSignal<string>;
5411
- /** Theme variant */
6421
+ /** Theme variant — mặc định `subtle` (nền Primary 50). */
5412
6422
  readonly theme: _angular_core.InputSignal<OlTooltipTheme>;
5413
6423
  /** Tooltip ID for ARIA */
5414
6424
  readonly tooltipId: _angular_core.InputSignal<string>;
@@ -5585,6 +6595,134 @@ declare class OlIconService {
5585
6595
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<any>;
5586
6596
  }
5587
6597
 
6598
+ type OlIconStackSize = 'sm' | 'base' | 'lg';
6599
+ /**
6600
+ * Chồng nhiều icon hoặc logo lên nhau, kèm số đếm "+N".
6601
+ *
6602
+ * Chưa có node Figma — dựng theo `ol-avatar-group`, component cùng cơ chế đã
6603
+ * có trong thư viện. Khác ở chỗ: avatar là ảnh người nên luôn tròn, còn icon
6604
+ * thường là biểu tượng dịch vụ hay môn học nên mặc định bo vừa (`rounded-lg`)
6605
+ * và có nền để icon một màu vẫn tách khỏi icon bên dưới.
6606
+ *
6607
+ * ## Vòng viền là thứ làm nên hiệu ứng chồng
6608
+ *
6609
+ * Mỗi ô có `ring-2 ring-buffer` — vòng viền màu nền trang, không phải viền
6610
+ * màu. Nhờ nó hai ô chồng nhau vẫn thấy rõ ranh giới. Bỏ vòng này thì các ô
6611
+ * dính thành một khối nhoè.
6612
+ *
6613
+ * `ring-buffer` chứ không `ring-white`: ở chế độ tối nền không phải trắng, và
6614
+ * viền trắng trên nền đen là đường kẻ sáng chói quanh mỗi icon.
6615
+ *
6616
+ * ## Thứ tự chồng
6617
+ *
6618
+ * Mặc định ô ĐẦU nằm trên — giống xếp bài. `reverse` lật lại cho ô cuối nằm
6619
+ * trên, dùng khi ô cuối là thứ cần nhấn (icon "thêm", trạng thái mới nhất).
6620
+ *
6621
+ * Làm bằng `flex-row-reverse` cộng đảo thứ tự dữ liệu chứ không bằng
6622
+ * `z-index`: `z-index` cần stacking context và sẽ hỏng khi nơi gọi đặt
6623
+ * component vào một phần tử có `transform` hay `opacity`.
6624
+ */
6625
+ declare class OlIconStack {
6626
+ readonly size: _angular_core.InputSignal<OlIconStackSize>;
6627
+ /** Số đếm "+N" ở cuối. `null` thì không vẽ. */
6628
+ readonly counter: _angular_core.InputSignal<string | number | null>;
6629
+ readonly counterAriaLabel: _angular_core.InputSignal<string>;
6630
+ /** Lật thứ tự chồng: ô CUỐI nằm trên thay vì ô đầu. */
6631
+ readonly reverse: _angular_core.InputSignal<boolean>;
6632
+ /**
6633
+ * Bo góc của từng ô. `full` cho logo tròn, `lg` cho biểu tượng vuông.
6634
+ *
6635
+ * Áp lên CON qua biến thể `*:` của Tailwind, nên nơi gọi chỉ việc đặt
6636
+ * `<ol-icon>` hay `<img>` vào là xong.
6637
+ */
6638
+ readonly rounded: _angular_core.InputSignal<"lg" | "full">;
6639
+ protected readonly rootClasses: _angular_core.Signal<string>;
6640
+ /**
6641
+ * Ô đếm "+N".
6642
+ *
6643
+ * `text-heading` chứ KHÔNG `text-body`: tổ hợp `bg-neutral-quaternary` +
6644
+ * `text-body` đo được 6.10 ở chế độ sáng nhưng chỉ 3.96 ở chế độ tối — dưới
6645
+ * ngưỡng WCAG 4.5. Đây là kết luận đã đo ở `ol-avatar-group` (BUG-016 mục 5),
6646
+ * theo tiền lệ sẵn có chứ không đoán lại.
6647
+ *
6648
+ * Chú thích để Ở ĐÂY chứ không trong template: template là một chuỗi
6649
+ * template literal, nên dấu backtick trong chú thích HTML đóng chuỗi sớm và
6650
+ * TS báo lỗi ở dòng chẳng liên quan (đã mắc khi viết file này).
6651
+ */
6652
+ protected readonly counterClasses: _angular_core.Signal<string>;
6653
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlIconStack, never>;
6654
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlIconStack, "ol-icon-stack", never, { "size": { "alias": "size"; "required": false; "isSignal": true; }; "counter": { "alias": "counter"; "required": false; "isSignal": true; }; "counterAriaLabel": { "alias": "counterAriaLabel"; "required": false; "isSignal": true; }; "reverse": { "alias": "reverse"; "required": false; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; }, {}, never, ["*"], true, never>;
6655
+ }
6656
+
6657
+ type OlItemSize = 'sm' | 'base' | 'lg';
6658
+ type OlItemAlign = 'start' | 'center';
6659
+ /**
6660
+ * Một dòng nội dung: phần dẫn đầu · tiêu đề + mô tả · phần hành động.
6661
+ *
6662
+ * Chưa có node Figma — dựng theo design system, lấy khoảng cách và cỡ chữ từ
6663
+ * `ol-checkbox-radio-form` (component gần nhất đang có cấu trúc này).
6664
+ *
6665
+ * ## Vì sao KHÔNG mở rộng `ol-list-group-item`
6666
+ *
6667
+ * Cái đó là `<li>` và phải nằm trong `<ul>` của `ol-list-group` — HTML quy
6668
+ * định vậy. Còn dòng nội dung thì hay đứng một mình trong thẻ, trong ngăn kéo,
6669
+ * hay trong kết quả tìm kiếm. Nhồi cấu trúc vào `<li>` là buộc mọi nơi dùng
6670
+ * phải bọc thêm `<ul>` không có ý nghĩa ngữ nghĩa nào.
6671
+ *
6672
+ * Hai component dùng chung cách xếp; `ol-item` lo phần bên trong, đặt ở đâu là
6673
+ * việc của nơi gọi.
6674
+ *
6675
+ * ## Ba khe, KHÔNG phải ba input
6676
+ *
6677
+ * `[lead]` và `[actions]` là khe cắm chứ không phải input nhận tên icon: phần
6678
+ * dẫn đầu có thể là icon, avatar, ảnh bìa hay số thứ tự; phần hành động có thể
6679
+ * là nút, huy hiệu, hay một dòng chữ. Nhận tên icon thì mỗi lần cần thứ khác
6680
+ * lại phải thêm một input.
6681
+ *
6682
+ * Riêng `icon` giữ lại làm lối tắt cho ca hay gặp nhất. Nó nằm TRONG cùng khe
6683
+ * `[lead]`, sau nội dung chiếu — nơi gọi truyền cả hai thì có hai phần dẫn đầu
6684
+ * cạnh nhau, nhưng đó là ca hiếm và không đáng thêm selector CSS để chặn.
6685
+ */
6686
+ declare class OlItem {
6687
+ readonly title: _angular_core.InputSignal<string>;
6688
+ readonly description: _angular_core.InputSignal<string>;
6689
+ /** Lối tắt cho phần dẫn đầu dạng icon. Khe `[lead]` đè lên nó. */
6690
+ readonly icon: _angular_core.InputSignal<string>;
6691
+ readonly size: _angular_core.InputSignal<OlItemSize>;
6692
+ /**
6693
+ * Căn phần dẫn đầu và hành động theo chiều dọc.
6694
+ *
6695
+ * `start` khi mô tả dài hai ba dòng — icon căn giữa một khối cao trông như
6696
+ * trôi lơ lửng. `center` khi chỉ có tiêu đề.
6697
+ */
6698
+ readonly align: _angular_core.InputSignal<OlItemAlign>;
6699
+ /** Bấm được: thêm hiệu ứng hover và con trỏ tay, phát `itemClick`. */
6700
+ readonly interactive: _angular_core.InputSignal<boolean>;
6701
+ readonly disabled: _angular_core.InputSignal<boolean>;
6702
+ /** Biến cả dòng thành liên kết. Bỏ qua khi `disabled`. */
6703
+ readonly href: _angular_core.InputSignal<string | null>;
6704
+ readonly itemClick: _angular_core.OutputEmitterRef<MouseEvent>;
6705
+ /**
6706
+ * Enter và Space kích hoạt dòng `interactive`, như một `<button>` thật.
6707
+ *
6708
+ * Dùng `(keydown)` trần rồi tự lọc phím, KHÔNG `(keydown.enter)`: hai
6709
+ * listener khác kiểu sự kiện trên cùng một thẻ thì Angular suy `$event` theo
6710
+ * cái đầu — `(click)` đứng trước nên `$event` thành `MouseEvent` và TS báo
6711
+ * lỗi ở dòng `keydown`.
6712
+ *
6713
+ * `preventDefault` cho Space, nếu không trang cuộn xuống một màn cùng lúc.
6714
+ */
6715
+ protected onKeyActivate(event: KeyboardEvent): void;
6716
+ protected onClick(event: MouseEvent): void;
6717
+ protected readonly rootClasses: _angular_core.Signal<string>;
6718
+ protected readonly titleClasses: _angular_core.Signal<string>;
6719
+ protected readonly descriptionClasses: _angular_core.Signal<string>;
6720
+ /** Cỡ icon lối tắt, theo cỡ dòng. */
6721
+ protected readonly iconSize: _angular_core.Signal<number>;
6722
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<OlItem, never>;
6723
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<OlItem, "ol-item", never, { "title": { "alias": "title"; "required": false; "isSignal": true; }; "description": { "alias": "description"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "align": { "alias": "align"; "required": false; "isSignal": true; }; "interactive": { "alias": "interactive"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "href": { "alias": "href"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, never, ["[lead]", "*", "[actions]"], true, never>;
6724
+ }
6725
+
5588
6726
  type OlBottomNavPosition = 'fixed' | 'relative';
5589
6727
  declare class OlBottomNav {
5590
6728
  readonly bordered: _angular_core.InputSignal<boolean>;
@@ -6423,6 +7561,24 @@ declare class OlFocusTrapService {
6423
7561
  * @param autoFocus Tự động focus vào element đầu tiên (default: true)
6424
7562
  */
6425
7563
  trap(element: HTMLElement, autoFocus?: boolean): void;
7564
+ /**
7565
+ * Chọn phần tử nhận focus khi overlay mở.
7566
+ *
7567
+ * Bản trước lấy phần tử focus được ĐẦU TIÊN theo thứ tự DOM. Header đứng
7568
+ * trước nội dung nên mở modal/drawer nào tiêu điểm cũng rơi vào nút ✕ —
7569
+ * người dùng phải Tab qua nút đóng mới tới ô nhập đầu tiên, và lỡ Enter là
7570
+ * đóng mất hộp thoại vừa mở (báo 2026-09-28).
7571
+ *
7572
+ * Thứ tự ưu tiên:
7573
+ * 1. phần tử nơi gọi đánh dấu `autofocus` / `data-autofocus` — trình duyệt
7574
+ * chỉ tôn trọng `autofocus` lúc tải trang, không phải lúc chèn hộp thoại,
7575
+ * nên service tự làm
7576
+ * 2. phần tử đầu tiên KHÔNG phải nút đóng (`[data-ol-dismiss]`) — thường là ô
7577
+ * nhập đầu tiên trong nội dung; nội dung không có gì bấm được thì tới nút
7578
+ * đầu tiên ở footer
7579
+ * 3. không còn gì khác mới về nút đóng, như cũ
7580
+ */
7581
+ private initialFocusTarget;
6426
7582
  /**
6427
7583
  * Release focus trap và restore focus về element trước đó
6428
7584
  */
@@ -6458,6 +7614,15 @@ declare class OlFocusTrapService {
6458
7614
  * Update danh sách focusable elements trong trap element
6459
7615
  */
6460
7616
  private updateFocusableElements;
7617
+ /**
7618
+ * Phần tử có đang hiện không.
7619
+ *
7620
+ * KHÔNG dùng `offsetParent !== null`: nó trả `null` cho chính phần tử
7621
+ * `position: fixed` (nút nổi trong drawer/modal) dù phần tử hiện rõ, và luôn
7622
+ * `null` trong jsdom — nên mọi phần tử bị lọc mất, service không focus được
7623
+ * gì và spec không viết được.
7624
+ */
7625
+ private isVisible;
6461
7626
  /**
6462
7627
  * Handle keyboard navigation trong trap
6463
7628
  */
@@ -6744,6 +7909,6 @@ declare class OlOverlayContainer {
6744
7909
  static ɵprov: _angular_core.ɵɵInjectableDeclaration<any>;
6745
7910
  }
6746
7911
 
6747
- export { CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_LABEL, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileInput, OlFloatingLabel, OlFocusTrapService, OlFooter, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlInput, OlInputCounter, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlRichTextEditor, OlScrollArea, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, endOfDay, fieldSizeClasses, formatDate, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isSameDay, isSameMonth, isSameYear, isToday, parseDate, startOfDay, stripDiacritics };
6748
- export type { AccordionVariant, AlertColor, AlertDialogTone, AlertLayout, AlertRounded, AlertType, AlertVariant, AppDevice, AppNavConfig, AppNavGroup, AppNavItem, AppNavUser, AppTheme, AutocompleteOption, AutocompleteSize, AutocompleteState, AvatarShape, AvatarSize, AvatarStatus, BadgeColor, BadgeShape, BadgeSize, BadgeTheme, BadgeType, BannerPosition, BannerVariant, BoxplotDataPoint, BreadcrumbItemStatus, BreadcrumbSeparator, BreadcrumbVariant, ButtonColor, ButtonGroupColor, ButtonGroupOrientation, ButtonGroupSize, ButtonSize, CardPadding, CardRadius, CardShadow, CardVariant, ChartDataPoint, ChartSeries, ChartType, ChatBubbleStatus, ChatSender, CheckboxSize, ChoiceControl, ChoiceControlPosition, ChoiceGroupLayout, ChoiceLayout, ChoiceShape, CommandGroup, CommandItem, DateCustomClass, DateRange, DatepickerPreset, DatepickerSelectionMode, DatepickerSize, DatepickerViewMode, DayCell, DeviceType, DrawerFooterLayout, DrawerPlacement, DrawerSize, DropdownAutoClose, DropdownPlacement, DropdownTrigger, EmptySize, FieldSize, FieldState, FileInputSize, FloatingLabelSize, FloatingLabelState, FloatingLabelVariant, HeatmapData, HelperFeature, HelperRule, HelperSize, HelperState, IconColor, IconShapeColor, IconShapeSize, IconShapeType, IconSize, IconVariant, InputCounterSize, InputSize, InputState, JumbotronVariant, ListGroupType, ModalFooterLayout, ModalPlacement, ModalSize, MonthCell, OlAvatarStatusPosition, OlBottomNavPosition, OlButtonType, OlCheckboxRadioFormAlign, OlCheckboxRadioGroupRole, OlDateRangePickerVariant, OlGalleryGap, OlGalleryRatio, OlHelperType, OlModalBackdrop, OlModalRole, OlSkeletonAnimation, OlSkeletonAspectRatio, OlSkeletonRounded, OlSkeletonType, OlSpeedDialTooltipPosition, OlTimepickerAddonType, OlToggleInputAlign, OlTreeNode, OlTreeNodeSelectionMode, OlTreeNodeToggleIcon, OlTreeSelectionMode, OlTreeToggleIcon, OtpInputSize, OverlayLayer, PaginationType, PaginationVariant, PopoverPlacement, PopoverTrigger, PositionedCoordinates, PositioningOptions, ProgressColor, ProgressLabelPosition, ProgressSize, RadarIndicator, RadioSize, RatingType, ScatterDataPoint, ScatterSeries, ScrollAreaAxis, ScrollAreaScrollbar, SelectOption, SelectSize, SelectState, SeparatorOrientation, SpeedDialAction, SpeedDialColor, SpeedDialDirection, SpeedDialTrigger, SpeedDialType, StepState, StepperBreakpoint, StepperOrientation, StepperVariant, TabsSize, TabsVariant, TagInputSize, TextareaState, TimeObject, TimepickerFormat, TimepickerSize, TimepickerState, Toast, ToastConfig, ToastPosition, ToggleColor, TogglePosition, ToggleSize, YearCell };
7912
+ export { CHAT_BUBBLE_STATUS, FIELD_ASTERISK, FIELD_DISABLED, FIELD_HEIGHT, FIELD_HELPER, FIELD_HELPER_GAP, FIELD_LABEL, FIELD_LABEL_GAP, FIELD_MIN_HEIGHT, FIELD_PADDING_X, FIELD_RADIUS, FIELD_STATE, FIELD_TEXT, FILE_KIND_META, MONTHS_FULL_EN, MONTHS_FULL_VI, MONTHS_SHORT_VI, OlAccordion, OlAccordionItem, OlAlert, OlAlertDialog, OlAppNav, OlAspectRatio, OlAutocomplete, OlAvatar, OlAvatarGroup, OlBackdrop, OlBadge, OlBanner, OlBottomNav, OlBottomNavItem, OlBreadcrumb, OlBreadcrumbItem, OlButton, OlButtonGroup, OlCard, OlCarousel, OlCarouselSlide, OlChart, OlChatBubble, OlChatContainer, OlCheckbox, OlCheckboxRadioForm, OlCheckboxRadioGroup, OlClipboard, OlCollapse, OlCommand, OlDateRangePicker, OlDatepicker, OlDatepickerDirective, OlDatepickerFooter, OlDatepickerHeader, OlDatepickerPopup, OlDatepickerStateService, OlDayView, OlDeviceMockup, OlDownloadAppButton, OlDrawer, OlDrawerFooter, OlDrawerHeader, OlDropdown, OlDropdownHeader, OlDropdownItem, OlEmpty, OlFileDropzone, OlFileInput, OlFileItem, OlFileUpload, OlFloatingLabel, OlFocusTrapService, OlFooter, OlFrame, OlGallery, OlGalleryItem, OlHelper, OlIcon, OlIconService, OlIconShape, OlIconStack, OlImagePreview, OlInput, OlInputCounter, OlItem, OlJumbotron, OlKbd, OlLabel, OlListGroup, OlListGroupItem, OlLoadingOverlay, OlMenubar, OlModal, OlModalFooter, OlModalHeader, OlMonthView, OlNavDropdown, OlNavbar, OlNavbarItem, OlOtpInput, OlOverlayContainer, OlPagination, OlPopover, OlPopoverContent, OlPositioningService, OlProgress, OlProgressBarService, OlProgressBarTop, OlRadio, OlRange, OlRating, OlResizable, OlRichTextEditor, OlScrollArea, OlScrollspy, OlSegmented, OlSelect, OlSeparator, OlSidebar, OlSidebarGroup, OlSidebarItem, OlSidebarSubmenu, OlSkeleton, OlSpeedDial, OlSpeedDialItem, OlSpinner, OlStepItem, OlStepper, OlTabItem, OlTable, OlTableLoading, OlTableRow, OlTabs, OlTagInput, OlTextarea, OlTimeline, OlTimelineItem, OlTimepicker, OlToast, OlToastContainer, OlToastService, OlToggle, OlToggleGroup, OlToggleInput, OlTooltip, OlTooltipDirective, OlTree, OlTreeNodeItem, OlYearView, WEEKDAYS_SHORT_EN, WEEKDAYS_SHORT_VI, boDau, cloneDate, detectFileKind, endOfDay, fieldSizeClasses, fileExtension, filesFromClipboard, formatDate, formatFileSize, generateMonthMatrix, generateMonthsMatrix, generateYearsMatrix, isBetween, isDateDisabled, isEditableTarget, isSameDay, isSameMonth, isSameYear, isToday, matchesAccept, parseDate, pasteShortcutLabel, startOfDay, stripDiacritics, toUploadFile, validateFiles };
7913
+ export type { AccordionVariant, AlertColor, AlertDialogTone, AlertLayout, AlertRounded, AlertType, AlertVariant, AppDevice, AppNavConfig, AppNavGroup, AppNavItem, AppNavUser, AppTheme, AutocompleteOption, AutocompleteSize, AutocompleteState, AvatarShape, AvatarSize, AvatarStatus, BadgeColor, BadgeShape, BadgeSize, BadgeTheme, BadgeType, BannerPosition, BannerVariant, BoxplotDataPoint, BreadcrumbItemStatus, BreadcrumbSeparator, BreadcrumbVariant, ButtonColor, ButtonGroupColor, ButtonGroupOrientation, ButtonGroupSize, ButtonSize, CardPadding, CardRadius, CardShadow, CardVariant, ChartDataPoint, ChartSeries, ChartType, ChatBubbleStatus, ChatSender, CheckboxSize, ChoiceControl, ChoiceControlPosition, ChoiceGroupLayout, ChoiceLayout, ChoiceShape, CommandGroup, CommandItem, DateCustomClass, DateRange, DatepickerPreset, DatepickerSelectionMode, DatepickerSize, DatepickerViewMode, DayCell, DeviceType, DrawerFooterLayout, DrawerPlacement, DrawerSize, DropdownAutoClose, DropdownPlacement, DropdownTrigger, EmptySize, FieldSize, FieldState, FileInputSize, FileItemVariant, FileKind, FileKindMeta, FileRejectReason, FileRejection, FileUploadMode, FileValidationResult, FileValidationRules, FloatingLabelSize, FloatingLabelState, FloatingLabelVariant, HeatmapData, HelperFeature, HelperRule, HelperSize, HelperState, IconColor, IconShapeColor, IconShapeSize, IconShapeType, IconSize, IconVariant, InputCounterSize, InputSize, InputState, JumbotronVariant, ListGroupType, ModalFooterLayout, ModalPlacement, ModalSize, MonthCell, OlAvatarStatusPosition, OlBottomNavPosition, OlButtonType, OlCheckboxRadioFormAlign, OlCheckboxRadioGroupRole, OlDateRangePickerVariant, OlFrameRounded, OlFrameSpacing, OlFrameVariant, OlGalleryGap, OlGalleryRatio, OlHelperType, OlIconStackSize, OlItemAlign, OlItemSize, OlMenubarItem, OlMenubarMenu, OlModalBackdrop, OlModalRole, OlPreviewImage, OlResizableDirection, OlScrollspyItem, OlScrollspyOrientation, OlSkeletonAnimation, OlSkeletonAspectRatio, OlSkeletonRounded, OlSkeletonType, OlSpeedDialTooltipPosition, OlTimepickerAddonType, OlToggleInputAlign, OlTreeNode, OlTreeNodeSelectionMode, OlTreeNodeToggleIcon, OlTreeSelectionMode, OlTreeToggleIcon, OlUploadFile, OtpInputSize, OverlayLayer, PaginationType, PaginationVariant, PopoverPlacement, PopoverTrigger, PositionedCoordinates, PositioningOptions, ProgressColor, ProgressLabelPosition, ProgressSize, RadarIndicator, RadioSize, RatingType, ScatterDataPoint, ScatterSeries, ScrollAreaAxis, ScrollAreaScrollbar, SegmentedOption, SegmentedSize, SelectOption, SelectSize, SelectState, SeparatorOrientation, SpeedDialAction, SpeedDialColor, SpeedDialDirection, SpeedDialTrigger, SpeedDialType, StepState, StepperBreakpoint, StepperOrientation, StepperVariant, TabsSize, TabsVariant, TagInputSize, TextareaState, TimeObject, TimepickerFormat, TimepickerSize, TimepickerState, Toast, ToastConfig, ToastPosition, ToggleColor, TogglePosition, ToggleSize, UploadStatus, YearCell };
6749
7914
  //# sourceMappingURL=edmicro-olkit.d.ts.map