senren-ui 0.2.0 → 0.4.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +238 -0
  3. data/CONTRIBUTING.md +11 -0
  4. data/README.md +64 -0
  5. data/docs/i18n.md +194 -0
  6. data/docs/visual_style.md +80 -0
  7. data/lib/generators/senren/install/install_generator.rb +39 -0
  8. data/lib/generators/senren/install/templates/base_component.rb.tt +136 -7
  9. data/lib/generators/senren/install/templates/conventions.md.tt +57 -0
  10. data/lib/generators/senren/install/templates/senren_themes.css.tt +339 -0
  11. data/lib/senren/rails/agent_rules_writer.rb +7 -0
  12. data/lib/senren/rails/base_component_patch.rb +24 -0
  13. data/lib/senren/rails/component_copier.rb +21 -0
  14. data/lib/senren/rails/host_paths.rb +5 -0
  15. data/lib/senren/rails/locale_installer.rb +90 -0
  16. data/lib/senren/rails/version.rb +1 -1
  17. data/lib/senren/rails.rb +1 -0
  18. data/lib/tasks/senren.rake +9 -0
  19. data/templates/components/accordion/accordion_component.html.erb +24 -6
  20. data/templates/components/accordion/accordion_component.rb +2 -1
  21. data/templates/components/alert_dialog/alert_dialog_component.html.erb +7 -4
  22. data/templates/components/alert_dialog/alert_dialog_component.rb +4 -0
  23. data/templates/components/api_key_field/api_key_field_component.html.erb +2 -2
  24. data/templates/components/api_key_field/api_key_field_component.rb +8 -1
  25. data/templates/components/app_shell/app_shell_component.rb +2 -1
  26. data/templates/components/breadcrumb/breadcrumb_component.html.erb +8 -6
  27. data/templates/components/breadcrumb/breadcrumb_component.rb +2 -1
  28. data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +1 -1
  29. data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +4 -2
  30. data/templates/components/button/button_component.html.erb +1 -1
  31. data/templates/components/button/button_component.rb +27 -1
  32. data/templates/components/calendar/calendar_component.html.erb +2 -2
  33. data/templates/components/calendar/calendar_component.rb +37 -2
  34. data/templates/components/card/card_component.html.erb +4 -6
  35. data/templates/components/carousel/carousel_component.html.erb +6 -6
  36. data/templates/components/carousel/carousel_component.rb +12 -1
  37. data/templates/components/cart/cart_component.html.erb +13 -7
  38. data/templates/components/cart/cart_component.rb +11 -2
  39. data/templates/components/clipboard/clipboard_component.html.erb +3 -3
  40. data/templates/components/clipboard/clipboard_component.rb +6 -1
  41. data/templates/components/collapsible/collapsible_component.html.erb +3 -3
  42. data/templates/components/collapsible/collapsible_component.rb +1 -1
  43. data/templates/components/combobox/combobox_component.html.erb +3 -3
  44. data/templates/components/combobox/combobox_component.rb +2 -1
  45. data/templates/components/command/command_component.html.erb +1 -1
  46. data/templates/components/command/command_component.rb +3 -1
  47. data/templates/components/context_menu/context_menu_component.html.erb +6 -3
  48. data/templates/components/data_table/data_table_component.html.erb +3 -3
  49. data/templates/components/data_table/data_table_component.rb +16 -1
  50. data/templates/components/date_picker/date_picker_component.html.erb +5 -5
  51. data/templates/components/date_picker/date_picker_component.rb +2 -1
  52. data/templates/components/dialog/dialog_component.html.erb +9 -5
  53. data/templates/components/dialog/dialog_component.rb +11 -0
  54. data/templates/components/dropdown_menu/dropdown_menu_component.html.erb +6 -3
  55. data/templates/components/dropdown_menu/dropdown_menu_component.rb +57 -5
  56. data/templates/components/filter_bar/filter_bar_component.rb +1 -1
  57. data/templates/components/form/form_component.html.erb +1 -1
  58. data/templates/components/form/form_component.rb +21 -0
  59. data/templates/components/hover_card/hover_card_component.html.erb +6 -3
  60. data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +11 -9
  61. data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +11 -3
  62. data/templates/components/link/link_component.html.erb +1 -1
  63. data/templates/components/native_select/native_select_component.html.erb +2 -2
  64. data/templates/components/native_select/native_select_component.rb +12 -3
  65. data/templates/components/pagination/pagination_component.html.erb +4 -4
  66. data/templates/components/pagination/pagination_component.rb +2 -1
  67. data/templates/components/popover/popover_component.html.erb +6 -3
  68. data/templates/components/product_card/product_card_component.html.erb +3 -3
  69. data/templates/components/product_card/product_card_component.rb +2 -2
  70. data/templates/components/progress/progress_component.html.erb +2 -2
  71. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +2 -16
  72. data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +32 -1
  73. data/templates/components/search_input/search_input_component.html.erb +2 -2
  74. data/templates/components/search_input/search_input_component.rb +2 -1
  75. data/templates/components/sheet/sheet_component.html.erb +15 -6
  76. data/templates/components/sheet/sheet_component.rb +11 -0
  77. data/templates/components/shortcut_key/shortcut_key_component.html.erb +2 -2
  78. data/templates/components/sidebar/sidebar_component.html.erb +4 -4
  79. data/templates/components/sidebar/sidebar_component.rb +4 -1
  80. data/templates/components/switch/switch_component.html.erb +1 -1
  81. data/templates/components/table/table_component.html.erb +2 -2
  82. data/templates/components/tabs/tabs_component.html.erb +3 -3
  83. data/templates/components/tabs/tabs_component.rb +4 -2
  84. data/templates/components/theme_toggle/theme_toggle_component.html.erb +17 -3
  85. data/templates/components/theme_toggle/theme_toggle_component.rb +6 -1
  86. data/templates/components/tooltip/tooltip_component.html.erb +5 -3
  87. data/templates/components/top_nav/top_nav_component.html.erb +3 -3
  88. data/templates/components/top_nav/top_nav_component.rb +3 -2
  89. data/templates/controllers/accordion_controller.js +68 -3
  90. data/templates/controllers/api_key_field_controller.js +3 -2
  91. data/templates/controllers/carousel_controller.js +6 -1
  92. data/templates/controllers/cart_controller.js +3 -1
  93. data/templates/controllers/clipboard_controller.js +3 -3
  94. data/templates/controllers/rich_text_editor_lite_controller.js +2 -2
  95. data/templates/controllers/theme_toggle_controller.js +13 -3
  96. data/templates/locales/senren.en.yml +151 -0
  97. data/templates/locales/senren.vi.yml +151 -0
  98. metadata +6 -1
@@ -6,6 +6,19 @@ export default class extends Controller {
6
6
  static targets = ["trigger", "panel"]
7
7
  static values = { multiple: Boolean }
8
8
 
9
+ // Every pending close timer, so disconnect() can clear them. A panel mid-close
10
+ // when Turbo swaps the page would otherwise leave a timer holding a reference
11
+ // to a detached element -- caught by bin/performance, which fails the build
12
+ // for a timer no disconnect() clears.
13
+ connect() {
14
+ this._timers = new Set()
15
+ }
16
+
17
+ disconnect() {
18
+ this._timers?.forEach(clearTimeout)
19
+ this._timers?.clear()
20
+ }
21
+
9
22
  toggle(event) {
10
23
  const trigger = event.currentTarget
11
24
  const panel = this._panelFor(trigger.dataset.panelId)
@@ -13,12 +26,64 @@ export default class extends Controller {
13
26
 
14
27
  if (!this.multipleValue) this._closeAll()
15
28
  trigger.setAttribute("aria-expanded", nextOpen ? "true" : "false")
16
- if (panel) panel.hidden = !nextOpen
29
+ this._setOpen(panel, nextOpen)
30
+ }
31
+
32
+ // The panel animates from grid-template-rows 0fr to 1fr, and it also carries
33
+ // the `hidden` attribute so collapsed content is not focusable or read aloud.
34
+ // Those two want opposite things: `hidden` has to be off for the browser to
35
+ // animate anything, and on once the panel is closed.
36
+ //
37
+ // Opening: drop `hidden`, then flip the row on the next frame -- setting both
38
+ // in one go gives the browser no first value to animate from, and the panel
39
+ // snaps open.
40
+ //
41
+ // Closing: flip the row now, restore `hidden` when the transition ends.
42
+ _setOpen(panel, open) {
43
+ if (!panel) return
44
+
45
+ if (open) {
46
+ panel.hidden = false
47
+ requestAnimationFrame(() => {
48
+ panel.classList.remove("grid-rows-[0fr]")
49
+ panel.classList.add("grid-rows-[1fr]")
50
+ })
51
+ return
52
+ }
53
+
54
+ panel.classList.remove("grid-rows-[1fr]")
55
+ panel.classList.add("grid-rows-[0fr]")
56
+ this._hideAfterTransition(panel)
57
+ }
58
+
59
+ // `transitionend` alone is not enough: it never fires when the panel is
60
+ // already collapsed, when a reduced-motion setting removes the transition, or
61
+ // when the element is off-screen. Without the timeout those panels would stay
62
+ // focusable forever, so whichever arrives first wins and the other is dropped.
63
+ _hideAfterTransition(panel) {
64
+ const finish = () => {
65
+ clearTimeout(timer)
66
+ this._timers?.delete(timer)
67
+ panel.removeEventListener("transitionend", onEnd)
68
+ if (panel.classList.contains("grid-rows-[0fr]")) panel.hidden = true
69
+ }
70
+
71
+ const onEnd = (event) => {
72
+ if (event.target === panel && event.propertyName === "grid-template-rows") finish()
73
+ }
74
+
75
+ const timer = setTimeout(finish, 300)
76
+ this._timers?.add(timer)
77
+ panel.addEventListener("transitionend", onEnd)
17
78
  }
18
79
 
19
80
  _closeAll() {
20
- this.triggerTargets.forEach((trigger) => { trigger.setAttribute("aria-expanded", "false") })
21
- this.panelTargets.forEach((panel) => { panel.hidden = true })
81
+ this.triggerTargets.forEach((trigger) => {
82
+ trigger.setAttribute("aria-expanded", "false")
83
+ })
84
+ this.panelTargets.forEach((panel) => {
85
+ this._setOpen(panel, false)
86
+ })
22
87
  }
23
88
 
24
89
  _panelFor(id) {
@@ -6,7 +6,8 @@ export default class extends Controller {
6
6
  static values = {
7
7
  revealLabel: String,
8
8
  hideLabel: String,
9
- copyLabel: String
9
+ copyLabel: String,
10
+ copiedStatus: String
10
11
  }
11
12
 
12
13
  connect() {
@@ -31,6 +32,6 @@ export default class extends Controller {
31
32
  this.inputTarget.setSelectionRange(0, 0)
32
33
  }
33
34
 
34
- if (this.hasStatusTarget) this.statusTarget.textContent = `${this.copyLabelValue} complete`
35
+ if (this.hasStatusTarget) this.statusTarget.textContent = this.copiedStatusValue
35
36
  }
36
37
  }
@@ -4,6 +4,7 @@ import { Controller } from "@hotwired/stimulus"
4
4
  // Local UI: previous/next controls, dots, and arrow-key navigation.
5
5
  export default class extends Controller {
6
6
  static targets = ["slide", "dot", "status"]
7
+ static values = { statusTemplate: String }
7
8
 
8
9
  connect() {
9
10
  this.index = 0
@@ -45,6 +46,10 @@ export default class extends Controller {
45
46
  this.dotTargets.forEach((dot, dotIndex) => {
46
47
  dot.setAttribute("aria-current", dotIndex === this.index ? "true" : "false")
47
48
  })
48
- if (this.hasStatusTarget) this.statusTarget.textContent = `Slide ${this.index + 1} of ${count}`
49
+ if (this.hasStatusTarget && this.statusTemplateValue) {
50
+ this.statusTarget.textContent = this.statusTemplateValue
51
+ .replaceAll("%{current}", this.index + 1)
52
+ .replaceAll("%{total}", count)
53
+ }
49
54
  }
50
55
  }
@@ -27,9 +27,11 @@ export default class extends Controller {
27
27
  if (!line) return
28
28
 
29
29
  const id = line.dataset.lineId
30
+ // Read before the node goes away.
31
+ const removeUrl = line.dataset.removeUrl || null
30
32
  line.remove()
31
33
  this._recalculate()
32
- this.dispatch("removed", { detail: { id, subtotalCents: this.subtotalCentsValue } })
34
+ this.dispatch("removed", { detail: { id, removeUrl, subtotalCents: this.subtotalCentsValue } })
33
35
  }
34
36
 
35
37
  // The value is the state; this renders it. Called on initialization too, so
@@ -4,7 +4,7 @@ import { Controller } from "@hotwired/stimulus"
4
4
  // Local UI: copy text to the clipboard and announce success.
5
5
  export default class extends Controller {
6
6
  static targets = ["source", "button", "status"]
7
- static values = { copiedLabel: String }
7
+ static values = { copiedLabel: String, copiedStatus: String }
8
8
 
9
9
  async copy() {
10
10
  const value = this.sourceTarget.value || this.sourceTarget.textContent
@@ -13,8 +13,8 @@ export default class extends Controller {
13
13
  if (!this.hasButtonTarget) return
14
14
 
15
15
  const original = this.buttonTarget.textContent
16
- this.buttonTarget.textContent = this.copiedLabelValue || "Copied"
17
- if (this.hasStatusTarget) this.statusTarget.textContent = "Copied to clipboard"
16
+ if (this.copiedLabelValue) this.buttonTarget.textContent = this.copiedLabelValue
17
+ if (this.hasStatusTarget) this.statusTarget.textContent = this.copiedStatusValue
18
18
 
19
19
  clearTimeout(this._resetTimer)
20
20
  this._resetTimer = setTimeout(() => {
@@ -16,7 +16,7 @@ const DROPPED_PASTE_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "
16
16
  // Local UI: tiny contenteditable toolbar synced to a hidden textarea.
17
17
  export default class extends Controller {
18
18
  static targets = ["editor", "input", "button"]
19
- static values = { debug: Boolean }
19
+ static values = { debug: Boolean, linkPrompt: String }
20
20
 
21
21
  connect() {
22
22
  this.savedRange = null
@@ -59,7 +59,7 @@ export default class extends Controller {
59
59
  this.debug("format:start", () => ({ command, before: this.snapshot() }))
60
60
  this.restoreSelection()
61
61
  if (command === "createLink") {
62
- const url = window.prompt("Paste a URL")
62
+ const url = window.prompt(this.linkPromptValue)
63
63
  this.debug("createLink:prompt", () => ({ url, afterPrompt: this.snapshot() }))
64
64
  if (!url) return
65
65
  this.restoreSelection()
@@ -3,7 +3,8 @@ import { Controller } from "@hotwired/stimulus"
3
3
  // senren--theme-toggle
4
4
  // Local UI: toggles the document dark class and stores the preference.
5
5
  export default class extends Controller {
6
- static targets = ["label", "icon"]
6
+ static targets = ["label", "iconLight", "iconDark"]
7
+ static values = { lightLabel: String, darkLabel: String }
7
8
 
8
9
  connect() {
9
10
  this._sync()
@@ -18,7 +19,16 @@ export default class extends Controller {
18
19
  _sync() {
19
20
  const dark = document.documentElement.classList.contains("dark")
20
21
  this.element.setAttribute("aria-pressed", dark ? "true" : "false")
21
- if (this.hasLabelTarget) this.labelTarget.textContent = dark ? "Light theme" : "Dark theme"
22
- if (this.hasIconTarget) this.iconTarget.textContent = dark ? "L" : "D"
22
+ if (this.hasLabelTarget) this.labelTarget.textContent = dark ? this.lightLabelValue : this.darkLabelValue
23
+
24
+ // The icon names the theme the click switches *to*, matching the label: in
25
+ // light mode the label reads "Dark theme", so the moon is showing. Showing
26
+ // the current state instead would have icon and label contradict each other.
27
+ //
28
+ // Swapping visibility rather than rewriting one element's text also means
29
+ // the icons are markup the server rendered, so the right one is on screen
30
+ // before this controller connects.
31
+ if (this.hasIconLightTarget) this.iconLightTarget.hidden = !dark
32
+ if (this.hasIconDarkTarget) this.iconDarkTarget.hidden = dark
23
33
  }
24
34
  }
@@ -0,0 +1,151 @@
1
+ # Senren UI - English strings. This is the canonical wording.
2
+ #
3
+ # GENERATED by bin/i18n-sync from the senren_t(key, default:) calls in
4
+ # templates/components. Do not edit it by hand: change the `default:` in the
5
+ # component and run bin/i18n-sync, or a test will fail.
6
+ #
7
+ # Copy this file to senren.<locale>.yml, change the top-level `en:` to your
8
+ # locale, and translate the values. Keep every %{variable} - they are filled in
9
+ # when the component renders. A key you leave out falls back to the English.
10
+ en:
11
+ senren:
12
+ shared:
13
+ open: "Open"
14
+ close: "Close"
15
+ accordion:
16
+ section: "Section %{number}"
17
+ api_key_field:
18
+ label: "API key"
19
+ reveal: "Reveal"
20
+ hide: "Hide"
21
+ copy: "Copy"
22
+ copied_status: "%{label} complete"
23
+ app_shell:
24
+ skip_to_content: "Skip to content"
25
+ breadcrumb:
26
+ label: "Breadcrumb"
27
+ bulk_action_bar:
28
+ items: "items"
29
+ selected: "%{count} %{item_label} selected"
30
+ ready: "Selection ready"
31
+ calendar:
32
+ title: "%{month} %{year}"
33
+ months:
34
+ january: "January"
35
+ february: "February"
36
+ march: "March"
37
+ april: "April"
38
+ may: "May"
39
+ june: "June"
40
+ july: "July"
41
+ august: "August"
42
+ september: "September"
43
+ october: "October"
44
+ november: "November"
45
+ december: "December"
46
+ weekdays:
47
+ sun: "Sun"
48
+ mon: "Mon"
49
+ tue: "Tue"
50
+ wed: "Wed"
51
+ thu: "Thu"
52
+ fri: "Fri"
53
+ sat: "Sat"
54
+ carousel:
55
+ label: "Carousel"
56
+ previous: "Previous slide"
57
+ next: "Next slide"
58
+ go_to: "Go to slide %{number}"
59
+ status: "Slide %{current} of %{total}"
60
+ cart:
61
+ title: "Cart"
62
+ empty: "Your cart is empty."
63
+ checkout: "Checkout"
64
+ subtotal: "Subtotal"
65
+ remove: "Remove"
66
+ quantity_group: "%{name} quantity"
67
+ decrease: "Decrease %{name} quantity"
68
+ increase: "Increase %{name} quantity"
69
+ remove_item: "Remove %{name}"
70
+ clipboard:
71
+ copy: "Copy"
72
+ copied: "Copied"
73
+ copied_status: "Copied to clipboard"
74
+ collapsible:
75
+ details: "Details"
76
+ combobox:
77
+ placeholder: "Search..."
78
+ command:
79
+ placeholder: "Type a command..."
80
+ label: "Command menu"
81
+ empty: "No results found."
82
+ data_table:
83
+ empty: "No records found."
84
+ sort: "Sort"
85
+ date_picker:
86
+ placeholder: "Select date"
87
+ today: "Today"
88
+ clear: "Clear"
89
+ filter_bar:
90
+ label: "Filters"
91
+ invite_member_dialog:
92
+ title: "Invite teammate"
93
+ description: "Send an invitation to join this workspace."
94
+ role_member: "Member"
95
+ role_admin: "Admin"
96
+ button: "Invite member"
97
+ email: "Email"
98
+ email_placeholder: "teammate@company.com"
99
+ role: "Role"
100
+ cancel: "Cancel"
101
+ send: "Send invite"
102
+ pagination:
103
+ label: "Pagination"
104
+ previous: "Previous"
105
+ next: "Next"
106
+ product_card:
107
+ add_to_cart: "Add to cart"
108
+ out_of_stock: "Out of stock"
109
+ no_image: "No image"
110
+ rich_text_editor_lite:
111
+ label: "Content"
112
+ placeholder: "Write something..."
113
+ link_prompt: "Paste a URL"
114
+ toolbar:
115
+ paragraph: "Paragraph"
116
+ heading_1: "Heading 1"
117
+ heading_2: "Heading 2"
118
+ heading_3: "Heading 3"
119
+ bold: "Bold"
120
+ italic: "Italic"
121
+ create_link: "Create link"
122
+ bulleted_list: "Bulleted list"
123
+ numbered_list: "Numbered list"
124
+ align_left: "Align left"
125
+ align_center: "Align center"
126
+ align_right: "Align right"
127
+ justify: "Justify"
128
+ toolbar_short:
129
+ link: "Link"
130
+ list: "List"
131
+ left: "Left"
132
+ center: "Center"
133
+ right: "Right"
134
+ justify: "Justify"
135
+ search_input:
136
+ placeholder: "Search..."
137
+ label: "Search"
138
+ sidebar:
139
+ brand: "Senren"
140
+ label: "Primary"
141
+ toggle: "Toggle sidebar"
142
+ tabs:
143
+ label: "Tabs"
144
+ tab: "Tab %{number}"
145
+ theme_toggle:
146
+ label: "Toggle theme"
147
+ light: "Light theme"
148
+ dark: "Dark theme"
149
+ top_nav:
150
+ label: "Global"
151
+ brand: "Senren"
@@ -0,0 +1,151 @@
1
+ # Senren UI - Vietnamese strings (Tiếng Việt).
2
+ #
3
+ # Written by hand, in the register of a Vietnamese product or support team:
4
+ # short and plain, with the words people already say in English ("email", "slide",
5
+ # "tab") left as they are. The keys and every %{variable} match senren.en.yml;
6
+ # test/i18n_catalog_test.rb fails if they stop matching.
7
+ #
8
+ # Copy this file to config/locales/ (bin/rails senren:locales vi), then change any
9
+ # wording you like - it is your file. A key you delete falls back to the English.
10
+ vi:
11
+ senren:
12
+ shared:
13
+ open: "Mở"
14
+ close: "Đóng"
15
+ accordion:
16
+ section: "Mục %{number}"
17
+ api_key_field:
18
+ label: "Khóa API"
19
+ reveal: "Hiện"
20
+ hide: "Ẩn"
21
+ copy: "Sao chép"
22
+ copied_status: "%{label} xong"
23
+ app_shell:
24
+ skip_to_content: "Chuyển đến nội dung chính"
25
+ breadcrumb:
26
+ label: "Đường dẫn"
27
+ bulk_action_bar:
28
+ items: "mục"
29
+ selected: "Đã chọn %{count} %{item_label}"
30
+ ready: "Đã chọn xong"
31
+ calendar:
32
+ title: "%{month} năm %{year}"
33
+ months:
34
+ january: "Tháng 1"
35
+ february: "Tháng 2"
36
+ march: "Tháng 3"
37
+ april: "Tháng 4"
38
+ may: "Tháng 5"
39
+ june: "Tháng 6"
40
+ july: "Tháng 7"
41
+ august: "Tháng 8"
42
+ september: "Tháng 9"
43
+ october: "Tháng 10"
44
+ november: "Tháng 11"
45
+ december: "Tháng 12"
46
+ weekdays:
47
+ sun: "CN"
48
+ mon: "T2"
49
+ tue: "T3"
50
+ wed: "T4"
51
+ thu: "T5"
52
+ fri: "T6"
53
+ sat: "T7"
54
+ carousel:
55
+ label: "Trình chiếu"
56
+ previous: "Slide trước"
57
+ next: "Slide sau"
58
+ go_to: "Đến slide %{number}"
59
+ status: "Slide %{current} trên %{total}"
60
+ cart:
61
+ title: "Giỏ hàng"
62
+ empty: "Giỏ hàng của bạn đang trống."
63
+ checkout: "Thanh toán"
64
+ subtotal: "Tạm tính"
65
+ remove: "Xóa"
66
+ quantity_group: "Số lượng %{name}"
67
+ decrease: "Giảm số lượng %{name}"
68
+ increase: "Tăng số lượng %{name}"
69
+ remove_item: "Xóa %{name}"
70
+ clipboard:
71
+ copy: "Sao chép"
72
+ copied: "Đã sao chép"
73
+ copied_status: "Đã sao chép vào bộ nhớ tạm"
74
+ collapsible:
75
+ details: "Chi tiết"
76
+ combobox:
77
+ placeholder: "Tìm kiếm..."
78
+ command:
79
+ placeholder: "Nhập lệnh..."
80
+ label: "Menu lệnh"
81
+ empty: "Không tìm thấy kết quả."
82
+ data_table:
83
+ empty: "Không có dữ liệu."
84
+ sort: "Sắp xếp"
85
+ date_picker:
86
+ placeholder: "Chọn ngày"
87
+ today: "Hôm nay"
88
+ clear: "Xóa"
89
+ filter_bar:
90
+ label: "Bộ lọc"
91
+ invite_member_dialog:
92
+ title: "Mời đồng nghiệp"
93
+ description: "Gửi lời mời tham gia không gian làm việc này."
94
+ role_member: "Thành viên"
95
+ role_admin: "Quản trị viên"
96
+ button: "Mời thành viên"
97
+ email: "Email"
98
+ email_placeholder: "ten@congty.com"
99
+ role: "Vai trò"
100
+ cancel: "Hủy"
101
+ send: "Gửi lời mời"
102
+ pagination:
103
+ label: "Phân trang"
104
+ previous: "Trước"
105
+ next: "Sau"
106
+ product_card:
107
+ add_to_cart: "Thêm vào giỏ"
108
+ out_of_stock: "Hết hàng"
109
+ no_image: "Chưa có ảnh"
110
+ rich_text_editor_lite:
111
+ label: "Nội dung"
112
+ placeholder: "Nhập nội dung..."
113
+ link_prompt: "Dán một URL"
114
+ toolbar:
115
+ paragraph: "Đoạn văn"
116
+ heading_1: "Tiêu đề 1"
117
+ heading_2: "Tiêu đề 2"
118
+ heading_3: "Tiêu đề 3"
119
+ bold: "In đậm"
120
+ italic: "In nghiêng"
121
+ create_link: "Tạo liên kết"
122
+ bulleted_list: "Danh sách dấu đầu dòng"
123
+ numbered_list: "Danh sách đánh số"
124
+ align_left: "Căn trái"
125
+ align_center: "Căn giữa"
126
+ align_right: "Căn phải"
127
+ justify: "Căn đều"
128
+ toolbar_short:
129
+ link: "Liên kết"
130
+ list: "Danh sách"
131
+ left: "Trái"
132
+ center: "Giữa"
133
+ right: "Phải"
134
+ justify: "Đều"
135
+ search_input:
136
+ placeholder: "Tìm kiếm..."
137
+ label: "Tìm kiếm"
138
+ sidebar:
139
+ brand: "Senren"
140
+ label: "Điều hướng chính"
141
+ toggle: "Bật/tắt thanh bên"
142
+ tabs:
143
+ label: "Các tab"
144
+ tab: "Tab %{number}"
145
+ theme_toggle:
146
+ label: "Đổi giao diện"
147
+ light: "Giao diện sáng"
148
+ dark: "Giao diện tối"
149
+ top_nav:
150
+ label: "Điều hướng chung"
151
+ brand: "Senren"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: senren-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - vutt
@@ -55,6 +55,7 @@ files:
55
55
  - Rakefile
56
56
  - docs/components.md
57
57
  - docs/hot_reload.md
58
+ - docs/i18n.md
58
59
  - docs/performance_testing.md
59
60
  - docs/visual_style.md
60
61
  - lib/commands/senren/add/add_command.rb
@@ -69,6 +70,7 @@ files:
69
70
  - lib/generators/senren/install/templates/conventions.md.tt
70
71
  - lib/generators/senren/install/templates/installed_components.yml.tt
71
72
  - lib/generators/senren/install/templates/senren.css.tt
73
+ - lib/generators/senren/install/templates/senren_themes.css.tt
72
74
  - lib/senren-ui.rb
73
75
  - lib/senren/rails.rb
74
76
  - lib/senren/rails/agent_rules_writer.rb
@@ -80,6 +82,7 @@ files:
80
82
  - lib/senren/rails/engine.rb
81
83
  - lib/senren/rails/host_paths.rb
82
84
  - lib/senren/rails/llms_writer.rb
85
+ - lib/senren/rails/locale_installer.rb
83
86
  - lib/senren/rails/marker_block.rb
84
87
  - lib/senren/rails/registry.rb
85
88
  - lib/senren/rails/safe_write.rb
@@ -243,6 +246,8 @@ files:
243
246
  - templates/controllers/tabs_controller.js
244
247
  - templates/controllers/theme_toggle_controller.js
245
248
  - templates/controllers/tooltip_controller.js
249
+ - templates/locales/senren.en.yml
250
+ - templates/locales/senren.vi.yml
246
251
  homepage: https://www.senren-ui.dev/
247
252
  licenses:
248
253
  - MIT