1
0
mirror of https://github.com/axzilla/templui.git synced 2025-03-13 10:53:35 +00:00

feat(rating): initial implementation

This commit is contained in:
axzilla 2025-03-09 19:24:13 +07:00
parent 9e3ce2e6ea
commit a871c15d35
9 changed files with 1142 additions and 0 deletions

@ -7,6 +7,7 @@
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--color-red-500: oklch(0.637 0.237 25.331);
--color-yellow-400: oklch(0.852 0.199 91.936);
--color-yellow-500: oklch(0.795 0.184 86.047);
--color-green-500: oklch(0.723 0.219 149.579);
--color-green-700: oklch(0.527 0.154 150.069);
@ -772,6 +773,9 @@
.transform {
transform: var(--tw-rotate-x) var(--tw-rotate-y) var(--tw-rotate-z) var(--tw-skew-x) var(--tw-skew-y);
}
.cursor-default {
cursor: default;
}
.cursor-not-allowed {
cursor: not-allowed;
}
@ -1344,6 +1348,9 @@
.text-white {
color: var(--color-white);
}
.text-yellow-400 {
color: var(--color-yellow-400);
}
.text-yellow-500 {
color: var(--color-yellow-500);
}
@ -1365,6 +1372,9 @@
.opacity-0 {
opacity: 0%;
}
.opacity-30 {
opacity: 30%;
}
.opacity-50 {
opacity: 50%;
}

@ -114,6 +114,7 @@ func main() {
mux.Handle("GET /docs/components/pagination", templ.Handler(pages.Pagination()))
mux.Handle("GET /docs/components/radio", templ.Handler(pages.Radio()))
mux.Handle("GET /docs/components/radio-card", templ.Handler(pages.RadioCard()))
mux.Handle("GET /docs/components/rating", templ.Handler(pages.Rating()))
mux.Handle("GET /docs/components/select", templ.Handler(pages.Select()))
mux.Handle("GET /docs/components/sheet", templ.Handler(pages.Sheet()))
mux.Handle("GET /docs/components/slider", templ.Handler(pages.Slider()))

295
components/rating.templ Normal file

@ -0,0 +1,295 @@
package components
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
// RatingSize defines available sizes for the Rating component
type RatingSize string
const (
RatingSizeSmall RatingSize = "small"
RatingSizeMedium RatingSize = "medium"
RatingSizeLarge RatingSize = "large"
)
// RatingStyle defines the visual style for the Rating component
type RatingStyle string
const (
RatingStyleStar RatingStyle = "star" // Star icons for ratings
RatingStyleHeart RatingStyle = "heart" // Heart icons for ratings
RatingStyleEmoji RatingStyle = "emoji" // Emoji faces for ratings
RatingStyleNumeric RatingStyle = "numeric" // Numeric display (with optional icon)
)
// RatingProps configures the Rating component
type RatingProps struct {
Value float64 // Current rating value
MaxValue int // Maximum rating value (default: 5)
ReadOnly bool // Whether the rating is interactive or display-only
Style RatingStyle // Visual style for the rating
Size RatingSize // Size of the rating elements
Precision float64 // Step precision for fractional ratings (0.5, 0.1, etc.)
Label string // Optional label for the rating
Name string // Form field name (when used in forms)
ShowValue bool // Whether to display the numeric value
OnlyInteger bool // Force ratings to integer values
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
// Default values for the Rating component
func (p *RatingProps) setDefaults() {
if p.MaxValue <= 0 {
p.MaxValue = 5
}
if p.Precision <= 0 {
p.Precision = 1.0
}
if p.Style == "" {
p.Style = RatingStyleStar
}
if p.Size == "" {
p.Size = RatingSizeMedium
}
}
// Get size class based on the RatingSize
func getSizeClass(size RatingSize) string {
switch size {
case RatingSizeSmall:
return "text-lg"
case RatingSizeLarge:
return "text-3xl"
default: // Medium
return "text-2xl"
}
}
// Get color class based on the RatingStyle
func getColorClass(style RatingStyle) string {
switch style {
case RatingStyleHeart:
return "text-destructive"
case RatingStyleEmoji:
return "text-yellow-500"
default: // Star and others
return "text-yellow-400"
}
}
// Get item icon based on the RatingStyle
func getRatingIcon(style RatingStyle, filled bool, value float64) templ.Component {
// Emoji style uses different icons based on value
if style == RatingStyleEmoji {
if filled {
// Choose emoji based on rating level (1-5)
switch {
case value <= 1:
return icons.Angry(icons.IconProps{})
case value <= 2:
return icons.Frown(icons.IconProps{})
case value <= 3:
return icons.Meh(icons.IconProps{})
case value <= 4:
return icons.Smile(icons.IconProps{})
default:
return icons.Laugh(icons.IconProps{})
}
}
return icons.Meh(icons.IconProps{})
}
// Handle other styles
if filled {
switch style {
case RatingStyleHeart:
return icons.Heart(icons.IconProps{Fill: "currentColor"})
default: // Star
return icons.Star(icons.IconProps{Fill: "currentColor"})
}
} else {
switch style {
case RatingStyleHeart:
return icons.Heart(icons.IconProps{})
default: // Star
return icons.Star(icons.IconProps{})
}
}
}
// Interactive Rating component with Alpine.js
templ RatingScript() {
{{ handle := templ.NewOnceHandle() }}
@handle.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('rating', () => ({
value: 0,
maxValue: 5,
precision: 1,
readonly: false,
name: '',
onlyInteger: false,
previewValue: 0,
init() {
// Get configuration from data attributes
this.value = parseFloat(this.$el.dataset.value) || 0;
this.maxValue = parseInt(this.$el.dataset.maxvalue) || 5;
this.precision = parseFloat(this.$el.dataset.precision) || 1;
this.readonly = this.$el.dataset.readonly === 'true';
this.name = this.$el.dataset.name || '';
this.onlyInteger = this.$el.dataset.onlyinteger === 'true';
// Round value to the nearest step based on precision
this.value = Math.round(this.value / this.precision) * this.precision;
},
setValue() {
if (this.readonly) return;
// Get the rating value from the clicked element's data attribute
const item = this.$event.target.closest('[data-rating-value]');
if (!item) return;
const newValue = parseInt(item.dataset.ratingValue);
// Handle precision for fractional ratings
if (this.onlyInteger) {
this.value = Math.round(newValue);
} else {
this.value = Math.round(newValue / this.precision) * this.precision;
}
// Ensure value is within bounds
this.value = Math.max(0, Math.min(this.maxValue, this.value));
// Dispatch event for form integration
this.$dispatch('rating-change', {
name: this.name,
value: this.value
});
},
getFormattedValue() {
// Format the value for display (rounded to 2 decimal places)
return Math.round(this.value * 100) / 100;
},
getItemStyle() {
// Get the index from the element's data attribute
const index = parseInt(this.$el.dataset.index || '0');
// Calculate item styling
const filled = index <= Math.floor(this.value);
const partial = !filled && (index - 1 < this.value && this.value < index);
const percentage = partial ? (this.value - Math.floor(this.value)) * 100 : 0;
// Return appropriate width style
return {
width: filled ? '100%' : (partial ? percentage + '%' : '0%')
};
},
hover() {
if (this.readonly) return;
// Preview rating on hover
const item = this.$event.target.closest('[data-rating-value]');
if (!item) return;
this.previewValue = parseInt(item.dataset.ratingValue);
},
getCursorClass() {
return this.readonly ? 'cursor-default' : 'cursor-pointer';
}
}));
});
</script>
}
}
// Rating component for user ratings and feedback
templ Rating(props RatingProps) {
// Apply default values
{{ props.setDefaults() }}
<div
x-data="rating"
data-value={ strconv.FormatFloat(props.Value, 'f', -1, 64) }
data-maxvalue={ strconv.Itoa(props.MaxValue) }
data-precision={ strconv.FormatFloat(props.Precision, 'f', -1, 64) }
data-readonly={ strconv.FormatBool(props.ReadOnly) }
data-name={ props.Name }
data-onlyinteger={ strconv.FormatBool(props.OnlyInteger) }
class={ utils.TwMerge(
"flex flex-col items-start gap-1",
props.Class,
) }
{ props.Attributes... }
>
if props.Label != "" {
<label class="text-sm font-medium text-foreground">{ props.Label }</label>
}
<div class="flex items-center gap-1">
if props.Style != RatingStyleNumeric {
<!-- Standard rating items (stars, hearts, emojis) -->
for i := 1; i <= props.MaxValue; i++ {
<div
class={
utils.TwMerge(
"relative",
getSizeClass(props.Size),
getColorClass(props.Style),
"transition-opacity",
),
templ.KV("cursor-pointer", !props.ReadOnly),
templ.KV("cursor-default", props.ReadOnly),
}
data-rating-value={ strconv.Itoa(i) }
@click="setValue"
@mouseover="hover"
>
<div class="opacity-30">
@getRatingIcon(props.Style, false, float64(i))
</div>
<div
class="absolute inset-0 overflow-hidden"
x-bind:style="getItemStyle"
data-index={ strconv.Itoa(i) }
>
@getRatingIcon(props.Style, true, float64(i))
</div>
</div>
}
} else {
<!-- Numeric display with optional icon -->
<span class={ utils.TwMerge("font-bold", getSizeClass(props.Size)) }>
<span x-text="getFormattedValue"></span>
<span>/</span><span x-text="maxValue"></span>
</span>
<span class={ utils.TwMerge(getColorClass(RatingStyleStar), getSizeClass(props.Size)) }>
@icons.Star(icons.IconProps{})
</span>
}
<!-- Hidden input for form submission -->
if props.Name != "" {
<input
type="hidden"
name={ props.Name }
x-bind:value="value"
/>
}
<!-- Optional display of numeric value -->
if props.ShowValue && props.Style != RatingStyleNumeric {
<span class="ml-2 text-sm text-muted-foreground">
<span x-text="getFormattedValue"></span>
<span>/</span><span x-text="maxValue"></span>
</span>
}
</div>
</div>
}

530
components/rating_templ.go Normal file

@ -0,0 +1,530 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.833
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"github.com/axzilla/templui/icons"
"github.com/axzilla/templui/utils"
"strconv"
)
// RatingSize defines available sizes for the Rating component
type RatingSize string
const (
RatingSizeSmall RatingSize = "small"
RatingSizeMedium RatingSize = "medium"
RatingSizeLarge RatingSize = "large"
)
// RatingStyle defines the visual style for the Rating component
type RatingStyle string
const (
RatingStyleStar RatingStyle = "star" // Star icons for ratings
RatingStyleHeart RatingStyle = "heart" // Heart icons for ratings
RatingStyleEmoji RatingStyle = "emoji" // Emoji faces for ratings
RatingStyleNumeric RatingStyle = "numeric" // Numeric display (with optional icon)
)
// RatingProps configures the Rating component
type RatingProps struct {
Value float64 // Current rating value
MaxValue int // Maximum rating value (default: 5)
ReadOnly bool // Whether the rating is interactive or display-only
Style RatingStyle // Visual style for the rating
Size RatingSize // Size of the rating elements
Precision float64 // Step precision for fractional ratings (0.5, 0.1, etc.)
Label string // Optional label for the rating
Name string // Form field name (when used in forms)
ShowValue bool // Whether to display the numeric value
OnlyInteger bool // Force ratings to integer values
Class string // Additional CSS classes
Attributes templ.Attributes // Additional HTML attributes
}
// Default values for the Rating component
func (p *RatingProps) setDefaults() {
if p.MaxValue <= 0 {
p.MaxValue = 5
}
if p.Precision <= 0 {
p.Precision = 1.0
}
if p.Style == "" {
p.Style = RatingStyleStar
}
if p.Size == "" {
p.Size = RatingSizeMedium
}
}
// Get size class based on the RatingSize
func getSizeClass(size RatingSize) string {
switch size {
case RatingSizeSmall:
return "text-lg"
case RatingSizeLarge:
return "text-3xl"
default: // Medium
return "text-2xl"
}
}
// Get color class based on the RatingStyle
func getColorClass(style RatingStyle) string {
switch style {
case RatingStyleHeart:
return "text-destructive"
case RatingStyleEmoji:
return "text-yellow-500"
default: // Star and others
return "text-yellow-400"
}
}
// Get item icon based on the RatingStyle
func getRatingIcon(style RatingStyle, filled bool, value float64) templ.Component {
// Emoji style uses different icons based on value
if style == RatingStyleEmoji {
if filled {
// Choose emoji based on rating level (1-5)
switch {
case value <= 1:
return icons.Angry(icons.IconProps{})
case value <= 2:
return icons.Frown(icons.IconProps{})
case value <= 3:
return icons.Meh(icons.IconProps{})
case value <= 4:
return icons.Smile(icons.IconProps{})
default:
return icons.Laugh(icons.IconProps{})
}
}
return icons.Meh(icons.IconProps{})
}
// Handle other styles
if filled {
switch style {
case RatingStyleHeart:
return icons.Heart(icons.IconProps{Fill: "currentColor"})
default: // Star
return icons.Star(icons.IconProps{Fill: "currentColor"})
}
} else {
switch style {
case RatingStyleHeart:
return icons.Heart(icons.IconProps{})
default: // Star
return icons.Star(icons.IconProps{})
}
}
}
// Interactive Rating component with Alpine.js
func RatingScript() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
handle := templ.NewOnceHandle()
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<script defer nonce=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.GetNonce(ctx))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 128, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">\n document.addEventListener('alpine:init', () => {\n Alpine.data('rating', () => ({\n value: 0,\n maxValue: 5,\n precision: 1,\n readonly: false,\n name: '',\n onlyInteger: false,\n previewValue: 0,\n \n init() {\n // Get configuration from data attributes\n this.value = parseFloat(this.$el.dataset.value) || 0;\n this.maxValue = parseInt(this.$el.dataset.maxvalue) || 5;\n this.precision = parseFloat(this.$el.dataset.precision) || 1;\n this.readonly = this.$el.dataset.readonly === 'true';\n this.name = this.$el.dataset.name || '';\n this.onlyInteger = this.$el.dataset.onlyinteger === 'true';\n \n // Round value to the nearest step based on precision\n this.value = Math.round(this.value / this.precision) * this.precision;\n },\n \n setValue() {\n if (this.readonly) return;\n \n // Get the rating value from the clicked element's data attribute\n const item = this.$event.target.closest('[data-rating-value]');\n if (!item) return;\n \n const newValue = parseInt(item.dataset.ratingValue);\n \n // Handle precision for fractional ratings\n if (this.onlyInteger) {\n this.value = Math.round(newValue);\n } else {\n this.value = Math.round(newValue / this.precision) * this.precision;\n }\n \n // Ensure value is within bounds\n this.value = Math.max(0, Math.min(this.maxValue, this.value));\n \n // Dispatch event for form integration\n this.$dispatch('rating-change', { \n name: this.name, \n value: this.value \n });\n },\n \n getFormattedValue() {\n // Format the value for display (rounded to 2 decimal places)\n return Math.round(this.value * 100) / 100;\n },\n \n getItemStyle() {\n // Get the index from the element's data attribute\n const index = parseInt(this.$el.dataset.index || '0');\n \n // Calculate item styling\n const filled = index <= Math.floor(this.value);\n const partial = !filled && (index - 1 < this.value && this.value < index);\n const percentage = partial ? (this.value - Math.floor(this.value)) * 100 : 0;\n \n // Return appropriate width style\n return {\n width: filled ? '100%' : (partial ? percentage + '%' : '0%')\n };\n },\n \n hover() {\n if (this.readonly) return;\n // Preview rating on hover\n const item = this.$event.target.closest('[data-rating-value]');\n if (!item) return;\n \n this.previewValue = parseInt(item.dataset.ratingValue);\n },\n \n getCursorClass() {\n return this.readonly ? 'cursor-default' : 'cursor-pointer';\n }\n }));\n });\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = handle.Once().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Rating component for user ratings and feedback
func Rating(props RatingProps) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
props.setDefaults()
var templ_7745c5c3_Var5 = []any{utils.TwMerge(
"flex flex-col items-start gap-1",
props.Class,
)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div x-data=\"rating\" data-value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatFloat(props.Value, 'f', -1, 64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 222, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" data-maxvalue=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(props.MaxValue))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 223, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" data-precision=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatFloat(props.Precision, 'f', -1, 64))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 224, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" data-readonly=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatBool(props.ReadOnly))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 225, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" data-name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 226, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" data-onlyinteger=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.FormatBool(props.OnlyInteger))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 227, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var5).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.RenderAttributes(ctx, templ_7745c5c3_Buffer, props.Attributes)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if props.Label != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<label class=\"text-sm font-medium text-foreground\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(props.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 235, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</label>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"flex items-center gap-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if props.Style != RatingStyleNumeric {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<!-- Standard rating items (stars, hearts, emojis) -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for i := 1; i <= props.MaxValue; i++ {
var templ_7745c5c3_Var14 = []any{
utils.TwMerge(
"relative",
getSizeClass(props.Size),
getColorClass(props.Style),
"transition-opacity",
),
templ.KV("cursor-pointer", !props.ReadOnly),
templ.KV("cursor-default", props.ReadOnly),
}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" data-rating-value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 252, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" @click=\"setValue\" @mouseover=\"hover\"><div class=\"opacity-30\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = getRatingIcon(props.Style, false, float64(i)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div><div class=\"absolute inset-0 overflow-hidden\" x-bind:style=\"getItemStyle\" data-index=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(i))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 262, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = getRatingIcon(props.Style, true, float64(i)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<!-- Numeric display with optional icon --> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 = []any{utils.TwMerge("font-bold", getSizeClass(props.Size))}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\"><span x-text=\"getFormattedValue\"></span> <span>/</span><span x-text=\"maxValue\"></span></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 = []any{utils.TwMerge(getColorClass(RatingStyleStar), getSizeClass(props.Size))}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var20).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = icons.Star(icons.IconProps{}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<!-- Hidden input for form submission -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if props.Name != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<input type=\"hidden\" name=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(props.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `components/rating.templ`, Line: 282, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" x-bind:value=\"value\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<!-- Optional display of numeric value -->")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if props.ShowValue && props.Style != RatingStyleNumeric {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"ml-2 text-sm text-muted-foreground\"><span x-text=\"getFormattedValue\"></span> <span>/</span><span x-text=\"maxValue\"></span></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

@ -5,6 +5,7 @@ import "github.com/axzilla/templui/components"
// ComponentScripts returns script tags for all components.
templ ComponentScripts() {
@components.ChartScripts()
@components.RatingScript()
@components.CarouselScript()
@components.CodeScript()
@components.ToastScript()

@ -36,6 +36,10 @@ func ComponentScripts() templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.RatingScript().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = components.CarouselScript().Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err

@ -129,6 +129,10 @@ var Sections = []Section{
Text: "RadioCard",
Href: "/docs/components/radio-card",
},
{
Text: "Rating",
Href: "/docs/components/rating",
},
{
Text: "Select",
Href: "/docs/components/select",

@ -0,0 +1,70 @@
package pages
import (
"github.com/axzilla/templui/internal/ui/layouts"
"github.com/axzilla/templui/internal/ui/modules"
"github.com/axzilla/templui/internal/ui/showcase"
)
templ Rating() {
@layouts.DocsLayout(
"Rating",
"Interactive rating component for capturing user feedback and displaying scores.",
) {
@modules.PageWrapper(modules.PageWrapperProps{
Name: "Rating",
Description: templ.Raw("Interactive rating component for capturing user feedback and displaying scores."),
Tailwind: true,
Alpine: true,
}) {
@modules.ExampleWrapper(modules.ExampleWrapperProps{
ShowcaseFile: showcase.RatingDefault(),
PreviewCodeFile: "rating.templ",
ComponentCodeFile: "rating.templ",
})
@modules.Usage(modules.UsageProps{
ComponentName: "Rating",
NeedsHandler: true,
PropsExample: "Value: 4.5, MaxValue: 5, ReadOnly: false, Style: components.RatingStyleStar",
})
@modules.ContainerWrapper(modules.ContainerWrapperProps{Title: "Examples"}) {
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Read-Only",
ShowcaseFile: showcase.RatingReadOnly(),
PreviewCodeFile: "rating_readonly.templ",
ComponentCodeFile: "rating.templ",
})
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Sizes",
ShowcaseFile: showcase.RatingSizes(),
PreviewCodeFile: "rating_sizes.templ",
ComponentCodeFile: "rating.templ",
})
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Styles",
ShowcaseFile: showcase.RatingStyles(),
PreviewCodeFile: "rating_styles.templ",
ComponentCodeFile: "rating.templ",
})
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Precision",
ShowcaseFile: showcase.RatingPrecision(),
PreviewCodeFile: "rating_precision.templ",
ComponentCodeFile: "rating.templ",
})
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Custom Max Values",
ShowcaseFile: showcase.RatingMaxValues(),
PreviewCodeFile: "rating_max_values.templ",
ComponentCodeFile: "rating.templ",
})
@modules.ExampleWrapper(modules.ExampleWrapperProps{
SectionName: "Form Integration",
ShowcaseFile: showcase.RatingForm(),
PreviewCodeFile: "rating_form.templ",
ComponentCodeFile: "rating.templ",
})
}
}
}
}

@ -0,0 +1,227 @@
package showcase
import "github.com/axzilla/templui/components"
// Basic star rating showcase
templ RatingDefault() {
@components.Rating(components.RatingProps{
Value: 3.5,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Precision: 0.5,
ShowValue: true,
})
}
// Read-only rating showcase
templ RatingReadOnly() {
@components.Rating(components.RatingProps{
Value: 4.7,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
})
}
// Different sizes showcase
templ RatingSizes() {
<div class="flex flex-col gap-4">
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeSmall,
Label: "Small",
})
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Label: "Medium",
})
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeLarge,
Label: "Large",
})
</div>
}
// Different styles showcase
templ RatingStyles() {
<div class="flex flex-col gap-4">
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Label: "Star",
})
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleHeart,
Size: components.RatingSizeMedium,
Label: "Heart",
})
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleEmoji,
Size: components.RatingSizeMedium,
Label: "Emoji",
})
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: true,
Style: components.RatingStyleNumeric,
Size: components.RatingSizeMedium,
Label: "Numeric",
})
</div>
}
// Form integration showcase
templ RatingForm() {
<form class="max-w-sm mx-auto">
@components.Card(components.CardProps{}) {
@components.CardHeader() {
@components.CardTitle() {
Product Review
}
@components.CardDescription() {
Please rate our service
}
}
@components.CardContent() {
@components.FormItem(components.FormItemProps{}) {
@components.Rating(components.RatingProps{
Value: 3,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Precision: 1.0,
Label: "Product Quality",
Name: "product_quality",
})
}
@components.FormItem(components.FormItemProps{}) {
@components.Rating(components.RatingProps{
Value: 4,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleHeart,
Size: components.RatingSizeMedium,
Precision: 1.0,
Label: "Customer Service",
Name: "customer_service",
})
}
@components.FormItem(components.FormItemProps{}) {
@components.Rating(components.RatingProps{
Value: 2.5,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleEmoji,
Size: components.RatingSizeMedium,
Precision: 0.5,
Label: "Overall Experience",
Name: "overall_experience",
})
}
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{
Text: "Additional Comments",
For: "comments",
})
@components.Textarea(components.TextareaProps{
ID: "comments",
Name: "comments",
Placeholder: "Tell us what you think...",
Rows: 3,
})
}
}
@components.CardFooter() {
@components.Button(components.ButtonProps{
Type: "submit",
Text: "Submit Review",
})
}
}
</form>
}
// Custom precision showcase
templ RatingPrecision() {
<div class="flex flex-col gap-4">
@components.Rating(components.RatingProps{
Value: 3.7,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Precision: 1.0,
Label: "Integer Ratings",
ShowValue: true,
OnlyInteger: true,
})
@components.Rating(components.RatingProps{
Value: 3.7,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Precision: 0.5,
Label: "Half-Star Ratings",
ShowValue: true,
})
@components.Rating(components.RatingProps{
Value: 3.7,
MaxValue: 5,
ReadOnly: false,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Precision: 0.1,
Label: "Decimal Ratings (0.1)",
ShowValue: true,
})
</div>
}
// Custom max values showcase
templ RatingMaxValues() {
<div class="flex flex-col gap-4">
@components.Rating(components.RatingProps{
Value: 3,
MaxValue: 3,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Label: "3-Star Rating",
})
@components.Rating(components.RatingProps{
Value: 7,
MaxValue: 10,
ReadOnly: true,
Style: components.RatingStyleStar,
Size: components.RatingSizeMedium,
Label: "10-Star Rating",
ShowValue: true,
})
</div>
}