@leviosa-ai/detail-page-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/components/cardnews/color-input.tsx +380 -0
  2. package/components/detail-page/ai-generate-panel.tsx +763 -0
  3. package/components/detail-page/annotation-canvas.tsx +860 -0
  4. package/components/detail-page/annotation-dialog.tsx +397 -0
  5. package/components/detail-page/bubble-tail-overlay.tsx +209 -0
  6. package/components/detail-page/canvas-context-menu.tsx +234 -0
  7. package/components/detail-page/canvas-observer.tsx +68 -0
  8. package/components/detail-page/canvas-overlay-host.tsx +44 -0
  9. package/components/detail-page/canvas-store-facade.ts +51 -0
  10. package/components/detail-page/chart-inspector.tsx +531 -0
  11. package/components/detail-page/design-reference-picker.tsx +383 -0
  12. package/components/detail-page/detail-page-basic-shapes.tsx +180 -0
  13. package/components/detail-page/detail-page-brand-gifs-panel.tsx +261 -0
  14. package/components/detail-page/detail-page-brand-kit-panel.tsx +77 -0
  15. package/components/detail-page/detail-page-brand-panel-header.tsx +44 -0
  16. package/components/detail-page/detail-page-charts-panel.tsx +104 -0
  17. package/components/detail-page/detail-page-decorations-panel.tsx +125 -0
  18. package/components/detail-page/detail-page-download-dialog.tsx +524 -0
  19. package/components/detail-page/detail-page-editor.tsx +974 -0
  20. package/components/detail-page/detail-page-elements-panel.tsx +117 -0
  21. package/components/detail-page/detail-page-font-picker.tsx +231 -0
  22. package/components/detail-page/detail-page-history-buttons.tsx +87 -0
  23. package/components/detail-page/detail-page-host-context.tsx +465 -0
  24. package/components/detail-page/detail-page-icons-panel.tsx +251 -0
  25. package/components/detail-page/detail-page-konva-json-preview.tsx +474 -0
  26. package/components/detail-page/detail-page-layers-panel.tsx +618 -0
  27. package/components/detail-page/detail-page-my-images-panel.tsx +179 -0
  28. package/components/detail-page/detail-page-my-shapes-panel.tsx +174 -0
  29. package/components/detail-page/detail-page-page-toolbar.tsx +171 -0
  30. package/components/detail-page/detail-page-pages-panel.tsx +299 -0
  31. package/components/detail-page/detail-page-pages-timeline.tsx +79 -0
  32. package/components/detail-page/detail-page-photos-panel.tsx +257 -0
  33. package/components/detail-page/detail-page-properties-panel.tsx +2680 -0
  34. package/components/detail-page/detail-page-qr-panel.tsx +137 -0
  35. package/components/detail-page/detail-page-references-panel.tsx +166 -0
  36. package/components/detail-page/detail-page-selection.ts +44 -0
  37. package/components/detail-page/detail-page-shapes-panel.tsx +16 -0
  38. package/components/detail-page/detail-page-sidebar-sections.tsx +324 -0
  39. package/components/detail-page/detail-page-tables-panel.tsx +106 -0
  40. package/components/detail-page/detail-page-text-panel.tsx +179 -0
  41. package/components/detail-page/detail-page-thumbnail-bus.ts +42 -0
  42. package/components/detail-page/edit-quota-ui.tsx +150 -0
  43. package/components/detail-page/editor-history.ts +56 -0
  44. package/components/detail-page/editor-hotkeys.tsx +165 -0
  45. package/components/detail-page/element-recents-strip.tsx +105 -0
  46. package/components/detail-page/element-rects.ts +82 -0
  47. package/components/detail-page/fill-control.tsx +261 -0
  48. package/components/detail-page/find-replace-panel.tsx +252 -0
  49. package/components/detail-page/generation-progress-fill.tsx +57 -0
  50. package/components/detail-page/gif-animator.tsx +184 -0
  51. package/components/detail-page/gif-effect-picker.tsx +136 -0
  52. package/components/detail-page/group-drill-in.tsx +219 -0
  53. package/components/detail-page/group-prompt-edit-panel.tsx +154 -0
  54. package/components/detail-page/hover-highlight-overlay.tsx +111 -0
  55. package/components/detail-page/hovered-layer.ts +34 -0
  56. package/components/detail-page/inspector-controls.tsx +175 -0
  57. package/components/detail-page/leviosa-canvas-workspace.tsx +313 -0
  58. package/components/detail-page/nested-selection-overlay.tsx +364 -0
  59. package/components/detail-page/panel-search-input.tsx +49 -0
  60. package/components/detail-page/prompt-edit-panel.tsx +146 -0
  61. package/components/detail-page/section-height-handle.tsx +276 -0
  62. package/components/detail-page/section-reauthor-controller.tsx +210 -0
  63. package/components/detail-page/spec-prompt-edit-panel.tsx +178 -0
  64. package/components/detail-page/spec-resize-absorber.tsx +149 -0
  65. package/components/detail-page/svg-prompt-edit-panel.tsx +139 -0
  66. package/components/detail-page/table-canvas-overlay.tsx +707 -0
  67. package/components/detail-page/table-inspector.tsx +586 -0
  68. package/components/ui/button.tsx +64 -0
  69. package/components/ui/command.tsx +175 -0
  70. package/components/ui/dialog.tsx +167 -0
  71. package/components/ui/label.tsx +24 -0
  72. package/components/ui/select.tsx +190 -0
  73. package/components/ui/slider.tsx +63 -0
  74. package/components/ui/switch.tsx +35 -0
  75. package/components/ui/tooltip.tsx +57 -0
  76. package/config/detail-page-bundle-font-tags.json +39 -0
  77. package/config/detail-page-fonts.json +373 -0
  78. package/index.ts +24 -0
  79. package/lib/cardnews/font-loader.ts +306 -0
  80. package/lib/detail-page/animation-sniff.ts +101 -0
  81. package/lib/detail-page/asset-bytes-url.ts +70 -0
  82. package/lib/detail-page/basic-shapes.ts +603 -0
  83. package/lib/detail-page/canvas-menu.ts +182 -0
  84. package/lib/detail-page/chart/defaults.ts +283 -0
  85. package/lib/detail-page/chart/format.ts +66 -0
  86. package/lib/detail-page/chart/normalize.ts +149 -0
  87. package/lib/detail-page/chart/palette.ts +85 -0
  88. package/lib/detail-page/chart/paste.ts +82 -0
  89. package/lib/detail-page/chart/preview.ts +78 -0
  90. package/lib/detail-page/chart/render.ts +188 -0
  91. package/lib/detail-page/chart/renderers/bar-h.ts +103 -0
  92. package/lib/detail-page/chart/renderers/bar-v.ts +213 -0
  93. package/lib/detail-page/chart/renderers/donut.ts +186 -0
  94. package/lib/detail-page/chart/renderers/gauge.ts +96 -0
  95. package/lib/detail-page/chart/renderers/line.ts +143 -0
  96. package/lib/detail-page/chart/renderers/shared.ts +263 -0
  97. package/lib/detail-page/chart/renderers/stack.ts +121 -0
  98. package/lib/detail-page/chart/sync.ts +92 -0
  99. package/lib/detail-page/chart/types.ts +145 -0
  100. package/lib/detail-page/css-color.ts +34 -0
  101. package/lib/detail-page/data-gif-payload.ts +167 -0
  102. package/lib/detail-page/design-reference.ts +244 -0
  103. package/lib/detail-page/distribute.ts +89 -0
  104. package/lib/detail-page/editor-routes.ts +10 -0
  105. package/lib/detail-page/element-recents.ts +167 -0
  106. package/lib/detail-page/find-replace.ts +120 -0
  107. package/lib/detail-page/format-painter.ts +159 -0
  108. package/lib/detail-page/gif-frames.ts +216 -0
  109. package/lib/detail-page/gif-timeline.ts +65 -0
  110. package/lib/detail-page/group-action.ts +62 -0
  111. package/lib/detail-page/hangul-roman.ts +58 -0
  112. package/lib/detail-page/icon-curated.ts +290 -0
  113. package/lib/detail-page/icon-fuzzy.ts +250 -0
  114. package/lib/detail-page/icon-keywords.generated.ts +3766 -0
  115. package/lib/detail-page/icon-keywords.ko.ts +524 -0
  116. package/lib/detail-page/icon-search.ts +257 -0
  117. package/lib/detail-page/icons.ts +67 -0
  118. package/lib/detail-page/image-credit.ts +121 -0
  119. package/lib/detail-page/image-data-uri.ts +29 -0
  120. package/lib/detail-page/insert-image.ts +75 -0
  121. package/lib/detail-page/insert-shape.ts +49 -0
  122. package/lib/detail-page/korean-dictionary.ts +187 -0
  123. package/lib/detail-page/layer-move.ts +257 -0
  124. package/lib/detail-page/new-product-draft-store.ts +105 -0
  125. package/lib/detail-page/new-product-draft.ts +283 -0
  126. package/lib/detail-page/personal-images-refresh.ts +30 -0
  127. package/lib/detail-page/product-source.ts +587 -0
  128. package/lib/detail-page/qr-code.ts +214 -0
  129. package/lib/detail-page/reference-image.ts +103 -0
  130. package/lib/detail-page/reference-upload.ts +181 -0
  131. package/lib/detail-page/replace-page.ts +35 -0
  132. package/lib/detail-page/replace-with-gif.ts +251 -0
  133. package/lib/detail-page/section-height.ts +131 -0
  134. package/lib/detail-page/section-reauthor-bus.ts +61 -0
  135. package/lib/detail-page/shape-to-image.ts +219 -0
  136. package/lib/detail-page/spec-group/clipboard.ts +35 -0
  137. package/lib/detail-page/spec-group/sync.ts +377 -0
  138. package/lib/detail-page/spec-group/text-metrics.ts +48 -0
  139. package/lib/detail-page/stock-photos.ts +88 -0
  140. package/lib/detail-page/svg-colors.ts +74 -0
  141. package/lib/detail-page/table/defaults.ts +344 -0
  142. package/lib/detail-page/table/edit.ts +312 -0
  143. package/lib/detail-page/table/harvest.ts +103 -0
  144. package/lib/detail-page/table/normalize.ts +125 -0
  145. package/lib/detail-page/table/paste.ts +49 -0
  146. package/lib/detail-page/table/preview.ts +66 -0
  147. package/lib/detail-page/table/render.ts +388 -0
  148. package/lib/detail-page/table/sync.ts +107 -0
  149. package/lib/detail-page/table/types.ts +102 -0
  150. package/lib/detail-page/text-gif-layout.ts +245 -0
  151. package/lib/detail-page/text-gif-payload.ts +67 -0
  152. package/lib/detail-page/text-presets.ts +230 -0
  153. package/lib/detail-page/use-fake-progress.ts +67 -0
  154. package/lib/detail-page/z-order.ts +82 -0
  155. package/lib/detail-page-canvas/bubble-path.ts +190 -0
  156. package/lib/detail-page-canvas/canvas-key.ts +22 -0
  157. package/lib/detail-page-canvas/clip-rect.ts +65 -0
  158. package/lib/detail-page-canvas/custom-props-adapter.ts +219 -0
  159. package/lib/detail-page-canvas/editor-fonts.ts +161 -0
  160. package/lib/detail-page-canvas/export/ai.ts +413 -0
  161. package/lib/detail-page-canvas/export/color.ts +35 -0
  162. package/lib/detail-page-canvas/export/document-model.ts +78 -0
  163. package/lib/detail-page-canvas/export/export-files.ts +290 -0
  164. package/lib/detail-page-canvas/export/font-subset.ts +264 -0
  165. package/lib/detail-page-canvas/export/gif-export.ts +310 -0
  166. package/lib/detail-page-canvas/export/gif-plan.ts +95 -0
  167. package/lib/detail-page-canvas/export/mp4-encode.ts +158 -0
  168. package/lib/detail-page-canvas/export/mp4-support.ts +22 -0
  169. package/lib/detail-page-canvas/export/pdf/geometry.ts +309 -0
  170. package/lib/detail-page-canvas/export/pdf/inline-svg.ts +399 -0
  171. package/lib/detail-page-canvas/export/pdf/resources.ts +443 -0
  172. package/lib/detail-page-canvas/export/pdf/text-ops.ts +81 -0
  173. package/lib/detail-page-canvas/export/pdf/writer.ts +109 -0
  174. package/lib/detail-page-canvas/export/psd.ts +354 -0
  175. package/lib/detail-page-canvas/export/raster.ts +255 -0
  176. package/lib/detail-page-canvas/export/svg.ts +479 -0
  177. package/lib/detail-page-canvas/export/text-layout.ts +142 -0
  178. package/lib/detail-page-canvas/font-catalog.ts +177 -0
  179. package/lib/detail-page-canvas/font-tags.ts +47 -0
  180. package/lib/detail-page-canvas/gif-web-fonts.ts +90 -0
  181. package/lib/detail-page-canvas/konva-fallback.ts +477 -0
  182. package/lib/detail-page-canvas/text-background-plan.ts +124 -0
  183. package/lib/detail-page-canvas/text-highlight-bands.ts +127 -0
  184. package/lib/utils.ts +10 -0
  185. package/package.json +68 -0
  186. package/types/commerce.ts +171 -0
  187. package/types/detail-page-api.ts +200 -0
  188. package/types/detail-page-canvas.ts +20 -0
  189. package/types/fontkit.d.ts +42 -0
  190. package/types/gifenc.d.ts +51 -0
@@ -0,0 +1,290 @@
1
+ import type { IconStyle } from "./icon-search";
2
+
3
+ /**
4
+ * 아이콘 격자의 **첫 화면**에 놓을 것들. 라우트(`/api/icons`)가 검색어 없이 불릴 때 쓴다.
5
+ *
6
+ * 라우트가 아니라 여기 있는 이유는 둘이다. 하나, 라우트 파일은 HTTP 메서드 말고 다른 것을
7
+ * 내보낼 수 없어서 목록만 따로 테스트할 수 없다. 둘, 이건 로직이 아니라 **우리가 고른 것**이라
8
+ * 손으로 늘어나는 자리다 — 늘릴 때 지켜야 할 규칙을 테스트가 지키게 두는 편이 낫다.
9
+ */
10
+
11
+ /**
12
+ * 우리가 쓰는 세트와 그 우선순위. 인터리브 순서가 이 순서다.
13
+ *
14
+ * 선(Lucide·Tabler)을 앞에 두는 것은 상세페이지 기본 축이 선이기 때문이다.
15
+ * **이 목록은 편의일 뿐 보안 경계가 아니다** — 실제 판정은 SPDX가 한다.
16
+ */
17
+ export const ICON_PREFIXES = ["lucide", "tabler", "ph", "ri", "material-symbols"] as const;
18
+
19
+ /**
20
+ * 검색어가 없을 때 보여 줄 큐레이션. 상세페이지에서 손이 가장 자주 가는 것들이다.
21
+ * 빈 격자를 보여주지 않는다(`/api/stock-photos`가 `curated`로 하는 것과 같은 처리).
22
+ *
23
+ * **선/채움 두 벌을 따로 둔다.** 큐레이션을 한 벌만 두면 선/채움 토글이 검색어를 넣기
24
+ * 전까지 아무것도 안 바꾼다 — Lucide·Tabler는 세트 전체가 선이고 Phosphor `-fill`은
25
+ * 세트 전체가 채움이라, 같은 목록에 스타일 필터를 걸면 한쪽이 통째로 빈다.
26
+ *
27
+ * 갈래 순서가 곧 격자 순서다(세트 인터리브가 그 안에서 한 번 더 섞는다). 앞쪽 몇 줄이
28
+ * 검증·배송·결제인 것은 상세페이지에서 그 셋이 가장 먼저 필요하기 때문이다.
29
+ *
30
+ * **`conceptOf`가 같은 것을 하나로 접는다.** 그래서 두 벌 안에서 개념이 겹치면 조용히
31
+ * 사라진다 — `lucide:star`와 `tabler:star-filled`를 한 벌에 같이 두면 안 된다.
32
+ * 그리고 여기 적은 이름은 **전부 제공처에 실재하는 것으로 확인했다**. 오타는 404가
33
+ * 아니라 조용한 결번으로 나타나니(마크업 배치에서 빠질 뿐이다) 손으로 늘릴 때 주의.
34
+ */
35
+ export const CURATED_STROKE: readonly string[] = [
36
+ // 신뢰 · 인증
37
+ "lucide:check", "lucide:circle-check", "lucide:square-check", "tabler:rosette-discount-check",
38
+ "lucide:shield-check", "tabler:certificate", "lucide:award", "lucide:medal",
39
+ "lucide:trophy", "lucide:crown", "lucide:star", "lucide:heart",
40
+ "lucide:thumbs-up", "lucide:thumbs-down", "lucide:bookmark", "lucide:stamp",
41
+ "tabler:ribbon-health", "lucide:diamond", "lucide:hand-heart", "tabler:hand-click",
42
+
43
+ // 배송 · 물류
44
+ "lucide:truck", "tabler:truck-delivery", "lucide:rocket", "lucide:package",
45
+ "lucide:package-2", "tabler:package-import", "lucide:plane", "lucide:ship",
46
+ "lucide:train", "lucide:forklift", "tabler:building-warehouse", "lucide:route",
47
+ "lucide:map", "lucide:map-pin", "lucide:compass", "lucide:navigation",
48
+ "tabler:package-export", "tabler:truck-return", "lucide:arrow-right-left", "lucide:radar",
49
+ "lucide:flag", "lucide:globe",
50
+
51
+ // 결제 · 가격
52
+ "lucide:credit-card", "tabler:cash", "tabler:coin", "lucide:wallet",
53
+ "lucide:receipt", "tabler:discount", "lucide:percent", "lucide:tag",
54
+ "lucide:ticket", "lucide:gift", "lucide:shopping-cart", "lucide:shopping-bag",
55
+ "tabler:shopping-cart-plus", "tabler:pig-money", "tabler:coin-bitcoin", "tabler:receipt-refund",
56
+ "tabler:gift-card", "lucide:calculator", "tabler:file-invoice", "tabler:arrows-transfer-up",
57
+
58
+ // 문의 · 응대
59
+ "lucide:phone", "lucide:mail", "lucide:message-circle", "lucide:headset",
60
+ "tabler:messages", "lucide:user", "lucide:users", "lucide:help-circle",
61
+ "lucide:info", "lucide:alert-circle", "lucide:megaphone", "tabler:speakerphone",
62
+ "lucide:bell", "tabler:message-2", "lucide:star-half", "tabler:hand-stop",
63
+ "lucide:clipboard-check", "lucide:phone-call",
64
+
65
+ // 치수 · 규격
66
+ "lucide:ruler", "tabler:dimensions", "lucide:scale", "lucide:weight",
67
+ "lucide:thermometer", "lucide:zoom-in", "lucide:zoom-out", "tabler:arrows-vertical",
68
+ "tabler:arrows-horizontal", "lucide:angle", "lucide:grid-3x3", "lucide:layers",
69
+ "tabler:align-center", "tabler:aspect-ratio", "tabler:cube",
70
+
71
+ // 소재 · 관리
72
+ "lucide:droplet", "tabler:wash-machine", "tabler:ironing", "lucide:wind",
73
+ "tabler:bleach", "tabler:hand-sanitizer", "lucide:sun", "lucide:snowflake",
74
+ "lucide:leaf", "lucide:recycle", "lucide:feather", "tabler:needle",
75
+ "lucide:scissors", "tabler:needle-thread", "tabler:texture", "lucide:flower",
76
+ "lucide:droplet-off", "tabler:air-balloon", "tabler:virus", "lucide:sparkles",
77
+
78
+ // 안전 · 보증
79
+ "lucide:shield", "lucide:lock", "lucide:key", "lucide:alert-triangle",
80
+ "lucide:ban", "lucide:flame", "lucide:fire-extinguisher", "tabler:helmet",
81
+ "lucide:hand-grab", "tabler:face-mask", "tabler:plant", "lucide:eye",
82
+ "lucide:fingerprint", "lucide:list-check",
83
+
84
+ // 의류 · 잡화
85
+ "lucide:shirt", "tabler:shoe", "tabler:sock", "lucide:briefcase",
86
+ "lucide:backpack", "tabler:zip", "lucide:circle-dot", "tabler:eyeglass",
87
+ "tabler:sunglasses", "lucide:clock", "lucide:watch", "tabler:diamonds",
88
+ "lucide:umbrella", "tabler:hanger-2",
89
+
90
+ // 뷰티 · 욕실
91
+ "lucide:sparkle", "tabler:spray", "tabler:perfume", "lucide:brush",
92
+ "lucide:mirror-rectangular", "tabler:face-id", "tabler:hand-finger", "tabler:manual-gearbox",
93
+ "tabler:bottle", "tabler:bowl", "tabler:sun-high", "tabler:massage",
94
+ "lucide:bath", "tabler:dental",
95
+
96
+ // 식음료
97
+ "lucide:coffee", "tabler:cup", "lucide:wine", "lucide:beer",
98
+ "lucide:bottle-wine", "lucide:apple", "lucide:carrot", "tabler:bread",
99
+ "lucide:egg", "lucide:fish", "tabler:meat", "lucide:cake",
100
+ "lucide:ice-cream", "lucide:cookie", "lucide:milk", "tabler:bubble-tea",
101
+ "lucide:salad", "lucide:pizza", "lucide:chef-hat", "tabler:tools-kitchen-2",
102
+ "tabler:bowl-spoon", "tabler:cooker", "tabler:cheese", "lucide:candy",
103
+ "tabler:grain", "lucide:cherry", "lucide:broccoli", "lucide:nut",
104
+
105
+ // 가전 · 디지털
106
+ "tabler:device-tv", "lucide:monitor", "tabler:device-laptop", "tabler:device-mobile",
107
+ "tabler:device-tablet", "lucide:headphones", "lucide:speaker", "lucide:camera",
108
+ "lucide:printer", "lucide:keyboard", "lucide:mouse", "lucide:plug",
109
+ "lucide:battery", "lucide:wifi", "lucide:bluetooth", "lucide:usb",
110
+ "lucide:battery-charging", "tabler:propeller", "tabler:air-conditioning", "tabler:vacuum-cleaner",
111
+ "tabler:bulb", "tabler:device-remote", "tabler:microphone", "tabler:device-gamepad",
112
+ "lucide:drone", "tabler:robot", "lucide:cpu", "lucide:database",
113
+ "lucide:power",
114
+
115
+ // 가구 · 생활
116
+ "lucide:sofa", "lucide:bed", "lucide:armchair", "tabler:desk",
117
+ "tabler:door", "tabler:window", "lucide:home", "lucide:building",
118
+ "tabler:building-store", "lucide:lamp", "tabler:photo", "lucide:trash",
119
+ "tabler:basket", "lucide:broom", "lucide:hammer", "tabler:tool",
120
+ "tabler:paint", "ri:brush-2-line", "tabler:ladder", "lucide:archive",
121
+ "lucide:blinds", "tabler:stairs",
122
+
123
+ // 반려 · 유아
124
+ "tabler:paw", "lucide:dog", "lucide:cat", "lucide:bone",
125
+ "lucide:bird", "tabler:fish-bone", "tabler:dog-bowl", "tabler:baby-carriage",
126
+ "tabler:baby-bottle", "tabler:diaper", "lucide:puzzle", "lucide:blocks",
127
+ "lucide:balloon",
128
+
129
+ // 운동 · 야외
130
+ "tabler:barbell", "lucide:bike", "tabler:run", "tabler:yoga",
131
+ "tabler:ball-football", "tabler:ball-basketball", "tabler:ball-tennis", "tabler:swimming",
132
+ "lucide:mountain", "lucide:tent", "tabler:trekking", "tabler:campfire",
133
+ "tabler:fish-hook", "tabler:ski-jumping", "tabler:golf", "tabler:heartbeat",
134
+ "ri:footprint-line",
135
+
136
+ // 문구 · 사무
137
+ "lucide:book", "lucide:notebook", "lucide:pen", "lucide:pencil",
138
+ "lucide:paperclip", "lucide:folder", "lucide:file", "lucide:calendar",
139
+ "lucide:clipboard", "tabler:note", "tabler:briefcase-2", "lucide:eraser",
140
+ "tabler:file-certificate", "tabler:news", "tabler:id", "tabler:mail-opened",
141
+
142
+ // 조작 · 표시
143
+ "lucide:arrow-right", "lucide:arrow-up", "lucide:plus", "lucide:minus",
144
+ "lucide:x", "tabler:refresh", "lucide:download", "lucide:upload",
145
+ "lucide:share", "lucide:link", "lucide:copy", "tabler:filter",
146
+ "lucide:search", "lucide:settings", "tabler:menu-2", "tabler:player-play",
147
+ "tabler:player-pause", "lucide:lock-open", "tabler:trash-x", "lucide:eye-off",
148
+
149
+ // 강조 · 지표
150
+ "lucide:bolt", "lucide:trending-up", "lucide:trending-down", "lucide:chart-bar",
151
+ "lucide:chart-pie", "lucide:chart-line", "lucide:target", "tabler:flag-2",
152
+ "tabler:confetti", "tabler:gift-off", "tabler:circle-number-1", "lucide:podium",
153
+ "lucide:hourglass", "tabler:stopwatch", "tabler:alarm", "tabler:calendar-event",
154
+ "lucide:infinity",
155
+
156
+ // 자연 · 계절
157
+ "lucide:moon", "lucide:cloud", "lucide:cloud-rain", "lucide:cloud-snow",
158
+ "tabler:windsock", "tabler:tree", "lucide:rose", "tabler:wave-sine",
159
+ "lucide:glass-water", "tabler:world-longitude", "tabler:snowman",
160
+ ];
161
+
162
+ /** 채움 큐레이션. 같은 개념을 채움 축에서 다시 고른 것이다. */
163
+ export const CURATED_FILL: readonly string[] = [
164
+ // 신뢰 · 인증
165
+ "ph:check-fill", "tabler:circle-check-filled", "tabler:square-check-filled", "tabler:rosette-discount-check-filled",
166
+ "ph:shield-check-fill", "ph:certificate-fill", "ri:award-fill", "ph:medal-fill",
167
+ "ph:trophy-fill", "ph:crown-fill", "ph:star-fill", "ph:heart-fill",
168
+ "ph:thumbs-up-fill", "ph:thumbs-down-fill", "ph:bookmark-fill", "ph:stamp-fill",
169
+ "ph:diamond-fill", "ph:hand-heart-fill", "ph:hands-clapping-fill",
170
+
171
+ // 배송 · 물류
172
+ "ph:truck-fill", "ph:rocket-fill", "ph:package-fill", "material-symbols:package-2",
173
+ "ri:plane-fill", "ri:ship-fill", "ph:train-fill", "material-symbols:forklift",
174
+ "ph:warehouse-fill", "ri:route-fill", "ri:map-fill", "ph:map-pin-fill",
175
+ "ph:compass-fill", "ri:navigation-fill", "ri:exchange-fill", "ri:radar-fill",
176
+ "ph:flag-fill", "ph:globe-fill",
177
+
178
+ // 결제 · 가격
179
+ "ph:credit-card-fill", "ri:cash-fill", "ph:coin-fill", "ph:wallet-fill",
180
+ "ph:receipt-fill", "tabler:discount-filled", "ph:percent-fill", "ph:tag-fill",
181
+ "ph:ticket-fill", "ph:gift-fill", "ph:shopping-cart-fill", "ph:shopping-bag-fill",
182
+ "ph:trolley-fill", "ph:piggy-bank-fill", "tabler:coin-bitcoin-filled", "ri:refund-fill",
183
+ "tabler:gift-card-filled", "ph:calculator-fill", "tabler:file-invoice-filled", "tabler:send-filled",
184
+
185
+ // 문의 · 응대
186
+ "ph:phone-fill", "ri:mail-fill", "tabler:message-circle-filled", "ph:headset-fill",
187
+ "tabler:messages-filled", "ph:user-fill", "ph:users-fill", "tabler:help-circle-filled",
188
+ "ph:info-fill", "tabler:alert-circle-filled", "ph:megaphone-fill", "ph:bell-fill",
189
+ "ri:message-2-fill", "ph:star-half-fill", "ph:hand-fill", "tabler:clipboard-check-filled",
190
+ "ph:phone-call-fill",
191
+
192
+ // 치수 · 규격
193
+ "ph:ruler-fill", "tabler:scale-filled", "ri:weight-fill", "ph:thermometer-fill",
194
+ "ri:zoom-in-fill", "ri:zoom-out-fill", "ph:arrows-vertical-fill", "ph:arrows-horizontal-fill",
195
+ "ph:angle-fill", "material-symbols:grid-3x3", "material-symbols:layers", "material-symbols:align-center",
196
+ "ri:aspect-ratio-fill", "ph:cube-fill",
197
+
198
+ // 소재 · 관리
199
+ "tabler:droplet-filled", "ph:washing-machine-fill", "tabler:ironing-filled", "ph:wind-fill",
200
+ "ph:flask-fill", "ri:hand-sanitizer-fill", "ph:sun-fill", "ph:snowflake-fill",
201
+ "ph:leaf-fill", "ph:recycle-fill", "ph:feather-fill", "ph:needle-fill",
202
+ "ph:scissors-fill", "material-symbols:texture", "ph:flower-fill", "tabler:air-balloon-filled",
203
+ "ph:virus-fill", "tabler:sparkles-filled",
204
+
205
+ // 안전 · 보증
206
+ "ph:shield-fill", "ph:lock-fill", "ph:key-fill", "tabler:alert-triangle-filled",
207
+ "ph:prohibit-fill", "ph:flame-fill", "ph:fire-extinguisher-fill", "ph:hard-hat-fill",
208
+ "ph:boxing-glove-fill", "ph:face-mask-fill", "ph:plant-fill", "ph:eye-fill",
209
+ "ph:fingerprint-fill", "tabler:list-check-filled",
210
+
211
+ // 의류 · 잡화
212
+ "ri:shirt-fill", "ph:hoodie-fill", "ph:pants-fill", "ph:dress-fill",
213
+ "ph:boot-fill", "ph:sneaker-fill", "ph:sock-fill", "ph:baseball-cap-fill",
214
+ "ph:briefcase-fill", "ph:backpack-fill", "tabler:circle-dot-filled", "tabler:eyeglass-filled",
215
+ "ph:sunglasses-fill", "ph:clock-fill", "ph:watch-fill", "tabler:diamonds-filled",
216
+ "ph:umbrella-fill", "ph:belt-fill", "tabler:hanger-2-filled",
217
+
218
+ // 뷰티 · 욕실
219
+ "ph:sparkle-fill", "ph:spray-bottle-fill", "ri:brush-fill", "ph:smiley-fill",
220
+ "ph:hand-palm-fill", "ph:hair-dryer-fill", "tabler:manual-gearbox-filled", "ph:hand-soap-fill",
221
+ "tabler:bottle-filled", "ph:jar-fill", "tabler:sun-high-filled", "material-symbols:massage",
222
+ "tabler:bath-filled", "ph:tooth-fill",
223
+
224
+ // 식음료
225
+ "ph:coffee-fill", "ri:cup-fill", "ph:wine-fill", "ri:beer-fill",
226
+ "ph:beer-bottle-fill", "ri:apple-fill", "ph:carrot-fill", "ph:bread-fill",
227
+ "ph:egg-fill", "ph:fish-fill", "ph:cake-fill", "ph:ice-cream-fill",
228
+ "ph:cookie-fill", "tabler:milk-filled", "tabler:salad-filled", "ph:pizza-fill",
229
+ "ph:chef-hat-fill", "tabler:tools-kitchen-2-filled", "tabler:bowl-spoon-filled", "ph:cooking-pot-fill",
230
+ "ph:cheese-fill", "material-symbols:grain", "tabler:cherry-filled", "ph:nut-fill",
231
+
232
+ // 가전 · 디지털
233
+ "tabler:device-tv-filled", "ph:monitor-fill", "ph:laptop-fill", "ph:device-mobile-fill",
234
+ "ph:device-tablet-fill", "ph:headphones-fill", "ri:speaker-fill", "ph:camera-fill",
235
+ "ph:printer-fill", "ph:keyboard-fill", "ph:mouse-fill", "ph:plug-fill",
236
+ "ri:battery-fill", "ri:wifi-fill", "ph:bluetooth-fill", "ph:usb-fill",
237
+ "ph:battery-charging-fill", "ph:fan-fill", "tabler:bulb-filled", "tabler:device-remote-filled",
238
+ "ph:microphone-fill", "tabler:device-gamepad-filled", "ph:drone-fill", "ph:robot-fill",
239
+ "ph:cpu-fill", "ph:database-fill", "ph:power-fill",
240
+
241
+ // 가구 · 생활
242
+ "ri:sofa-fill", "ph:bed-fill", "ph:armchair-fill", "ph:desk-fill",
243
+ "ph:door-fill", "ri:window-fill", "ri:home-fill", "ph:building-fill",
244
+ "ph:storefront-fill", "ph:lamp-fill", "ph:potted-plant-fill", "tabler:photo-filled",
245
+ "ph:trash-fill", "ph:basket-fill", "ph:broom-fill", "ph:hammer-fill",
246
+ "ph:wrench-fill", "ph:screwdriver-fill", "ri:paint-fill", "ri:brush-2-fill",
247
+ "ph:ladder-fill", "ph:archive-fill", "material-symbols:curtains", "ph:rug-fill",
248
+ "ph:stairs-fill",
249
+
250
+ // 반려 · 유아
251
+ "tabler:paw-filled", "ph:dog-fill", "ph:cat-fill", "ph:bone-fill",
252
+ "ph:bird-fill", "tabler:fish-bone-filled", "ph:baby-carriage-fill", "ri:puzzle-fill",
253
+ "ph:balloon-fill",
254
+
255
+ // 운동 · 야외
256
+ "ph:barbell-fill", "ri:bike-fill", "ri:run-fill", "ph:soccer-ball-fill",
257
+ "ph:basketball-fill", "ph:tennis-ball-fill", "ph:person-simple-swim-fill", "tabler:mountain-filled",
258
+ "ph:tent-fill", "material-symbols:hiking", "ph:campfire-fill", "ph:person-simple-ski-fill",
259
+ "ph:golf-fill", "ph:heartbeat-fill", "ri:footprint-fill",
260
+
261
+ // 문구 · 사무
262
+ "ph:book-fill", "ph:notebook-fill", "ph:pen-fill", "ph:pencil-fill",
263
+ "ph:paperclip-fill", "ph:folder-fill", "ph:file-fill", "ph:calendar-fill",
264
+ "ph:clipboard-fill", "ph:note-fill", "ri:briefcase-2-fill", "ph:eraser-fill",
265
+ "ri:file-check-fill", "ri:news-fill", "tabler:id-filled", "tabler:mail-opened-filled",
266
+
267
+ // 조작 · 표시
268
+ "ph:arrow-right-fill", "ph:arrow-up-fill", "ph:plus-fill", "ph:minus-fill",
269
+ "ph:x-fill", "ri:refresh-fill", "ph:download-fill", "ph:upload-fill",
270
+ "ph:share-fill", "ph:link-fill", "ph:copy-fill", "ri:filter-fill",
271
+ "ri:search-fill", "ri:settings-fill", "ri:menu-2-fill", "tabler:player-play-filled",
272
+ "tabler:player-pause-filled", "ph:lock-open-fill", "tabler:trash-x-filled", "ri:eye-off-fill",
273
+
274
+ // 강조 · 지표
275
+ "tabler:bolt-filled", "material-symbols:trending-up", "material-symbols:trending-down", "ph:chart-bar-fill",
276
+ "ph:chart-pie-fill", "ph:chart-line-fill", "ph:target-fill", "ri:flag-2-fill",
277
+ "ph:confetti-fill", "ri:gift-2-fill", "tabler:circle-number-1-filled", "ph:ranking-fill",
278
+ "ph:hourglass-fill", "ph:alarm-fill", "ri:calendar-event-fill", "ph:infinity-fill",
279
+
280
+ // 자연 · 계절
281
+ "ph:moon-fill", "ph:cloud-fill", "ph:cloud-rain-fill", "ph:cloud-snow-fill",
282
+ "tabler:windsock-filled", "ph:tree-fill", "ph:flower-lotus-fill", "ph:wave-sine-fill",
283
+ "ph:drop-half-fill", "ph:planet-fill",
284
+ ];
285
+
286
+ export function curatedFor(style: IconStyle | undefined): readonly string[] {
287
+ if (style === "fill") return CURATED_FILL;
288
+ if (style === "stroke") return CURATED_STROKE;
289
+ return [...CURATED_STROKE, ...CURATED_FILL];
290
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * "정확히 맞은 것 먼저, 비슷한 것은 그 아래."
3
+ *
4
+ * 아이콘 검색이 두 가지로 새고 있었다.
5
+ *
6
+ * 1. **오타 한 글자에 0건.** `aple`을 치면 제공처 검색이 아무것도 안 준다. 사람은
7
+ * 자기가 틀린 줄 모르고 "그런 아이콘이 없다"고 읽는다.
8
+ * 2. **외래어가 안 걸린다.** "애플"은 사전에 없으면 0건인데, 정작 `apple`로 치면 나온다.
9
+ * 한글로 치는 사람만 손해를 본다.
10
+ *
11
+ * 그래서 이름을 **자리표(skeleton)**로 뭉갠 뒤 견준다. 모음과 겹글자를 걷어내고 같은
12
+ * 소리를 내는 자음을 한 글자로 모으면, `apple`·`aple`·`애플`이 전부 `apl`로 만난다.
13
+ *
14
+ * 등급을 매기는 이유는 순서 때문이다. 비슷한 것을 정확한 것과 섞어 버리면 검색이
15
+ * 나빠진다 — 아래로 내려보내야 "덤"이 된다.
16
+ */
17
+
18
+ import { hasHangul, romanizeHangul } from "./hangul-roman";
19
+
20
+ /** 얼마나 잘 맞았나. 큰 것이 위로 간다. */
21
+ export const MATCH_EXACT = 3;
22
+ export const MATCH_PARTIAL = 2;
23
+ export const MATCH_FUZZY = 1;
24
+ export const MATCH_NONE = 0;
25
+
26
+ export type MatchTier =
27
+ | typeof MATCH_EXACT
28
+ | typeof MATCH_PARTIAL
29
+ | typeof MATCH_FUZZY
30
+ | typeof MATCH_NONE;
31
+
32
+ /**
33
+ * 견줄 수 있는 한 가지 모양으로 뭉갠다.
34
+ *
35
+ * 규칙은 전부 "한글로 옮겨 적을 때 흔들리는 자리"다 — `c/k`, `ph/f`, `x/ks`, 겹자음,
36
+ * 끝의 묵음 `e`. 모음은 남긴다(모음까지 지우면 `bell`과 `ball`이 같아진다).
37
+ */
38
+ export function skeleton(text: string): string {
39
+ let out = text.toLowerCase().replace(/[^a-z]/g, "");
40
+ if (!out) return "";
41
+ out = out
42
+ .replace(/ph/g, "f")
43
+ .replace(/ck/g, "k")
44
+ .replace(/c/g, "k")
45
+ .replace(/q/g, "k")
46
+ .replace(/x/g, "ks")
47
+ .replace(/z/g, "s")
48
+ .replace(/(.)\1+/g, "$1");
49
+ // 끝의 묵음 e — `apple`과 `appl`이 갈리면 안 된다.
50
+ if (out.length > 2 && out.endsWith("e")) out = out.slice(0, -1);
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * 한글 낱말의 자리표. 로마자로 옮긴 뒤 **한국어가 끼워 넣는 모음을 뺀다.**
56
+ *
57
+ * "플"의 `eu`(ㅡ)와 "트"의 `eu`는 영어에 없던 소리다 — 그걸 빼야 `aepeul`이 `apl`이
58
+ * 되어 `apple`과 만난다. `ae`(ㅐ)와 `eo`(ㅓ)도 영어의 `a`·`o` 자리라 되돌린다.
59
+ */
60
+ export function koreanSkeleton(text: string): string {
61
+ const roman = romanizeHangul(text);
62
+ const folded = roman
63
+ .toLowerCase()
64
+ .replace(/eu/g, "")
65
+ .replace(/ae/g, "a")
66
+ .replace(/eo/g, "o")
67
+ .replace(/oe/g, "o");
68
+ return skeleton(folded);
69
+ }
70
+
71
+ /**
72
+ * 한글 자리표에서 **영어 쪽으로 갈릴 수 있는 자리**를 되돌린 후보들.
73
+ *
74
+ * 자리표를 더 뭉개는 길도 있었지만 그쪽은 못 쓴다. `f`와 `p`를 합치면 `folder`와
75
+ * `fold`가 같은 자리표가 되어 "폴더"가 `calendar-fold` 계열을 물어 온다(실측 415건).
76
+ * 뭉개기는 **모든 이름에** 걸리기 때문이다.
77
+ *
78
+ * 그래서 반대로 간다. 이름은 건드리지 않고 **질의 쪽에서만** 후보를 펼친 뒤
79
+ * *정확히* 같은 것을 찾는다. 흔들리는 자리는 넷뿐이다.
80
+ *
81
+ * - ㅍ은 `p`이자 `f`다 — "필터" ↔ `filter`
82
+ * - ㄹ은 `r`이자 `l`이다 — "링크" ↔ `link`
83
+ * - 종성 ㅇ은 늘 `ng`인데 영어는 그 자리가 `n`인 것이 많다 — `ringk` ↔ `link`
84
+ * - 영어의 약모음 꼬리를 한국어는 ㅓ/ㅡ로 받는다 — "폴더" ↔ `folder`
85
+ *
86
+ * 정확히 같은 것만 세므로 후보를 늘려도 잡음이 거의 안 는다. `fold`처럼 진짜로 겹치는
87
+ * 말만 남는데, 그것들은 어차피 맨 아래 등급이다.
88
+ */
89
+ export function pronunciationVariants(base: string): string[] {
90
+ if (base.length < 3) return [];
91
+
92
+ let set = new Set([base]);
93
+ const branch = (fold: (word: string) => string) => {
94
+ const next = new Set(set);
95
+ for (const word of set) {
96
+ const folded = fold(word);
97
+ if (folded !== word) next.add(folded);
98
+ }
99
+ set = next;
100
+ };
101
+ branch((word) => word.replace(/p/g, "f"));
102
+ branch((word) => word.replace(/r/g, "l"));
103
+ branch((word) => word.replace(/ng/g, "n"));
104
+
105
+ const withTails = new Set(set);
106
+ for (const word of set) {
107
+ if (!/[ou]$/.test(word)) continue;
108
+ for (const tail of ["er", "or", "ar", "e", "a", ""]) {
109
+ withTails.add(word.slice(0, -1) + tail);
110
+ }
111
+ }
112
+ return [...withTails].filter((word) => word.length >= 3);
113
+ }
114
+
115
+ /**
116
+ * 질의 하나의 후보 집합. `matchTier`는 이름마다 불리므로(3.6만 번) 여기서 한 번만 편다.
117
+ */
118
+ const variantMemo = new Map<string, ReadonlySet<string>>();
119
+
120
+ function variantsFor(term: string): ReadonlySet<string> {
121
+ const hit = variantMemo.get(term);
122
+ if (hit) return hit;
123
+ const made = new Set(pronunciationVariants(koreanSkeleton(term)));
124
+ // 질의는 몇 개 안 된다. 늘어나면 통째로 버린다.
125
+ if (variantMemo.size > 200) variantMemo.clear();
126
+ variantMemo.set(term, made);
127
+ return made;
128
+ }
129
+
130
+ /** 두 낱말 사이의 편집 거리. `max`를 넘으면 그 자리에서 그만둔다(빨라야 한다). */
131
+ export function editDistance(a: string, b: string, max = 3): number {
132
+ if (a === b) return 0;
133
+ if (Math.abs(a.length - b.length) > max) return max + 1;
134
+ if (!a.length || !b.length) return Math.max(a.length, b.length);
135
+
136
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
137
+ for (let i = 1; i <= a.length; i += 1) {
138
+ const row = [i];
139
+ let best = i;
140
+ for (let j = 1; j <= b.length; j += 1) {
141
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
142
+ const value = Math.min(row[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
143
+ row.push(value);
144
+ if (value < best) best = value;
145
+ }
146
+ // 이 줄에서 이미 전부 max를 넘었으면 더 볼 것이 없다.
147
+ if (best > max) return max + 1;
148
+ prev = row;
149
+ }
150
+ return prev[b.length];
151
+ }
152
+
153
+ /**
154
+ * 낱말 길이에 맞춘 오타 허용치. 짧은 말일수록 빡빡해야 한다.
155
+ *
156
+ * 네 글자까지 한 글자도 안 봐주는 것은 실측 때문이다. `kopi`(커피)에 한 글자를 허용하면
157
+ * `copy`와 `cookie`가 딸려 오고, `star`에 한 글자를 허용하면 `start`가 붙은 정렬
158
+ * 아이콘이 수백 개 들어온다. 짧은 말에서는 한 글자가 곧 다른 말이다.
159
+ */
160
+ function tolerance(word: string): number {
161
+ if (word.length <= 4) return 0;
162
+ if (word.length <= 7) return 1;
163
+ return 2;
164
+ }
165
+
166
+ /** 아이콘 이름을 견줄 조각들로. `arrow-right-fill` → `["arrow","right","fill"]`. */
167
+ function words(name: string): string[] {
168
+ return name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
169
+ }
170
+
171
+ /**
172
+ * 이 이름이 이 낱말들에 얼마나 맞는가.
173
+ *
174
+ * 이름 전체가 그 말이면 최고점, 이름 안에 그 말이 조각으로 들어 있으면 그 다음,
175
+ * 자리표가 오타 한두 개 거리면 마지막 등급이다.
176
+ */
177
+ export function matchTier(
178
+ name: string,
179
+ terms: readonly string[],
180
+ /**
181
+ * 오타·소리로 되짚어도 되는 말들. 기본은 `terms` 전부다.
182
+ *
183
+ * 라우트는 여기에 **사용자가 실제로 친 말만** 넣는다. 사전이 옮겨 준 영어까지
184
+ * 흐리게 견주면 얻는 것 없이 잃기만 한다 — "별점"의 `stars`가 `align-start`를
185
+ * 물어 오는 식이다(실측 79건). 사전 낱말은 이미 정확한 영어다.
186
+ */
187
+ fuzzyTerms: readonly string[] = terms,
188
+ ): MatchTier {
189
+ const flat = name.toLowerCase();
190
+ const parts = words(name);
191
+ const joined = parts.join("");
192
+ let best: MatchTier = MATCH_NONE;
193
+
194
+ for (const raw of terms) {
195
+ const term = raw.toLowerCase().trim();
196
+ if (!term) continue;
197
+
198
+ if (flat === term || joined === term.replace(/[^a-z0-9]/g, "")) return MATCH_EXACT;
199
+ // `apple`로 쳤을 때 `apple-fill`·`apple-logo`도 정확히 맞은 것으로 친다.
200
+ if (parts[0] === term) return MATCH_EXACT;
201
+
202
+ // 조각이 **통째로** 그 말일 때만 친다. 글자만 들어 있으면 되게 두면
203
+ // `star`가 `align-start`를 물어 온다(실측 207건).
204
+ if (parts.includes(term)) {
205
+ best = Math.max(best, MATCH_PARTIAL) as MatchTier;
206
+ continue;
207
+ }
208
+
209
+ if (!fuzzyTerms.includes(raw)) continue;
210
+ const korean = hasHangul(term);
211
+ const key = korean ? koreanSkeleton(term) : skeleton(term);
212
+ if (key.length < 3) continue;
213
+ // 소리 후보는 한글에만 편다. 영어로 친 말은 이미 영어라 펼 것이 없다.
214
+ const sounds = korean ? variantsFor(term) : null;
215
+ const limit = tolerance(key);
216
+ for (const part of [joined, ...parts]) {
217
+ const other = skeleton(part);
218
+ if (other.length < 3) continue;
219
+ if (sounds?.has(other) || editDistance(key, other, limit) <= limit) {
220
+ best = Math.max(best, MATCH_FUZZY) as MatchTier;
221
+ break;
222
+ }
223
+ }
224
+ }
225
+ return best;
226
+ }
227
+
228
+ /**
229
+ * 이름 목록을 등급별로 나눈다. 위 등급부터 내보내면 "정확한 것 먼저"가 된다.
230
+ *
231
+ * `ids`는 `"prefix:name"`이다 — 등급은 이름만 보고 매긴다.
232
+ */
233
+ export function tierIds(
234
+ ids: readonly string[],
235
+ terms: readonly string[],
236
+ fuzzyTerms: readonly string[] = terms,
237
+ ): { exact: string[]; partial: string[]; fuzzy: string[] } {
238
+ const exact: string[] = [];
239
+ const partial: string[] = [];
240
+ const fuzzy: string[] = [];
241
+ for (const id of ids) {
242
+ const colon = id.indexOf(":");
243
+ const name = colon > 0 ? id.slice(colon + 1) : id;
244
+ const tier = matchTier(name, terms, fuzzyTerms);
245
+ if (tier === MATCH_EXACT) exact.push(id);
246
+ else if (tier === MATCH_PARTIAL) partial.push(id);
247
+ else if (tier === MATCH_FUZZY) fuzzy.push(id);
248
+ }
249
+ return { exact, partial, fuzzy };
250
+ }